@askexenow/exe-os 0.8.41 → 0.8.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/bin/backfill-conversations.js +805 -642
  2. package/dist/bin/backfill-responses.js +804 -641
  3. package/dist/bin/backfill-vectors.js +791 -634
  4. package/dist/bin/cleanup-stale-review-tasks.js +788 -631
  5. package/dist/bin/cli.js +1345 -660
  6. package/dist/bin/exe-agent.js +20 -1
  7. package/dist/bin/exe-assign.js +1503 -1343
  8. package/dist/bin/exe-boot.js +2518 -1798
  9. package/dist/bin/exe-call.js +39 -1
  10. package/dist/bin/exe-cloud.js +15 -1
  11. package/dist/bin/exe-dispatch.js +39 -2
  12. package/dist/bin/exe-doctor.js +790 -633
  13. package/dist/bin/exe-export-behaviors.js +792 -637
  14. package/dist/bin/exe-forget.js +145 -0
  15. package/dist/bin/exe-gateway.js +2500 -1877
  16. package/dist/bin/exe-heartbeat.js +147 -1
  17. package/dist/bin/exe-kill.js +795 -640
  18. package/dist/bin/exe-launch-agent.js +2168 -2008
  19. package/dist/bin/exe-link.js +28 -2
  20. package/dist/bin/exe-new-employee.js +25 -3
  21. package/dist/bin/exe-pending-messages.js +146 -1
  22. package/dist/bin/exe-pending-notifications.js +788 -631
  23. package/dist/bin/exe-pending-reviews.js +147 -1
  24. package/dist/bin/exe-rename.js +23 -0
  25. package/dist/bin/exe-review.js +490 -327
  26. package/dist/bin/exe-search.js +154 -3
  27. package/dist/bin/exe-session-cleanup.js +2466 -413
  28. package/dist/bin/exe-status.js +474 -317
  29. package/dist/bin/exe-team.js +474 -317
  30. package/dist/bin/git-sweep.js +2690 -150
  31. package/dist/bin/graph-backfill.js +794 -637
  32. package/dist/bin/graph-export.js +798 -641
  33. package/dist/bin/scan-tasks.js +2951 -44
  34. package/dist/bin/setup.js +62 -26
  35. package/dist/bin/shard-migrate.js +792 -637
  36. package/dist/bin/wiki-sync.js +794 -637
  37. package/dist/gateway/index.js +2504 -1895
  38. package/dist/hooks/bug-report-worker.js +2118 -576
  39. package/dist/hooks/commit-complete.js +2689 -149
  40. package/dist/hooks/error-recall.js +154 -3
  41. package/dist/hooks/ingest-worker.js +1439 -815
  42. package/dist/hooks/instructions-loaded.js +151 -0
  43. package/dist/hooks/notification.js +153 -2
  44. package/dist/hooks/post-compact.js +164 -0
  45. package/dist/hooks/pre-compact.js +3073 -101
  46. package/dist/hooks/pre-tool-use.js +151 -0
  47. package/dist/hooks/prompt-ingest-worker.js +1714 -1537
  48. package/dist/hooks/prompt-submit.js +2658 -1113
  49. package/dist/hooks/response-ingest-worker.js +170 -6
  50. package/dist/hooks/session-end.js +153 -2
  51. package/dist/hooks/session-start.js +154 -3
  52. package/dist/hooks/stop.js +151 -0
  53. package/dist/hooks/subagent-stop.js +151 -0
  54. package/dist/hooks/summary-worker.js +179 -7
  55. package/dist/index.js +278 -100
  56. package/dist/lib/cloud-sync.js +28 -2
  57. package/dist/lib/consolidation.js +69 -2
  58. package/dist/lib/database.js +19 -0
  59. package/dist/lib/device-registry.js +19 -0
  60. package/dist/lib/employee-templates.js +20 -1
  61. package/dist/lib/exe-daemon.js +236 -16
  62. package/dist/lib/hybrid-search.js +154 -3
  63. package/dist/lib/license.js +15 -1
  64. package/dist/lib/messaging.js +39 -2
  65. package/dist/lib/schedules.js +792 -637
  66. package/dist/lib/store.js +796 -636
  67. package/dist/lib/tasks.js +1614 -1091
  68. package/dist/lib/tmux-routing.js +149 -9
  69. package/dist/mcp/server.js +1825 -1138
  70. package/dist/mcp/tools/create-task.js +2280 -828
  71. package/dist/mcp/tools/list-tasks.js +2788 -159
  72. package/dist/mcp/tools/send-message.js +39 -2
  73. package/dist/mcp/tools/update-task.js +64 -0
  74. package/dist/runtime/index.js +235 -67
  75. package/dist/tui/App.js +1452 -644
  76. package/package.json +3 -2
@@ -987,6 +987,13 @@ async function ensureSchema() {
987
987
  });
988
988
  } catch {
989
989
  }
990
+ try {
991
+ await client.execute({
992
+ sql: `ALTER TABLE tasks ADD COLUMN session_scope TEXT`,
993
+ args: []
994
+ });
995
+ } catch {
996
+ }
990
997
  try {
991
998
  await client.execute({
992
999
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -1433,6 +1440,18 @@ async function ensureSchema() {
1433
1440
  CREATE INDEX IF NOT EXISTS idx_session_kills_agent
1434
1441
  ON session_kills(agent_id);
1435
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
+ `);
1436
1455
  await client.executeMultiple(`
1437
1456
  CREATE TABLE IF NOT EXISTS conversations (
1438
1457
  id TEXT PRIMARY KEY,
@@ -1640,6 +1659,61 @@ var init_keychain = __esm({
1640
1659
  }
1641
1660
  });
1642
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
+
1643
1717
  // src/lib/shard-manager.ts
1644
1718
  var shard_manager_exports = {};
1645
1719
  __export(shard_manager_exports, {
@@ -1881,6 +1955,71 @@ var init_shard_manager = __esm({
1881
1955
  }
1882
1956
  });
1883
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
+
1884
2023
  // src/lib/store.ts
1885
2024
  var store_exports = {};
1886
2025
  __export(store_exports, {
@@ -1960,6 +2099,11 @@ async function initStore(options) {
1960
2099
  "version-query"
1961
2100
  );
1962
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
+ }
1963
2107
  }
1964
2108
  function classifyTier(record) {
1965
2109
  if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
@@ -2001,6 +2145,12 @@ async function writeMemory(record) {
2001
2145
  supersedes_id: record.supersedes_id ?? null
2002
2146
  };
2003
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
+ });
2004
2154
  const MAX_PENDING = 1e3;
2005
2155
  if (_pendingRecords.length > MAX_PENDING) {
2006
2156
  const dropped = _pendingRecords.length - MAX_PENDING;
@@ -2346,6 +2496,7 @@ var init_store = __esm({
2346
2496
  init_database();
2347
2497
  init_keychain();
2348
2498
  init_config();
2499
+ init_state_bus();
2349
2500
  INIT_MAX_RETRIES = 3;
2350
2501
  INIT_RETRY_DELAY_MS = 1e3;
2351
2502
  _pendingRecords = [];
@@ -3162,7 +3313,7 @@ __export(license_exports, {
3162
3313
  validateLicense: () => validateLicense
3163
3314
  });
3164
3315
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
3165
- import { randomUUID as randomUUID2 } from "crypto";
3316
+ import { randomUUID as randomUUID3 } from "crypto";
3166
3317
  import path10 from "path";
3167
3318
  import { jwtVerify, importSPKI } from "jose";
3168
3319
  async function fetchRetry(url, init) {
@@ -3189,7 +3340,7 @@ function loadDeviceId() {
3189
3340
  }
3190
3341
  } catch {
3191
3342
  }
3192
- const id = randomUUID2();
3343
+ const id = randomUUID3();
3193
3344
  mkdirSync3(EXE_AI_DIR, { recursive: true });
3194
3345
  writeFileSync2(DEVICE_ID_PATH, id, "utf8");
3195
3346
  return id;
@@ -3304,7 +3455,21 @@ function getCacheAgeMs() {
3304
3455
  }
3305
3456
  }
3306
3457
  async function checkLicense() {
3307
- const key = loadLicense();
3458
+ let key = loadLicense();
3459
+ if (!key) {
3460
+ try {
3461
+ const configPath = path10.join(EXE_AI_DIR, "config.json");
3462
+ if (existsSync8(configPath)) {
3463
+ const raw = JSON.parse(readFileSync6(configPath, "utf8"));
3464
+ const cloud = raw.cloud;
3465
+ if (cloud?.apiKey) {
3466
+ key = cloud.apiKey;
3467
+ saveLicense(key);
3468
+ }
3469
+ }
3470
+ } catch {
3471
+ }
3472
+ }
3308
3473
  if (!key) return FREE_LICENSE;
3309
3474
  const cached = await getCachedLicense();
3310
3475
  if (cached && getCacheAgeMs() < CACHE_MAX_AGE_MS) return cached;
@@ -3662,1207 +3827,1646 @@ var init_notifications = __esm({
3662
3827
  }
3663
3828
  });
3664
3829
 
3665
- // src/lib/tasks-crud.ts
3666
- import crypto6 from "crypto";
3830
+ // src/lib/session-registry.ts
3831
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
3667
3832
  import path15 from "path";
3668
- import { execSync as execSync6 } from "child_process";
3669
- import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3670
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3671
- async function writeCheckpoint(input) {
3672
- const client = getClient();
3673
- const row = await resolveTask(client, input.taskId);
3674
- const taskId = String(row.id);
3675
- const now = (/* @__PURE__ */ new Date()).toISOString();
3676
- const blockedByIds = [];
3677
- if (row.blocked_by) {
3678
- blockedByIds.push(String(row.blocked_by));
3833
+ import os4 from "os";
3834
+ function registerSession(entry) {
3835
+ const dir = path15.dirname(REGISTRY_PATH);
3836
+ if (!existsSync11(dir)) {
3837
+ mkdirSync4(dir, { recursive: true });
3679
3838
  }
3680
- const checkpoint = {
3681
- step: input.step,
3682
- context_summary: input.contextSummary,
3683
- files_touched: input.filesTouched ?? [],
3684
- blocked_by_ids: blockedByIds,
3685
- last_checkpoint_at: now
3686
- };
3687
- const result = await client.execute({
3688
- sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
3689
- args: [JSON.stringify(checkpoint), now, taskId]
3690
- });
3691
- if (result.rowsAffected === 0) {
3692
- throw new Error(`Checkpoint write failed: task ${taskId} not found`);
3839
+ const sessions = listSessions();
3840
+ const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
3841
+ if (idx >= 0) {
3842
+ sessions[idx] = entry;
3843
+ } else {
3844
+ sessions.push(entry);
3693
3845
  }
3694
- const countResult = await client.execute({
3695
- sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
3696
- args: [taskId]
3697
- });
3698
- const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
3699
- return { checkpointCount };
3700
- }
3701
- function extractParentFromContext(contextBody) {
3702
- if (!contextBody) return null;
3703
- const match = contextBody.match(
3704
- /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
3705
- );
3706
- return match ? match[1].toLowerCase() : null;
3707
- }
3708
- function slugify(title) {
3709
- return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3846
+ writeFileSync5(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
3710
3847
  }
3711
- async function resolveTask(client, identifier) {
3712
- let result = await client.execute({
3713
- sql: "SELECT * FROM tasks WHERE id = ?",
3714
- args: [identifier]
3715
- });
3716
- if (result.rows.length === 1) return result.rows[0];
3717
- result = await client.execute({
3718
- sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
3719
- args: [`%${identifier}%`]
3720
- });
3721
- if (result.rows.length === 1) return result.rows[0];
3722
- if (result.rows.length > 1) {
3723
- const exact = result.rows.filter(
3724
- (r) => String(r.task_file).endsWith(`/${identifier}.md`)
3725
- );
3726
- if (exact.length === 1) return exact[0];
3727
- const candidates = exact.length > 1 ? exact : result.rows;
3728
- const active = candidates.filter(
3729
- (r) => !["done", "cancelled"].includes(String(r.status))
3730
- );
3731
- if (active.length === 1) return active[0];
3732
- const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
3733
- throw new Error(
3734
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3735
- );
3736
- }
3737
- result = await client.execute({
3738
- sql: "SELECT * FROM tasks WHERE title LIKE ?",
3739
- args: [`%${identifier}%`]
3740
- });
3741
- if (result.rows.length === 1) return result.rows[0];
3742
- if (result.rows.length > 1) {
3743
- const active = result.rows.filter(
3744
- (r) => !["done", "cancelled"].includes(String(r.status))
3745
- );
3746
- if (active.length === 1) return active[0];
3747
- const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
3748
- throw new Error(
3749
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
3750
- );
3848
+ function listSessions() {
3849
+ try {
3850
+ const raw = readFileSync9(REGISTRY_PATH, "utf8");
3851
+ return JSON.parse(raw);
3852
+ } catch {
3853
+ return [];
3751
3854
  }
3752
- throw new Error(`Task not found: ${identifier}`);
3753
3855
  }
3754
- async function createTaskCore(input) {
3755
- const client = getClient();
3756
- const id = crypto6.randomUUID();
3757
- const now = (/* @__PURE__ */ new Date()).toISOString();
3758
- const slug = slugify(input.title);
3759
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
3760
- let blockedById = null;
3761
- const initialStatus = input.blockedBy ? "blocked" : "open";
3762
- if (input.blockedBy) {
3763
- const blocker = await resolveTask(client, input.blockedBy);
3764
- blockedById = String(blocker.id);
3765
- }
3766
- let parentTaskId = null;
3767
- let parentRef = input.parentTaskId;
3768
- if (!parentRef) {
3769
- const extracted = extractParentFromContext(input.context);
3770
- if (extracted) {
3771
- parentRef = extracted;
3772
- process.stderr.write(
3773
- "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
3774
- );
3775
- }
3856
+ var REGISTRY_PATH;
3857
+ var init_session_registry = __esm({
3858
+ "src/lib/session-registry.ts"() {
3859
+ "use strict";
3860
+ REGISTRY_PATH = path15.join(os4.homedir(), ".exe-os", "session-registry.json");
3776
3861
  }
3777
- if (parentRef) {
3778
- try {
3779
- const parent = await resolveTask(client, parentRef);
3780
- parentTaskId = String(parent.id);
3781
- } catch (err) {
3782
- if (!input.parentTaskId) {
3783
- throw new Error(
3784
- `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
3785
- );
3862
+ });
3863
+
3864
+ // src/lib/tmux-transport.ts
3865
+ var tmux_transport_exports = {};
3866
+ __export(tmux_transport_exports, {
3867
+ TmuxTransport: () => TmuxTransport
3868
+ });
3869
+ import { execFileSync } from "child_process";
3870
+ var QUIET, TmuxTransport;
3871
+ var init_tmux_transport = __esm({
3872
+ "src/lib/tmux-transport.ts"() {
3873
+ "use strict";
3874
+ QUIET = {
3875
+ encoding: "utf8",
3876
+ stdio: ["pipe", "pipe", "pipe"]
3877
+ };
3878
+ TmuxTransport = class {
3879
+ getMySession() {
3880
+ try {
3881
+ return execFileSync("tmux", ["display-message", "-p", "#{session_name}"], QUIET).trim() || null;
3882
+ } catch {
3883
+ return null;
3884
+ }
3786
3885
  }
3787
- throw err;
3788
- }
3789
- }
3790
- let warning;
3791
- const dupCheck = await client.execute({
3792
- sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
3793
- args: [input.title, input.assignedTo]
3794
- });
3795
- if (dupCheck.rows.length > 0) {
3796
- warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
3797
- }
3798
- if (input.baseDir) {
3799
- try {
3800
- await mkdir4(path15.join(input.baseDir, "exe", "output"), { recursive: true });
3801
- await mkdir4(path15.join(input.baseDir, "exe", "research"), { recursive: true });
3802
- await ensureArchitectureDoc(input.baseDir, input.projectName);
3803
- await ensureGitignoreExe(input.baseDir);
3804
- } catch {
3805
- }
3806
- }
3807
- const complexity = input.complexity ?? "standard";
3808
- await client.execute({
3809
- 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)
3810
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3811
- args: [
3812
- id,
3813
- input.title,
3814
- input.assignedTo,
3815
- input.assignedBy,
3816
- input.projectName,
3817
- input.priority,
3818
- initialStatus,
3819
- taskFile,
3820
- blockedById,
3821
- parentTaskId,
3822
- input.reviewer ?? null,
3823
- input.context,
3824
- complexity,
3825
- input.budgetTokens ?? null,
3826
- input.budgetFallbackModel ?? null,
3827
- 0,
3828
- null,
3829
- now,
3830
- now
3831
- ]
3832
- });
3833
- return {
3834
- id,
3835
- title: input.title,
3836
- assignedTo: input.assignedTo,
3837
- assignedBy: input.assignedBy,
3838
- projectName: input.projectName,
3839
- priority: input.priority,
3840
- status: initialStatus,
3841
- taskFile,
3842
- createdAt: now,
3843
- updatedAt: now,
3844
- warning,
3845
- budgetTokens: input.budgetTokens ?? null,
3846
- budgetFallbackModel: input.budgetFallbackModel ?? null,
3847
- tokensUsed: 0,
3848
- tokensWarnedAt: null
3849
- };
3850
- }
3851
- async function listTasks(input) {
3852
- const client = getClient();
3853
- const conditions = [];
3854
- const args = [];
3855
- if (input.assignedTo) {
3856
- conditions.push("assigned_to = ?");
3857
- args.push(input.assignedTo);
3858
- }
3859
- if (input.status) {
3860
- conditions.push("status = ?");
3861
- args.push(input.status);
3862
- } else {
3863
- conditions.push("status IN ('open', 'in_progress', 'blocked')");
3886
+ listSessions() {
3887
+ try {
3888
+ return execFileSync("tmux", ["list-sessions", "-F", "#{session_name}"], QUIET).trim().split("\n").filter(Boolean);
3889
+ } catch {
3890
+ return [];
3891
+ }
3892
+ }
3893
+ isAlive(target) {
3894
+ try {
3895
+ const sessions = this.listSessions();
3896
+ if (!sessions.includes(target)) return false;
3897
+ const paneStatus = execFileSync(
3898
+ "tmux",
3899
+ ["list-panes", "-t", target, "-F", "#{pane_dead}"],
3900
+ QUIET
3901
+ ).trim();
3902
+ return paneStatus !== "1";
3903
+ } catch {
3904
+ return false;
3905
+ }
3906
+ }
3907
+ sendKeys(target, keys) {
3908
+ execFileSync("tmux", ["send-keys", "-t", target, keys, "Enter"], QUIET);
3909
+ }
3910
+ capturePane(target, lines) {
3911
+ const args = ["capture-pane", "-t", target, "-p"];
3912
+ if (lines) args.push("-S", `-${lines}`);
3913
+ return execFileSync("tmux", args, { ...QUIET, timeout: 3e3 });
3914
+ }
3915
+ isPaneInCopyMode(target) {
3916
+ try {
3917
+ const result = execFileSync(
3918
+ "tmux",
3919
+ ["display-message", "-p", "-t", target, "#{pane_in_mode}"],
3920
+ { ...QUIET, timeout: 3e3 }
3921
+ ).trim();
3922
+ return result === "1";
3923
+ } catch {
3924
+ return false;
3925
+ }
3926
+ }
3927
+ spawn(name, config2) {
3928
+ try {
3929
+ const args = ["new-session", "-d", "-s", name];
3930
+ if (config2.cwd) args.push("-c", config2.cwd);
3931
+ args.push(config2.command);
3932
+ execFileSync("tmux", args);
3933
+ return { sessionName: name };
3934
+ } catch (e) {
3935
+ return { sessionName: name, error: `spawn failed: ${e}` };
3936
+ }
3937
+ }
3938
+ kill(target) {
3939
+ try {
3940
+ execFileSync("tmux", ["kill-session", "-t", target], QUIET);
3941
+ } catch {
3942
+ }
3943
+ }
3944
+ pipeLog(target, logFile) {
3945
+ try {
3946
+ const safePath = logFile.replace(/'/g, "'\\''");
3947
+ execFileSync("tmux", ["pipe-pane", "-t", target, `cat >> '${safePath}'`], QUIET);
3948
+ } catch {
3949
+ }
3950
+ }
3951
+ };
3864
3952
  }
3865
- if (input.projectName) {
3866
- conditions.push("project_name = ?");
3867
- args.push(input.projectName);
3953
+ });
3954
+
3955
+ // src/lib/transport.ts
3956
+ function getTransport() {
3957
+ if (!_transport) {
3958
+ const { TmuxTransport: TmuxTransport2 } = (init_tmux_transport(), __toCommonJS(tmux_transport_exports));
3959
+ _transport = new TmuxTransport2();
3868
3960
  }
3869
- if (input.priority) {
3870
- conditions.push("priority = ?");
3871
- args.push(input.priority);
3961
+ return _transport;
3962
+ }
3963
+ var _transport;
3964
+ var init_transport = __esm({
3965
+ "src/lib/transport.ts"() {
3966
+ "use strict";
3967
+ _transport = null;
3872
3968
  }
3873
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3874
- const result = await client.execute({
3875
- 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`,
3876
- args
3877
- });
3878
- return result.rows.map((r) => ({
3879
- id: String(r.id),
3880
- title: String(r.title),
3881
- assignedTo: String(r.assigned_to),
3882
- assignedBy: String(r.assigned_by),
3883
- projectName: String(r.project_name),
3884
- priority: String(r.priority),
3885
- status: String(r.status),
3886
- taskFile: String(r.task_file),
3887
- createdAt: String(r.created_at),
3888
- updatedAt: String(r.updated_at),
3889
- checkpointCount: Number(r.checkpoint_count ?? 0),
3890
- budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
3891
- budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
3892
- tokensUsed: Number(r.tokens_used ?? 0),
3893
- tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
3894
- }));
3969
+ });
3970
+
3971
+ // src/lib/cc-agent-support.ts
3972
+ import { execSync as execSync6 } from "child_process";
3973
+ function _resetCcAgentSupportCache() {
3974
+ _cachedSupport = null;
3895
3975
  }
3896
- function checkStaleCompletion(taskContext, taskCreatedAt) {
3897
- if (!taskContext) return null;
3898
- if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
3976
+ function claudeSupportsAgentFlag() {
3977
+ if (_cachedSupport !== null) return _cachedSupport;
3899
3978
  try {
3900
- const since = new Date(taskCreatedAt).toISOString();
3901
- const branch = execSync6(
3902
- "git rev-parse --abbrev-ref HEAD 2>/dev/null",
3903
- { encoding: "utf8", timeout: 3e3 }
3904
- ).trim();
3905
- const branchArg = branch && branch !== "HEAD" ? branch : "";
3906
- const commitCount = execSync6(
3907
- `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
3908
- { encoding: "utf8", timeout: 5e3 }
3909
- ).trim();
3910
- const count = parseInt(commitCount, 10);
3911
- if (count === 0) {
3912
- return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
3913
- }
3914
- return null;
3979
+ const helpOutput = execSync6("claude --help 2>&1", {
3980
+ encoding: "utf-8",
3981
+ timeout: 5e3
3982
+ });
3983
+ _cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
3915
3984
  } catch {
3916
- return null;
3985
+ _cachedSupport = false;
3917
3986
  }
3987
+ return _cachedSupport;
3918
3988
  }
3919
- async function updateTaskStatus(input) {
3920
- const client = getClient();
3921
- const now = (/* @__PURE__ */ new Date()).toISOString();
3922
- const row = await resolveTask(client, input.taskId);
3923
- const taskId = String(row.id);
3924
- const taskFile = String(row.task_file);
3925
- if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
3926
- process.stderr.write(
3927
- `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
3928
- `
3929
- );
3989
+ var _cachedSupport;
3990
+ var init_cc_agent_support = __esm({
3991
+ "src/lib/cc-agent-support.ts"() {
3992
+ "use strict";
3993
+ _cachedSupport = null;
3930
3994
  }
3931
- if (input.status === "done") {
3932
- const existingRow = await client.execute({
3933
- sql: "SELECT context, created_at FROM tasks WHERE id = ?",
3934
- args: [taskId]
3935
- });
3936
- if (existingRow.rows.length > 0) {
3937
- const ctx = existingRow.rows[0];
3938
- const warning = checkStaleCompletion(ctx.context, ctx.created_at);
3939
- if (warning) {
3940
- input.result = input.result ? `\u26A0\uFE0F ${warning}
3995
+ });
3941
3996
 
3942
- ${input.result}` : `\u26A0\uFE0F ${warning}`;
3943
- process.stderr.write(`[tasks] ${warning} (task: ${taskId})
3944
- `);
3945
- }
3997
+ // src/lib/mcp-prefix.ts
3998
+ function expandDualPrefixTools(shortNames) {
3999
+ const out = [];
4000
+ for (const name of shortNames) {
4001
+ for (const prefix of MCP_TOOL_PREFIXES) {
4002
+ out.push(prefix + name);
3946
4003
  }
3947
4004
  }
3948
- if (input.status === "in_progress") {
3949
- const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
3950
- const claim = await client.execute({
3951
- sql: `UPDATE tasks
3952
- SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
3953
- WHERE id = ? AND status = 'open'`,
3954
- args: [tmuxSession, now, taskId]
3955
- });
3956
- if (claim.rowsAffected === 0) {
3957
- const current = await client.execute({
3958
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
3959
- args: [taskId]
3960
- });
3961
- const cur = current.rows[0];
3962
- const status = cur?.status ?? "unknown";
3963
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
3964
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
3965
- }
3966
- try {
3967
- await writeCheckpoint({
3968
- taskId,
3969
- step: "claimed",
3970
- contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
3971
- });
3972
- } catch {
3973
- }
3974
- return { row, taskFile, now, taskId };
4005
+ return out;
4006
+ }
4007
+ var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
4008
+ var init_mcp_prefix = __esm({
4009
+ "src/lib/mcp-prefix.ts"() {
4010
+ "use strict";
4011
+ MCP_PRIMARY_KEY = "exe-os";
4012
+ MCP_LEGACY_KEY = "exe-mem";
4013
+ MCP_TOOL_PREFIXES = [
4014
+ `mcp__${MCP_PRIMARY_KEY}__`,
4015
+ `mcp__${MCP_LEGACY_KEY}__`
4016
+ ];
3975
4017
  }
3976
- if (input.result) {
3977
- await client.execute({
3978
- sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
3979
- args: [input.status, input.result, now, taskId]
3980
- });
3981
- } else {
3982
- await client.execute({
3983
- sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
3984
- args: [input.status, now, taskId]
3985
- });
4018
+ });
4019
+
4020
+ // src/lib/provider-table.ts
4021
+ function detectActiveProvider(env = process.env) {
4022
+ const baseUrl = env.ANTHROPIC_BASE_URL;
4023
+ if (!baseUrl) return DEFAULT_PROVIDER;
4024
+ for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
4025
+ if (cfg.baseUrl === baseUrl) return name;
4026
+ }
4027
+ return DEFAULT_PROVIDER;
4028
+ }
4029
+ var PROVIDER_TABLE, DEFAULT_PROVIDER;
4030
+ var init_provider_table = __esm({
4031
+ "src/lib/provider-table.ts"() {
4032
+ "use strict";
4033
+ PROVIDER_TABLE = {
4034
+ opencode: {
4035
+ baseUrl: "https://opencode.ai/zen/go",
4036
+ apiKeyEnv: "OPENCODE_API_KEY",
4037
+ defaultModel: "minimax-m2.7"
4038
+ }
4039
+ };
4040
+ DEFAULT_PROVIDER = "default";
3986
4041
  }
4042
+ });
4043
+
4044
+ // src/lib/intercom-queue.ts
4045
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, renameSync as renameSync2, existsSync as existsSync12, mkdirSync as mkdirSync5 } from "fs";
4046
+ import path16 from "path";
4047
+ import os5 from "os";
4048
+ function ensureDir() {
4049
+ const dir = path16.dirname(QUEUE_PATH);
4050
+ if (!existsSync12(dir)) mkdirSync5(dir, { recursive: true });
4051
+ }
4052
+ function readQueue() {
3987
4053
  try {
3988
- await writeCheckpoint({
3989
- taskId,
3990
- step: `status_transition:${input.status}`,
3991
- contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
3992
- });
4054
+ if (!existsSync12(QUEUE_PATH)) return [];
4055
+ return JSON.parse(readFileSync10(QUEUE_PATH, "utf8"));
3993
4056
  } catch {
4057
+ return [];
3994
4058
  }
3995
- return { row, taskFile, now, taskId };
3996
4059
  }
3997
- async function deleteTaskCore(taskId, _baseDir) {
3998
- const client = getClient();
3999
- const row = await resolveTask(client, taskId);
4000
- const id = String(row.id);
4001
- const taskFile = String(row.task_file);
4002
- const assignedTo = String(row.assigned_to);
4003
- const assignedBy = String(row.assigned_by);
4004
- await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
4005
- const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
4006
- return { taskFile, assignedTo, assignedBy, taskSlug };
4060
+ function writeQueue(queue) {
4061
+ ensureDir();
4062
+ const tmp = `${QUEUE_PATH}.tmp`;
4063
+ writeFileSync6(tmp, JSON.stringify(queue, null, 2));
4064
+ renameSync2(tmp, QUEUE_PATH);
4007
4065
  }
4008
- async function ensureArchitectureDoc(baseDir, projectName) {
4009
- const archPath = path15.join(baseDir, "exe", "ARCHITECTURE.md");
4010
- try {
4011
- if (existsSync11(archPath)) return;
4012
- const template = [
4013
- `# ${projectName} \u2014 System Architecture`,
4014
- "",
4015
- "> Employees: read this before every task. Update it when you change system structure.",
4016
- `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
4017
- "",
4018
- "## Overview",
4019
- "",
4020
- "<!-- Describe what this system does, its main components, and how they connect. -->",
4021
- "",
4022
- "## Key Components",
4023
- "",
4024
- "<!-- List the major modules, services, or subsystems. -->",
4025
- "",
4026
- "## Data Flow",
4027
- "",
4028
- "<!-- How does data move through the system? What writes where? -->",
4029
- "",
4030
- "## Invariants",
4031
- "",
4032
- "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
4033
- "",
4034
- "## Dependencies",
4035
- "",
4036
- "<!-- What depends on what? If I change X, what else is affected? -->",
4037
- ""
4038
- ].join("\n");
4039
- await writeFile4(archPath, template, "utf-8");
4040
- } catch {
4066
+ function queueIntercom(targetSession, reason) {
4067
+ const queue = readQueue();
4068
+ const existing = queue.find((q) => q.targetSession === targetSession);
4069
+ if (existing) {
4070
+ existing.attempts++;
4071
+ existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
4072
+ existing.reason = reason;
4073
+ } else {
4074
+ queue.push({
4075
+ targetSession,
4076
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
4077
+ attempts: 0,
4078
+ reason
4079
+ });
4041
4080
  }
4081
+ writeQueue(queue);
4042
4082
  }
4043
- async function ensureGitignoreExe(baseDir) {
4044
- const gitignorePath = path15.join(baseDir, ".gitignore");
4045
- try {
4046
- if (existsSync11(gitignorePath)) {
4047
- const content = readFileSync9(gitignorePath, "utf-8");
4048
- if (/^\/?exe\/?$/m.test(content)) return;
4049
- await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
4050
- } else {
4051
- await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
4052
- }
4053
- } catch {
4083
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
4084
+ var init_intercom_queue = __esm({
4085
+ "src/lib/intercom-queue.ts"() {
4086
+ "use strict";
4087
+ QUEUE_PATH = path16.join(os5.homedir(), ".exe-os", "intercom-queue.json");
4088
+ TTL_MS = 60 * 60 * 1e3;
4089
+ INTERCOM_LOG = path16.join(os5.homedir(), ".exe-os", "intercom.log");
4090
+ }
4091
+ });
4092
+
4093
+ // src/lib/session-kill-telemetry.ts
4094
+ import crypto6 from "crypto";
4095
+ async function recordSessionKill(input) {
4096
+ try {
4097
+ const client = getClient();
4098
+ await client.execute({
4099
+ sql: `INSERT INTO session_kills
4100
+ (id, session_name, agent_id, killed_at, reason,
4101
+ ticks_idle, estimated_tokens_saved)
4102
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
4103
+ args: [
4104
+ crypto6.randomUUID(),
4105
+ input.sessionName,
4106
+ input.agentId,
4107
+ (/* @__PURE__ */ new Date()).toISOString(),
4108
+ input.reason,
4109
+ input.ticksIdle ?? null,
4110
+ input.estimatedTokensSaved ?? null
4111
+ ]
4112
+ });
4113
+ } catch (err) {
4114
+ process.stderr.write(
4115
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
4116
+ `
4117
+ );
4054
4118
  }
4055
4119
  }
4056
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4057
- var init_tasks_crud = __esm({
4058
- "src/lib/tasks-crud.ts"() {
4120
+ var init_session_kill_telemetry = __esm({
4121
+ "src/lib/session-kill-telemetry.ts"() {
4059
4122
  "use strict";
4060
4123
  init_database();
4061
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
4062
- TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4063
4124
  }
4064
4125
  });
4065
4126
 
4066
- // src/lib/session-registry.ts
4067
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync12 } from "fs";
4068
- import path16 from "path";
4069
- import os4 from "os";
4070
- function registerSession(entry) {
4071
- const dir = path16.dirname(REGISTRY_PATH);
4072
- if (!existsSync12(dir)) {
4073
- mkdirSync4(dir, { recursive: true });
4127
+ // src/lib/capacity-monitor.ts
4128
+ var capacity_monitor_exports = {};
4129
+ __export(capacity_monitor_exports, {
4130
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
4131
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
4132
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
4133
+ confirmCapacityKill: () => confirmCapacityKill,
4134
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
4135
+ extractContextPercent: () => extractContextPercent,
4136
+ isAtCapacity: () => isAtCapacity,
4137
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
4138
+ pollCapacityDead: () => pollCapacityDead
4139
+ });
4140
+ function resumeTaskTitle(agentId) {
4141
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
4142
+ }
4143
+ function buildResumeContext(agentId, openTasks) {
4144
+ const taskList = openTasks.map(
4145
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
4146
+ ).join("\n");
4147
+ return [
4148
+ "## Context",
4149
+ "",
4150
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
4151
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
4152
+ "",
4153
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
4154
+ "",
4155
+ taskList,
4156
+ "",
4157
+ "Read each task file and chain through them. Build and commit after each one."
4158
+ ].join("\n");
4159
+ }
4160
+ function filterPaneContent(paneOutput) {
4161
+ return paneOutput.split("\n").filter((line) => {
4162
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
4163
+ for (const marker of CONTENT_LINE_MARKERS) {
4164
+ if (line.includes(marker)) return false;
4165
+ }
4166
+ for (const re of SOURCE_CODE_MARKERS) {
4167
+ if (re.test(line)) return false;
4168
+ }
4169
+ return true;
4170
+ }).join("\n");
4171
+ }
4172
+ function extractContextPercent(paneOutput) {
4173
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
4174
+ if (!match) return null;
4175
+ const parsed = Number.parseInt(match[2], 10);
4176
+ return Number.isFinite(parsed) ? parsed : null;
4177
+ }
4178
+ function isAtCapacity(paneOutput) {
4179
+ const filtered = filterPaneContent(paneOutput);
4180
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
4181
+ }
4182
+ function confirmCapacityKill(agentId, now = Date.now()) {
4183
+ const pendingSince = _pendingCapacityKill.get(agentId);
4184
+ if (pendingSince === void 0) {
4185
+ _pendingCapacityKill.set(agentId, now);
4186
+ return false;
4074
4187
  }
4075
- const sessions = listSessions();
4076
- const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
4077
- if (idx >= 0) {
4078
- sessions[idx] = entry;
4079
- } else {
4080
- sessions.push(entry);
4188
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
4189
+ _pendingCapacityKill.set(agentId, now);
4190
+ return false;
4081
4191
  }
4082
- writeFileSync5(REGISTRY_PATH, JSON.stringify(sessions, null, 2));
4192
+ _pendingCapacityKill.delete(agentId);
4193
+ return true;
4083
4194
  }
4084
- function listSessions() {
4195
+ function _resetPendingCapacityKills() {
4196
+ _pendingCapacityKill.clear();
4197
+ }
4198
+ function _resetLastRelaunchCache() {
4199
+ _lastRelaunch.clear();
4200
+ }
4201
+ async function lastResumeCreatedAtMs(agentId) {
4202
+ const client = getClient();
4203
+ const result = await client.execute({
4204
+ sql: `SELECT MAX(created_at) AS last_created_at
4205
+ FROM tasks
4206
+ WHERE assigned_to = ? AND title LIKE ?`,
4207
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`]
4208
+ });
4209
+ const raw = result.rows[0]?.last_created_at;
4210
+ if (raw === null || raw === void 0) return null;
4211
+ const parsed = Date.parse(String(raw));
4212
+ return Number.isNaN(parsed) ? null : parsed;
4213
+ }
4214
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
4215
+ const cached = _lastRelaunch.get(agentId);
4216
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
4217
+ const persisted = await lastResumeCreatedAtMs(agentId);
4218
+ if (persisted === null) return false;
4219
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
4220
+ _lastRelaunch.set(agentId, persisted);
4221
+ return true;
4222
+ }
4223
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
4224
+ const client = getClient();
4225
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4226
+ const context = buildResumeContext(agentId, openTasks);
4227
+ const existing = await client.execute({
4228
+ sql: `SELECT id FROM tasks
4229
+ WHERE assigned_to = ?
4230
+ AND title LIKE ?
4231
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
4232
+ ORDER BY created_at DESC
4233
+ LIMIT 1`,
4234
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
4235
+ });
4236
+ if (existing.rows.length > 0) {
4237
+ const taskId = String(existing.rows[0].id);
4238
+ await client.execute({
4239
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
4240
+ args: [context, now, taskId]
4241
+ });
4242
+ return { created: false, taskId };
4243
+ }
4244
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
4245
+ const task = await createTask2({
4246
+ title: resumeTaskTitle(agentId),
4247
+ assignedTo: agentId,
4248
+ assignedBy: "system",
4249
+ projectName: projectDir.split("/").pop() ?? "unknown",
4250
+ priority: "p0",
4251
+ context,
4252
+ baseDir: projectDir
4253
+ });
4254
+ return { created: true, taskId: task.id };
4255
+ }
4256
+ async function pollCapacityDead() {
4257
+ const transport = getTransport();
4258
+ const relaunched = [];
4259
+ const registered = listSessions().filter(
4260
+ (s) => s.agentId !== "exe"
4261
+ );
4262
+ if (registered.length === 0) return [];
4263
+ let liveSessions;
4085
4264
  try {
4086
- const raw = readFileSync10(REGISTRY_PATH, "utf8");
4087
- return JSON.parse(raw);
4265
+ liveSessions = transport.listSessions();
4088
4266
  } catch {
4089
4267
  return [];
4090
4268
  }
4091
- }
4092
- var REGISTRY_PATH;
4093
- var init_session_registry = __esm({
4094
- "src/lib/session-registry.ts"() {
4095
- "use strict";
4096
- REGISTRY_PATH = path16.join(os4.homedir(), ".exe-os", "session-registry.json");
4097
- }
4098
- });
4099
-
4100
- // src/lib/tmux-transport.ts
4101
- var tmux_transport_exports = {};
4102
- __export(tmux_transport_exports, {
4103
- TmuxTransport: () => TmuxTransport
4104
- });
4105
- import { execFileSync } from "child_process";
4106
- var QUIET, TmuxTransport;
4107
- var init_tmux_transport = __esm({
4108
- "src/lib/tmux-transport.ts"() {
4109
- "use strict";
4110
- QUIET = {
4111
- encoding: "utf8",
4112
- stdio: ["pipe", "pipe", "pipe"]
4113
- };
4114
- TmuxTransport = class {
4115
- getMySession() {
4116
- try {
4117
- return execFileSync("tmux", ["display-message", "-p", "#{session_name}"], QUIET).trim() || null;
4118
- } catch {
4119
- return null;
4120
- }
4121
- }
4122
- listSessions() {
4123
- try {
4124
- return execFileSync("tmux", ["list-sessions", "-F", "#{session_name}"], QUIET).trim().split("\n").filter(Boolean);
4125
- } catch {
4126
- return [];
4127
- }
4128
- }
4129
- isAlive(target) {
4130
- try {
4131
- const sessions = this.listSessions();
4132
- if (!sessions.includes(target)) return false;
4133
- const paneStatus = execFileSync(
4134
- "tmux",
4135
- ["list-panes", "-t", target, "-F", "#{pane_dead}"],
4136
- QUIET
4137
- ).trim();
4138
- return paneStatus !== "1";
4139
- } catch {
4140
- return false;
4141
- }
4142
- }
4143
- sendKeys(target, keys) {
4144
- execFileSync("tmux", ["send-keys", "-t", target, keys, "Enter"], QUIET);
4145
- }
4146
- capturePane(target, lines) {
4147
- const args = ["capture-pane", "-t", target, "-p"];
4148
- if (lines) args.push("-S", `-${lines}`);
4149
- return execFileSync("tmux", args, { ...QUIET, timeout: 3e3 });
4150
- }
4151
- isPaneInCopyMode(target) {
4152
- try {
4153
- const result = execFileSync(
4154
- "tmux",
4155
- ["display-message", "-p", "-t", target, "#{pane_in_mode}"],
4156
- { ...QUIET, timeout: 3e3 }
4157
- ).trim();
4158
- return result === "1";
4159
- } catch {
4160
- return false;
4161
- }
4162
- }
4163
- spawn(name, config2) {
4164
- try {
4165
- const args = ["new-session", "-d", "-s", name];
4166
- if (config2.cwd) args.push("-c", config2.cwd);
4167
- args.push(config2.command);
4168
- execFileSync("tmux", args);
4169
- return { sessionName: name };
4170
- } catch (e) {
4171
- return { sessionName: name, error: `spawn failed: ${e}` };
4172
- }
4173
- }
4174
- kill(target) {
4175
- try {
4176
- execFileSync("tmux", ["kill-session", "-t", target], QUIET);
4177
- } catch {
4178
- }
4269
+ for (const entry of registered) {
4270
+ const { windowName, agentId, projectDir } = entry;
4271
+ if (!liveSessions.includes(windowName)) continue;
4272
+ if (await isWithinRelaunchCooldown(agentId)) continue;
4273
+ let pane;
4274
+ try {
4275
+ pane = transport.capturePane(windowName, 15);
4276
+ } catch {
4277
+ continue;
4278
+ }
4279
+ if (!isAtCapacity(pane)) continue;
4280
+ const ctxPct = extractContextPercent(pane);
4281
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
4282
+ process.stderr.write(
4283
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
4284
+ `
4285
+ );
4286
+ continue;
4287
+ }
4288
+ if (!confirmCapacityKill(agentId)) {
4289
+ process.stderr.write(
4290
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
4291
+ `
4292
+ );
4293
+ continue;
4294
+ }
4295
+ const verify = await verifyPaneAtCapacity(windowName);
4296
+ if (!verify.atCapacity) {
4297
+ process.stderr.write(
4298
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
4299
+ `
4300
+ );
4301
+ void recordSessionKill({
4302
+ sessionName: windowName,
4303
+ agentId,
4304
+ reason: "capacity_false_positive_blocked"
4305
+ });
4306
+ continue;
4307
+ }
4308
+ process.stderr.write(
4309
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
4310
+ `
4311
+ );
4312
+ try {
4313
+ transport.kill(windowName);
4314
+ void recordSessionKill({
4315
+ sessionName: windowName,
4316
+ agentId,
4317
+ reason: "capacity"
4318
+ });
4319
+ const client = getClient();
4320
+ const openTasks = await client.execute({
4321
+ sql: `SELECT id, title, priority, task_file, status
4322
+ FROM tasks
4323
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')
4324
+ ORDER BY
4325
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
4326
+ created_at ASC
4327
+ LIMIT 10`,
4328
+ args: [agentId]
4329
+ });
4330
+ if (openTasks.rows.length === 0) {
4331
+ process.stderr.write(
4332
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
4333
+ `
4334
+ );
4335
+ continue;
4179
4336
  }
4180
- pipeLog(target, logFile) {
4181
- try {
4182
- const safePath = logFile.replace(/'/g, "'\\''");
4183
- execFileSync("tmux", ["pipe-pane", "-t", target, `cat >> '${safePath}'`], QUIET);
4184
- } catch {
4185
- }
4337
+ const { created } = await createOrRefreshResumeTask(
4338
+ agentId,
4339
+ projectDir,
4340
+ openTasks.rows
4341
+ );
4342
+ if (created) {
4343
+ await writeNotification({
4344
+ agentId: "system",
4345
+ agentRole: "daemon",
4346
+ event: "capacity_relaunch",
4347
+ project: projectDir.split("/").pop() ?? "unknown",
4348
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
4349
+ });
4186
4350
  }
4187
- };
4188
- }
4189
- });
4190
-
4191
- // src/lib/transport.ts
4192
- function getTransport() {
4193
- if (!_transport) {
4194
- const { TmuxTransport: TmuxTransport2 } = (init_tmux_transport(), __toCommonJS(tmux_transport_exports));
4195
- _transport = new TmuxTransport2();
4351
+ _lastRelaunch.set(agentId, Date.now());
4352
+ if (created) relaunched.push(agentId);
4353
+ } catch (err) {
4354
+ process.stderr.write(
4355
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
4356
+ `
4357
+ );
4358
+ }
4196
4359
  }
4197
- return _transport;
4360
+ return relaunched;
4198
4361
  }
4199
- var _transport;
4200
- var init_transport = __esm({
4201
- "src/lib/transport.ts"() {
4362
+ 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;
4363
+ var init_capacity_monitor = __esm({
4364
+ "src/lib/capacity-monitor.ts"() {
4202
4365
  "use strict";
4203
- _transport = null;
4366
+ init_session_registry();
4367
+ init_transport();
4368
+ init_notifications();
4369
+ init_database();
4370
+ init_session_kill_telemetry();
4371
+ init_tmux_routing();
4372
+ CAPACITY_PATTERNS = [
4373
+ /conversation is too long/i,
4374
+ /maximum context length/i,
4375
+ /context window.*(?:limit|exceed|full)/i,
4376
+ /reached.*(?:token|context).*limit/i
4377
+ ];
4378
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
4379
+ CONTENT_LINE_MARKERS = [
4380
+ "RESUME:",
4381
+ "intercom",
4382
+ "capacity-monitor",
4383
+ "CAPACITY_PATTERNS",
4384
+ "isAtCapacity",
4385
+ "CONTENT_LINE_MARKERS",
4386
+ "pollCapacityDead",
4387
+ "confirmCapacityKill",
4388
+ "session_kills",
4389
+ "capacity-monitor.test"
4390
+ ];
4391
+ SOURCE_CODE_MARKERS = [
4392
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
4393
+ /(?:maximum context length|conversation is too long).*["'`/]/i
4394
+ ];
4395
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
4396
+ _lastRelaunch = /* @__PURE__ */ new Map();
4397
+ RESUME_TITLE_PREFIX = "RESUME:";
4398
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
4399
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
4400
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
4401
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
4402
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
4403
+ CTX_FLOOR_PERCENT = 50;
4204
4404
  }
4205
4405
  });
4206
4406
 
4207
- // src/lib/cc-agent-support.ts
4208
- import { execSync as execSync7 } from "child_process";
4209
- function _resetCcAgentSupportCache() {
4210
- _cachedSupport = null;
4407
+ // src/lib/tmux-routing.ts
4408
+ var tmux_routing_exports = {};
4409
+ __export(tmux_routing_exports, {
4410
+ acquireSpawnLock: () => acquireSpawnLock2,
4411
+ employeeSessionName: () => employeeSessionName,
4412
+ ensureEmployee: () => ensureEmployee,
4413
+ extractRootExe: () => extractRootExe,
4414
+ findFreeInstance: () => findFreeInstance,
4415
+ getDispatchedBy: () => getDispatchedBy,
4416
+ getMySession: () => getMySession,
4417
+ getParentExe: () => getParentExe,
4418
+ getSessionState: () => getSessionState,
4419
+ isEmployeeAlive: () => isEmployeeAlive,
4420
+ isExeSession: () => isExeSession,
4421
+ isSessionBusy: () => isSessionBusy,
4422
+ notifyParentExe: () => notifyParentExe,
4423
+ parseParentExe: () => parseParentExe,
4424
+ registerParentExe: () => registerParentExe,
4425
+ releaseSpawnLock: () => releaseSpawnLock2,
4426
+ resolveExeSession: () => resolveExeSession,
4427
+ sendIntercom: () => sendIntercom,
4428
+ spawnEmployee: () => spawnEmployee,
4429
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
4430
+ });
4431
+ import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
4432
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync13, appendFileSync } from "fs";
4433
+ import path17 from "path";
4434
+ import os6 from "os";
4435
+ import { fileURLToPath as fileURLToPath2 } from "url";
4436
+ import { unlinkSync as unlinkSync4 } from "fs";
4437
+ function spawnLockPath(sessionName) {
4438
+ return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4211
4439
  }
4212
- function claudeSupportsAgentFlag() {
4213
- if (_cachedSupport !== null) return _cachedSupport;
4440
+ function isProcessAlive(pid) {
4214
4441
  try {
4215
- const helpOutput = execSync7("claude --help 2>&1", {
4216
- encoding: "utf-8",
4217
- timeout: 5e3
4218
- });
4219
- _cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
4442
+ process.kill(pid, 0);
4443
+ return true;
4220
4444
  } catch {
4221
- _cachedSupport = false;
4445
+ return false;
4222
4446
  }
4223
- return _cachedSupport;
4224
4447
  }
4225
- var _cachedSupport;
4226
- var init_cc_agent_support = __esm({
4227
- "src/lib/cc-agent-support.ts"() {
4228
- "use strict";
4229
- _cachedSupport = null;
4448
+ function acquireSpawnLock2(sessionName) {
4449
+ if (!existsSync13(SPAWN_LOCK_DIR)) {
4450
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4230
4451
  }
4231
- });
4232
-
4233
- // src/lib/mcp-prefix.ts
4234
- function expandDualPrefixTools(shortNames) {
4235
- const out = [];
4236
- for (const name of shortNames) {
4237
- for (const prefix of MCP_TOOL_PREFIXES) {
4238
- out.push(prefix + name);
4452
+ const lockFile = spawnLockPath(sessionName);
4453
+ if (existsSync13(lockFile)) {
4454
+ try {
4455
+ const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
4456
+ const age = Date.now() - lock.timestamp;
4457
+ if (isProcessAlive(lock.pid) && age < 6e4) {
4458
+ return false;
4459
+ }
4460
+ } catch {
4239
4461
  }
4240
4462
  }
4241
- return out;
4463
+ writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4464
+ return true;
4242
4465
  }
4243
- var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
4244
- var init_mcp_prefix = __esm({
4245
- "src/lib/mcp-prefix.ts"() {
4246
- "use strict";
4247
- MCP_PRIMARY_KEY = "exe-os";
4248
- MCP_LEGACY_KEY = "exe-mem";
4249
- MCP_TOOL_PREFIXES = [
4250
- `mcp__${MCP_PRIMARY_KEY}__`,
4251
- `mcp__${MCP_LEGACY_KEY}__`
4252
- ];
4466
+ function releaseSpawnLock2(sessionName) {
4467
+ try {
4468
+ unlinkSync4(spawnLockPath(sessionName));
4469
+ } catch {
4253
4470
  }
4254
- });
4255
-
4256
- // src/lib/provider-table.ts
4257
- function detectActiveProvider(env = process.env) {
4258
- const baseUrl = env.ANTHROPIC_BASE_URL;
4259
- if (!baseUrl) return DEFAULT_PROVIDER;
4260
- for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
4261
- if (cfg.baseUrl === baseUrl) return name;
4471
+ }
4472
+ function resolveBehaviorsExporterScript() {
4473
+ try {
4474
+ const thisFile = fileURLToPath2(import.meta.url);
4475
+ const scriptPath = path17.join(
4476
+ path17.dirname(thisFile),
4477
+ "..",
4478
+ "bin",
4479
+ "exe-export-behaviors.js"
4480
+ );
4481
+ return existsSync13(scriptPath) ? scriptPath : null;
4482
+ } catch {
4483
+ return null;
4262
4484
  }
4263
- return DEFAULT_PROVIDER;
4264
4485
  }
4265
- var PROVIDER_TABLE, DEFAULT_PROVIDER;
4266
- var init_provider_table = __esm({
4267
- "src/lib/provider-table.ts"() {
4268
- "use strict";
4269
- PROVIDER_TABLE = {
4270
- opencode: {
4271
- baseUrl: "https://opencode.ai/zen/go",
4272
- apiKeyEnv: "OPENCODE_API_KEY",
4273
- defaultModel: "minimax-m2.7"
4274
- }
4275
- };
4276
- DEFAULT_PROVIDER = "default";
4486
+ function exportBehaviorsSync(agentId, projectName, sessionKey) {
4487
+ const script = resolveBehaviorsExporterScript();
4488
+ if (!script) return null;
4489
+ try {
4490
+ const output = execFileSync2(
4491
+ process.execPath,
4492
+ [script, agentId, projectName, sessionKey],
4493
+ { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
4494
+ ).trim();
4495
+ return output.length > 0 ? output : null;
4496
+ } catch (err) {
4497
+ process.stderr.write(
4498
+ `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
4499
+ `
4500
+ );
4501
+ return null;
4277
4502
  }
4278
- });
4279
-
4280
- // src/lib/intercom-queue.ts
4281
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, renameSync as renameSync2, existsSync as existsSync13, mkdirSync as mkdirSync5 } from "fs";
4282
- import path17 from "path";
4283
- import os5 from "os";
4284
- function ensureDir() {
4285
- const dir = path17.dirname(QUEUE_PATH);
4286
- if (!existsSync13(dir)) mkdirSync5(dir, { recursive: true });
4287
4503
  }
4288
- function readQueue() {
4504
+ function getMySession() {
4505
+ return getTransport().getMySession();
4506
+ }
4507
+ function employeeSessionName(employee, exeSession, instance) {
4508
+ if (!/^exe\d+$/.test(exeSession)) {
4509
+ const root = extractRootExe(exeSession);
4510
+ if (root) {
4511
+ process.stderr.write(
4512
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
4513
+ `
4514
+ );
4515
+ exeSession = root;
4516
+ } else {
4517
+ throw new Error(
4518
+ `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
4519
+ );
4520
+ }
4521
+ }
4522
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
4523
+ const name = `${employee}${suffix}-${exeSession}`;
4524
+ if (!VALID_SESSION_NAME.test(name)) {
4525
+ throw new Error(
4526
+ `Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
4527
+ );
4528
+ }
4529
+ return name;
4530
+ }
4531
+ function parseParentExe(sessionName, agentId) {
4532
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4533
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
4534
+ const match = sessionName.match(regex);
4535
+ return match?.[1] ?? null;
4536
+ }
4537
+ function extractRootExe(name) {
4538
+ const match = name.match(/(exe\d+)$/);
4539
+ return match?.[1] ?? null;
4540
+ }
4541
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
4542
+ if (!existsSync13(SESSION_CACHE)) {
4543
+ mkdirSync6(SESSION_CACHE, { recursive: true });
4544
+ }
4545
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
4546
+ const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4547
+ writeFileSync7(filePath, JSON.stringify({
4548
+ parentExe: rootExe,
4549
+ dispatchedBy: dispatchedBy || rootExe,
4550
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4551
+ }));
4552
+ }
4553
+ function getParentExe(sessionKey) {
4289
4554
  try {
4290
- if (!existsSync13(QUEUE_PATH)) return [];
4291
- return JSON.parse(readFileSync11(QUEUE_PATH, "utf8"));
4555
+ const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4556
+ return data.parentExe || null;
4292
4557
  } catch {
4293
- return [];
4558
+ return null;
4294
4559
  }
4295
4560
  }
4296
- function writeQueue(queue) {
4297
- ensureDir();
4298
- const tmp = `${QUEUE_PATH}.tmp`;
4299
- writeFileSync6(tmp, JSON.stringify(queue, null, 2));
4300
- renameSync2(tmp, QUEUE_PATH);
4561
+ function getDispatchedBy(sessionKey) {
4562
+ try {
4563
+ const data = JSON.parse(readFileSync11(
4564
+ path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4565
+ "utf8"
4566
+ ));
4567
+ return data.dispatchedBy ?? data.parentExe ?? null;
4568
+ } catch {
4569
+ return null;
4570
+ }
4301
4571
  }
4302
- function queueIntercom(targetSession, reason) {
4303
- const queue = readQueue();
4304
- const existing = queue.find((q) => q.targetSession === targetSession);
4305
- if (existing) {
4306
- existing.attempts++;
4307
- existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
4308
- existing.reason = reason;
4309
- } else {
4310
- queue.push({
4311
- targetSession,
4312
- queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
4313
- attempts: 0,
4314
- reason
4315
- });
4572
+ function resolveExeSession() {
4573
+ const mySession = getMySession();
4574
+ if (!mySession) return null;
4575
+ try {
4576
+ const key = getSessionKey();
4577
+ const parentExe = getParentExe(key);
4578
+ if (parentExe) {
4579
+ return extractRootExe(parentExe) ?? parentExe;
4580
+ }
4581
+ } catch {
4316
4582
  }
4317
- writeQueue(queue);
4583
+ return extractRootExe(mySession) ?? mySession;
4318
4584
  }
4319
- var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
4320
- var init_intercom_queue = __esm({
4321
- "src/lib/intercom-queue.ts"() {
4322
- "use strict";
4323
- QUEUE_PATH = path17.join(os5.homedir(), ".exe-os", "intercom-queue.json");
4324
- TTL_MS = 60 * 60 * 1e3;
4325
- INTERCOM_LOG = path17.join(os5.homedir(), ".exe-os", "intercom.log");
4585
+ function isEmployeeAlive(sessionName) {
4586
+ return getTransport().isAlive(sessionName);
4587
+ }
4588
+ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4589
+ const base = employeeSessionName(employeeName, exeSession);
4590
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
4591
+ for (let i = 2; i <= maxInstances; i++) {
4592
+ const candidate = employeeSessionName(employeeName, exeSession, i);
4593
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
4594
+ }
4595
+ return null;
4596
+ }
4597
+ async function verifyPaneAtCapacity(sessionName) {
4598
+ const transport = getTransport();
4599
+ if (!transport.isAlive(sessionName)) {
4600
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
4601
+ }
4602
+ let pane;
4603
+ try {
4604
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
4605
+ } catch (err) {
4606
+ return {
4607
+ atCapacity: false,
4608
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
4609
+ };
4610
+ }
4611
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
4612
+ if (!isAtCapacity2(pane)) {
4613
+ return {
4614
+ atCapacity: false,
4615
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
4616
+ };
4617
+ }
4618
+ return {
4619
+ atCapacity: true,
4620
+ reason: "capacity banner matched in recent pane output"
4621
+ };
4622
+ }
4623
+ function readDebounceState() {
4624
+ try {
4625
+ if (!existsSync13(DEBOUNCE_FILE)) return {};
4626
+ return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
4627
+ } catch {
4628
+ return {};
4629
+ }
4630
+ }
4631
+ function writeDebounceState(state) {
4632
+ try {
4633
+ if (!existsSync13(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4634
+ writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
4635
+ } catch {
4636
+ }
4637
+ }
4638
+ function isDebounced(targetSession) {
4639
+ const state = readDebounceState();
4640
+ const lastSent = state[targetSession] ?? 0;
4641
+ return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4642
+ }
4643
+ function recordDebounce(targetSession) {
4644
+ const state = readDebounceState();
4645
+ state[targetSession] = Date.now();
4646
+ const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4647
+ for (const key of Object.keys(state)) {
4648
+ if ((state[key] ?? 0) < cutoff) delete state[key];
4326
4649
  }
4327
- });
4328
-
4329
- // src/lib/tmux-routing.ts
4330
- import { execFileSync as execFileSync2, execSync as execSync8 } from "child_process";
4331
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync14, appendFileSync } from "fs";
4332
- import path18 from "path";
4333
- import os6 from "os";
4334
- import { fileURLToPath as fileURLToPath2 } from "url";
4335
- import { unlinkSync as unlinkSync4 } from "fs";
4336
- function spawnLockPath(sessionName) {
4337
- return path18.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4650
+ writeDebounceState(state);
4338
4651
  }
4339
- function isProcessAlive(pid) {
4652
+ function logIntercom(msg) {
4653
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
4654
+ `;
4655
+ process.stderr.write(`[intercom] ${msg}
4656
+ `);
4340
4657
  try {
4341
- process.kill(pid, 0);
4342
- return true;
4658
+ appendFileSync(INTERCOM_LOG2, line);
4343
4659
  } catch {
4344
- return false;
4345
4660
  }
4346
4661
  }
4347
- function acquireSpawnLock2(sessionName) {
4348
- if (!existsSync14(SPAWN_LOCK_DIR)) {
4349
- mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4350
- }
4351
- const lockFile = spawnLockPath(sessionName);
4352
- if (existsSync14(lockFile)) {
4353
- try {
4354
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
4355
- const age = Date.now() - lock.timestamp;
4356
- if (isProcessAlive(lock.pid) && age < 6e4) {
4357
- return false;
4662
+ function getSessionState(sessionName) {
4663
+ const transport = getTransport();
4664
+ if (!transport.isAlive(sessionName)) return "offline";
4665
+ try {
4666
+ const pane = transport.capturePane(sessionName, 5);
4667
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4668
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4669
+ return "no_claude";
4358
4670
  }
4359
- } catch {
4360
4671
  }
4672
+ if (/Running…/.test(pane)) return "tool";
4673
+ if (BUSY_PATTERN.test(pane)) return "thinking";
4674
+ return "idle";
4675
+ } catch {
4676
+ return "offline";
4361
4677
  }
4362
- writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
4363
- return true;
4364
4678
  }
4365
- function releaseSpawnLock2(sessionName) {
4679
+ function isSessionBusy(sessionName) {
4680
+ const state = getSessionState(sessionName);
4681
+ return state === "thinking" || state === "tool";
4682
+ }
4683
+ function isExeSession(sessionName) {
4684
+ return /^exe\d*$/.test(sessionName);
4685
+ }
4686
+ function sendIntercom(targetSession) {
4687
+ const transport = getTransport();
4688
+ if (isExeSession(targetSession)) {
4689
+ logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
4690
+ return "skipped_exe";
4691
+ }
4692
+ if (isDebounced(targetSession)) {
4693
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4694
+ return "debounced";
4695
+ }
4366
4696
  try {
4367
- unlinkSync4(spawnLockPath(sessionName));
4697
+ const sessions = transport.listSessions();
4698
+ if (!sessions.includes(targetSession)) {
4699
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4700
+ return "failed";
4701
+ }
4702
+ const sessionState = getSessionState(targetSession);
4703
+ if (sessionState === "no_claude") {
4704
+ queueIntercom(targetSession, "claude not running in session");
4705
+ recordDebounce(targetSession);
4706
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4707
+ return "queued";
4708
+ }
4709
+ if (sessionState === "thinking" || sessionState === "tool") {
4710
+ queueIntercom(targetSession, "session busy at send time");
4711
+ recordDebounce(targetSession);
4712
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4713
+ return "queued";
4714
+ }
4715
+ if (transport.isPaneInCopyMode(targetSession)) {
4716
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4717
+ transport.sendKeys(targetSession, "q");
4718
+ }
4719
+ transport.sendKeys(targetSession, "/exe-intercom");
4720
+ recordDebounce(targetSession);
4721
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4722
+ return "delivered";
4368
4723
  } catch {
4724
+ logIntercom(`FAIL \u2192 ${targetSession}`);
4725
+ return "failed";
4369
4726
  }
4370
4727
  }
4371
- function resolveBehaviorsExporterScript() {
4372
- try {
4373
- const thisFile = fileURLToPath2(import.meta.url);
4374
- const scriptPath = path18.join(
4375
- path18.dirname(thisFile),
4376
- "..",
4377
- "bin",
4378
- "exe-export-behaviors.js"
4379
- );
4380
- return existsSync14(scriptPath) ? scriptPath : null;
4381
- } catch {
4382
- return null;
4728
+ function notifyParentExe(sessionKey) {
4729
+ const target = getDispatchedBy(sessionKey);
4730
+ if (!target) {
4731
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
4732
+ `);
4733
+ return false;
4734
+ }
4735
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4736
+ `);
4737
+ const result = sendIntercom(target);
4738
+ if (result === "failed") {
4739
+ const rootExe = resolveExeSession();
4740
+ if (rootExe && rootExe !== target) {
4741
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4742
+ `);
4743
+ const fallback = sendIntercom(rootExe);
4744
+ return fallback !== "failed";
4745
+ }
4746
+ return false;
4383
4747
  }
4748
+ return true;
4384
4749
  }
4385
- function exportBehaviorsSync(agentId, projectName, sessionKey) {
4386
- const script = resolveBehaviorsExporterScript();
4387
- if (!script) return null;
4750
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4751
+ if (employeeName === "exe") {
4752
+ return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
4753
+ }
4388
4754
  try {
4389
- const output = execFileSync2(
4390
- process.execPath,
4391
- [script, agentId, projectName, sessionKey],
4392
- { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
4393
- ).trim();
4394
- return output.length > 0 ? output : null;
4755
+ assertEmployeeLimitSync();
4395
4756
  } catch (err) {
4396
- process.stderr.write(
4397
- `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
4757
+ if (err instanceof PlanLimitError) {
4758
+ return { status: "failed", sessionName: "", error: err.message };
4759
+ }
4760
+ }
4761
+ if (/-exe\d*$/.test(employeeName)) {
4762
+ const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
4763
+ return {
4764
+ status: "failed",
4765
+ sessionName: "",
4766
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
4767
+ };
4768
+ }
4769
+ if (!/^exe\d+$/.test(exeSession)) {
4770
+ const root = extractRootExe(exeSession);
4771
+ if (root) {
4772
+ process.stderr.write(
4773
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
4398
4774
  `
4775
+ );
4776
+ exeSession = root;
4777
+ } else {
4778
+ return {
4779
+ status: "failed",
4780
+ sessionName: "",
4781
+ error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
4782
+ };
4783
+ }
4784
+ }
4785
+ let effectiveInstance = opts?.instance;
4786
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
4787
+ const free = findFreeInstance(
4788
+ employeeName,
4789
+ exeSession,
4790
+ opts.maxAutoInstances ?? 10
4399
4791
  );
4400
- return null;
4792
+ if (free === null) {
4793
+ return {
4794
+ status: "failed",
4795
+ sessionName: employeeSessionName(employeeName, exeSession),
4796
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4797
+ };
4798
+ }
4799
+ effectiveInstance = free === 0 ? void 0 : free;
4401
4800
  }
4801
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
4802
+ if (isEmployeeAlive(sessionName)) {
4803
+ const result2 = sendIntercom(sessionName);
4804
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4805
+ return { status: "intercom_sent", sessionName };
4806
+ }
4807
+ if (result2 === "delivered") {
4808
+ return { status: "intercom_unprocessed", sessionName };
4809
+ }
4810
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
4811
+ }
4812
+ const spawnOpts = { ...opts, instance: effectiveInstance };
4813
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
4814
+ if (result.error) {
4815
+ return { status: "failed", sessionName, error: result.error };
4816
+ }
4817
+ return { status: "spawned", sessionName };
4402
4818
  }
4403
- function getMySession() {
4404
- return getTransport().getMySession();
4405
- }
4406
- function employeeSessionName(employee, exeSession, instance) {
4407
- const suffix = instance != null && instance > 0 ? String(instance) : "";
4408
- return `${employee}${suffix}-${exeSession}`;
4409
- }
4410
- function extractRootExe(name) {
4411
- const match = name.match(/(exe\d+)$/);
4412
- return match?.[1] ?? null;
4413
- }
4414
- function getParentExe(sessionKey) {
4819
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4820
+ const transport = getTransport();
4821
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4822
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4823
+ const logDir = path17.join(os6.homedir(), ".exe-os", "session-logs");
4824
+ const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4825
+ if (!existsSync13(logDir)) {
4826
+ mkdirSync6(logDir, { recursive: true });
4827
+ }
4828
+ transport.kill(sessionName);
4829
+ let cleanupSuffix = "";
4415
4830
  try {
4416
- const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4417
- return data.parentExe || null;
4831
+ const thisFile = fileURLToPath2(import.meta.url);
4832
+ const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4833
+ if (existsSync13(cleanupScript)) {
4834
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4835
+ }
4418
4836
  } catch {
4419
- return null;
4420
4837
  }
4421
- }
4422
- function getDispatchedBy(sessionKey) {
4423
4838
  try {
4424
- const data = JSON.parse(readFileSync12(
4425
- path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4426
- "utf8"
4427
- ));
4428
- return data.dispatchedBy ?? data.parentExe ?? null;
4839
+ const claudeJsonPath = path17.join(os6.homedir(), ".claude.json");
4840
+ let claudeJson = {};
4841
+ try {
4842
+ claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
4843
+ } catch {
4844
+ }
4845
+ if (!claudeJson.projects) claudeJson.projects = {};
4846
+ const projects = claudeJson.projects;
4847
+ const trustDir = opts?.cwd ?? projectDir;
4848
+ if (!projects[trustDir]) projects[trustDir] = {};
4849
+ projects[trustDir].hasTrustDialogAccepted = true;
4850
+ writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4429
4851
  } catch {
4430
- return null;
4431
4852
  }
4432
- }
4433
- function resolveExeSession() {
4434
- const mySession = getMySession();
4435
- if (!mySession) return null;
4436
4853
  try {
4437
- const key = getSessionKey();
4438
- const parentExe = getParentExe(key);
4439
- if (parentExe) {
4440
- return extractRootExe(parentExe) ?? parentExe;
4854
+ const settingsDir = path17.join(os6.homedir(), ".claude", "projects");
4855
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4856
+ const projSettingsDir = path17.join(settingsDir, normalizedKey);
4857
+ const settingsPath = path17.join(projSettingsDir, "settings.json");
4858
+ let settings = {};
4859
+ try {
4860
+ settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
4861
+ } catch {
4862
+ }
4863
+ const perms = settings.permissions ?? {};
4864
+ const allow = perms.allow ?? [];
4865
+ const toolNames = [
4866
+ "recall_my_memory",
4867
+ "store_memory",
4868
+ "create_task",
4869
+ "update_task",
4870
+ "list_tasks",
4871
+ "get_task",
4872
+ "ask_team_memory",
4873
+ "store_behavior",
4874
+ "get_identity",
4875
+ "send_message"
4876
+ ];
4877
+ const requiredTools = expandDualPrefixTools(toolNames);
4878
+ let changed = false;
4879
+ for (const tool of requiredTools) {
4880
+ if (!allow.includes(tool)) {
4881
+ allow.push(tool);
4882
+ changed = true;
4883
+ }
4884
+ }
4885
+ if (changed) {
4886
+ perms.allow = allow;
4887
+ settings.permissions = perms;
4888
+ mkdirSync6(projSettingsDir, { recursive: true });
4889
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4441
4890
  }
4442
4891
  } catch {
4443
4892
  }
4444
- return extractRootExe(mySession) ?? mySession;
4445
- }
4446
- function isEmployeeAlive(sessionName) {
4447
- return getTransport().isAlive(sessionName);
4448
- }
4449
- function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
4450
- const base = employeeSessionName(employeeName, exeSession);
4451
- if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
4452
- for (let i = 2; i <= maxInstances; i++) {
4453
- const candidate = employeeSessionName(employeeName, exeSession, i);
4454
- if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
4455
- }
4456
- return null;
4457
- }
4458
- function readDebounceState() {
4459
- try {
4460
- if (!existsSync14(DEBOUNCE_FILE)) return {};
4461
- return JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
4462
- } catch {
4463
- return {};
4464
- }
4465
- }
4466
- function writeDebounceState(state) {
4467
- try {
4468
- if (!existsSync14(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4469
- writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
4470
- } catch {
4893
+ const spawnCwd = opts?.cwd ?? projectDir;
4894
+ const useExeAgent = !!(opts?.model && opts?.provider);
4895
+ const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
4896
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4897
+ let identityFlag = "";
4898
+ let behaviorsFlag = "";
4899
+ let legacyFallbackWarned = false;
4900
+ if (!useExeAgent && !useBinSymlink) {
4901
+ const identityPath2 = path17.join(
4902
+ os6.homedir(),
4903
+ ".exe-os",
4904
+ "identity",
4905
+ `${employeeName}.md`
4906
+ );
4907
+ _resetCcAgentSupportCache();
4908
+ const hasAgentFlag = claudeSupportsAgentFlag();
4909
+ if (hasAgentFlag) {
4910
+ identityFlag = ` --agent ${employeeName}`;
4911
+ } else if (existsSync13(identityPath2)) {
4912
+ identityFlag = ` --append-system-prompt-file ${identityPath2}`;
4913
+ legacyFallbackWarned = true;
4914
+ }
4915
+ const behaviorsFile = exportBehaviorsSync(
4916
+ employeeName,
4917
+ path17.basename(spawnCwd),
4918
+ sessionName
4919
+ );
4920
+ if (behaviorsFile) {
4921
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
4922
+ }
4471
4923
  }
4472
- }
4473
- function isDebounced(targetSession) {
4474
- const state = readDebounceState();
4475
- const lastSent = state[targetSession] ?? 0;
4476
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
4477
- }
4478
- function recordDebounce(targetSession) {
4479
- const state = readDebounceState();
4480
- state[targetSession] = Date.now();
4481
- const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
4482
- for (const key of Object.keys(state)) {
4483
- if ((state[key] ?? 0) < cutoff) delete state[key];
4924
+ if (legacyFallbackWarned) {
4925
+ process.stderr.write(
4926
+ `[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.
4927
+ `
4928
+ );
4484
4929
  }
4485
- writeDebounceState(state);
4486
- }
4487
- function logIntercom(msg) {
4488
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
4489
- `;
4490
- process.stderr.write(`[intercom] ${msg}
4491
- `);
4930
+ let sessionContextFlag = "";
4492
4931
  try {
4493
- appendFileSync(INTERCOM_LOG2, line);
4932
+ const ctxDir = path17.join(os6.homedir(), ".exe-os", "session-cache");
4933
+ mkdirSync6(ctxDir, { recursive: true });
4934
+ const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
4935
+ const ctxContent = [
4936
+ `## Session Context`,
4937
+ `You are running in tmux session: ${sessionName}.`,
4938
+ `Your parent exe session is ${exeSession}.`,
4939
+ `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
4940
+ ].join("\n");
4941
+ writeFileSync7(ctxFile, ctxContent);
4942
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
4494
4943
  } catch {
4495
4944
  }
4496
- }
4497
- function getSessionState(sessionName) {
4498
- const transport = getTransport();
4499
- if (!transport.isAlive(sessionName)) return "offline";
4500
- try {
4501
- const pane = transport.capturePane(sessionName, 5);
4502
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4503
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4504
- return "no_claude";
4945
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
4946
+ if (ccProvider !== DEFAULT_PROVIDER) {
4947
+ const cfg = PROVIDER_TABLE[ccProvider];
4948
+ if (cfg?.apiKeyEnv) {
4949
+ const keyVal = process.env[cfg.apiKeyEnv];
4950
+ if (keyVal) {
4951
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
4505
4952
  }
4506
4953
  }
4507
- if (/Running…/.test(pane)) return "tool";
4508
- if (BUSY_PATTERN.test(pane)) return "thinking";
4509
- return "idle";
4510
- } catch {
4511
- return "offline";
4512
4954
  }
4513
- }
4514
- function isExeSession(sessionName) {
4515
- return /^exe\d*$/.test(sessionName);
4516
- }
4517
- function sendIntercom(targetSession) {
4518
- const transport = getTransport();
4519
- if (isExeSession(targetSession)) {
4520
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
4521
- return "skipped_exe";
4955
+ let spawnCommand;
4956
+ if (useExeAgent) {
4957
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4958
+ } else if (useBinSymlink) {
4959
+ const binName = `${employeeName}-${ccProvider}`;
4960
+ process.stderr.write(
4961
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
4962
+ `
4963
+ );
4964
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
4965
+ } else {
4966
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
4522
4967
  }
4523
- if (isDebounced(targetSession)) {
4524
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
4525
- return "debounced";
4968
+ const spawnResult = transport.spawn(sessionName, {
4969
+ cwd: spawnCwd,
4970
+ command: spawnCommand
4971
+ });
4972
+ if (spawnResult.error) {
4973
+ releaseSpawnLock2(sessionName);
4974
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4526
4975
  }
4976
+ transport.pipeLog(sessionName, logFile);
4527
4977
  try {
4528
- const sessions = transport.listSessions();
4529
- if (!sessions.includes(targetSession)) {
4530
- logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4531
- return "failed";
4532
- }
4533
- const sessionState = getSessionState(targetSession);
4534
- if (sessionState === "no_claude") {
4535
- queueIntercom(targetSession, "claude not running in session");
4536
- recordDebounce(targetSession);
4537
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4538
- return "queued";
4539
- }
4540
- if (sessionState === "thinking" || sessionState === "tool") {
4541
- queueIntercom(targetSession, "session busy at send time");
4542
- recordDebounce(targetSession);
4543
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
4544
- return "queued";
4978
+ const mySession = getMySession();
4979
+ const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4980
+ writeFileSync7(dispatchInfo, JSON.stringify({
4981
+ dispatchedBy: mySession,
4982
+ rootExe: exeSession,
4983
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
4984
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
4985
+ }));
4986
+ } catch {
4987
+ }
4988
+ let booted = false;
4989
+ for (let i = 0; i < 30; i++) {
4990
+ try {
4991
+ execSync7("sleep 0.5");
4992
+ } catch {
4545
4993
  }
4546
- if (transport.isPaneInCopyMode(targetSession)) {
4547
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4548
- transport.sendKeys(targetSession, "q");
4994
+ try {
4995
+ const pane = transport.capturePane(sessionName);
4996
+ if (useExeAgent) {
4997
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
4998
+ booted = true;
4999
+ break;
5000
+ }
5001
+ } else {
5002
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
5003
+ booted = true;
5004
+ break;
5005
+ }
5006
+ }
5007
+ } catch {
4549
5008
  }
4550
- transport.sendKeys(targetSession, "/exe-intercom");
4551
- recordDebounce(targetSession);
4552
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4553
- return "delivered";
4554
- } catch {
4555
- logIntercom(`FAIL \u2192 ${targetSession}`);
4556
- return "failed";
4557
5009
  }
4558
- }
4559
- function notifyParentExe(sessionKey) {
4560
- const target = getDispatchedBy(sessionKey);
4561
- if (!target) {
4562
- process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
4563
- `);
4564
- return false;
5010
+ if (!booted) {
5011
+ releaseSpawnLock2(sessionName);
5012
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4565
5013
  }
4566
- process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4567
- `);
4568
- const result = sendIntercom(target);
4569
- if (result === "failed") {
4570
- const rootExe = resolveExeSession();
4571
- if (rootExe && rootExe !== target) {
4572
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4573
- `);
4574
- const fallback = sendIntercom(rootExe);
4575
- return fallback !== "failed";
5014
+ if (!useExeAgent) {
5015
+ try {
5016
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
5017
+ } catch {
4576
5018
  }
4577
- return false;
4578
5019
  }
4579
- return true;
5020
+ registerSession({
5021
+ windowName: sessionName,
5022
+ agentId: employeeName,
5023
+ projectDir: spawnCwd,
5024
+ parentExe: exeSession,
5025
+ pid: 0,
5026
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
5027
+ });
5028
+ releaseSpawnLock2(sessionName);
5029
+ return { sessionName };
4580
5030
  }
4581
- function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4582
- if (employeeName === "exe") {
4583
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
5031
+ 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;
5032
+ var init_tmux_routing = __esm({
5033
+ "src/lib/tmux-routing.ts"() {
5034
+ "use strict";
5035
+ init_session_registry();
5036
+ init_session_key();
5037
+ init_transport();
5038
+ init_cc_agent_support();
5039
+ init_mcp_prefix();
5040
+ init_provider_table();
5041
+ init_intercom_queue();
5042
+ init_plan_limits();
5043
+ SPAWN_LOCK_DIR = path17.join(os6.homedir(), ".exe-os", "spawn-locks");
5044
+ SESSION_CACHE = path17.join(os6.homedir(), ".exe-os", "session-cache");
5045
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5046
+ VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
5047
+ VERIFY_PANE_LINES = 200;
5048
+ INTERCOM_DEBOUNCE_MS = 3e4;
5049
+ INTERCOM_LOG2 = path17.join(os6.homedir(), ".exe-os", "intercom.log");
5050
+ DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
5051
+ DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5052
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4584
5053
  }
4585
- try {
4586
- assertEmployeeLimitSync();
4587
- } catch (err) {
4588
- if (err instanceof PlanLimitError) {
4589
- return { status: "failed", sessionName: "", error: err.message };
4590
- }
5054
+ });
5055
+
5056
+ // src/lib/tasks-crud.ts
5057
+ import crypto7 from "crypto";
5058
+ import path18 from "path";
5059
+ import { execSync as execSync8 } from "child_process";
5060
+ import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
5061
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
5062
+ async function writeCheckpoint(input) {
5063
+ const client = getClient();
5064
+ const row = await resolveTask(client, input.taskId);
5065
+ const taskId = String(row.id);
5066
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5067
+ const blockedByIds = [];
5068
+ if (row.blocked_by) {
5069
+ blockedByIds.push(String(row.blocked_by));
4591
5070
  }
4592
- if (/-exe\d*$/.test(employeeName)) {
4593
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
4594
- return {
4595
- status: "failed",
4596
- sessionName: "",
4597
- error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
4598
- };
5071
+ const checkpoint = {
5072
+ step: input.step,
5073
+ context_summary: input.contextSummary,
5074
+ files_touched: input.filesTouched ?? [],
5075
+ blocked_by_ids: blockedByIds,
5076
+ last_checkpoint_at: now
5077
+ };
5078
+ const result = await client.execute({
5079
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
5080
+ args: [JSON.stringify(checkpoint), now, taskId]
5081
+ });
5082
+ if (result.rowsAffected === 0) {
5083
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
4599
5084
  }
4600
- let effectiveInstance = opts?.instance;
4601
- if (effectiveInstance === void 0 && opts?.autoInstance) {
4602
- const free = findFreeInstance(
4603
- employeeName,
4604
- exeSession,
4605
- opts.maxAutoInstances ?? 10
5085
+ const countResult = await client.execute({
5086
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
5087
+ args: [taskId]
5088
+ });
5089
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
5090
+ return { checkpointCount };
5091
+ }
5092
+ function extractParentFromContext(contextBody) {
5093
+ if (!contextBody) return null;
5094
+ const match = contextBody.match(
5095
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
5096
+ );
5097
+ return match ? match[1].toLowerCase() : null;
5098
+ }
5099
+ function slugify(title) {
5100
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
5101
+ }
5102
+ async function resolveTask(client, identifier) {
5103
+ let result = await client.execute({
5104
+ sql: "SELECT * FROM tasks WHERE id = ?",
5105
+ args: [identifier]
5106
+ });
5107
+ if (result.rows.length === 1) return result.rows[0];
5108
+ result = await client.execute({
5109
+ sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
5110
+ args: [`%${identifier}%`]
5111
+ });
5112
+ if (result.rows.length === 1) return result.rows[0];
5113
+ if (result.rows.length > 1) {
5114
+ const exact = result.rows.filter(
5115
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
5116
+ );
5117
+ if (exact.length === 1) return exact[0];
5118
+ const candidates = exact.length > 1 ? exact : result.rows;
5119
+ const active = candidates.filter(
5120
+ (r) => !["done", "cancelled"].includes(String(r.status))
5121
+ );
5122
+ if (active.length === 1) return active[0];
5123
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
5124
+ throw new Error(
5125
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
4606
5126
  );
4607
- if (free === null) {
4608
- return {
4609
- status: "failed",
4610
- sessionName: employeeSessionName(employeeName, exeSession),
4611
- error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4612
- };
4613
- }
4614
- effectiveInstance = free === 0 ? void 0 : free;
4615
- }
4616
- const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
4617
- if (isEmployeeAlive(sessionName)) {
4618
- const result2 = sendIntercom(sessionName);
4619
- if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4620
- return { status: "intercom_sent", sessionName };
4621
- }
4622
- if (result2 === "delivered") {
4623
- return { status: "intercom_unprocessed", sessionName };
4624
- }
4625
- return { status: "failed", sessionName, error: "intercom delivery failed" };
4626
5127
  }
4627
- const spawnOpts = { ...opts, instance: effectiveInstance };
4628
- const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
4629
- if (result.error) {
4630
- return { status: "failed", sessionName, error: result.error };
5128
+ result = await client.execute({
5129
+ sql: "SELECT * FROM tasks WHERE title LIKE ?",
5130
+ args: [`%${identifier}%`]
5131
+ });
5132
+ if (result.rows.length === 1) return result.rows[0];
5133
+ if (result.rows.length > 1) {
5134
+ const active = result.rows.filter(
5135
+ (r) => !["done", "cancelled"].includes(String(r.status))
5136
+ );
5137
+ if (active.length === 1) return active[0];
5138
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
5139
+ throw new Error(
5140
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
5141
+ );
4631
5142
  }
4632
- return { status: "spawned", sessionName };
5143
+ throw new Error(`Task not found: ${identifier}`);
4633
5144
  }
4634
- function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4635
- const transport = getTransport();
4636
- const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4637
- const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4638
- const logDir = path18.join(os6.homedir(), ".exe-os", "session-logs");
4639
- const logFile = path18.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4640
- if (!existsSync14(logDir)) {
4641
- mkdirSync6(logDir, { recursive: true });
5145
+ async function createTaskCore(input) {
5146
+ const client = getClient();
5147
+ const id = crypto7.randomUUID();
5148
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5149
+ const slug = slugify(input.title);
5150
+ const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
5151
+ let blockedById = null;
5152
+ const initialStatus = input.blockedBy ? "blocked" : "open";
5153
+ if (input.blockedBy) {
5154
+ const blocker = await resolveTask(client, input.blockedBy);
5155
+ blockedById = String(blocker.id);
4642
5156
  }
4643
- transport.kill(sessionName);
4644
- let cleanupSuffix = "";
4645
- try {
4646
- const thisFile = fileURLToPath2(import.meta.url);
4647
- const cleanupScript = path18.join(path18.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4648
- if (existsSync14(cleanupScript)) {
4649
- cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5157
+ let parentTaskId = null;
5158
+ let parentRef = input.parentTaskId;
5159
+ if (!parentRef) {
5160
+ const extracted = extractParentFromContext(input.context);
5161
+ if (extracted) {
5162
+ parentRef = extracted;
5163
+ process.stderr.write(
5164
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
5165
+ );
4650
5166
  }
4651
- } catch {
4652
5167
  }
4653
- try {
4654
- const claudeJsonPath = path18.join(os6.homedir(), ".claude.json");
4655
- let claudeJson = {};
5168
+ if (parentRef) {
4656
5169
  try {
4657
- claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
4658
- } catch {
5170
+ const parent = await resolveTask(client, parentRef);
5171
+ parentTaskId = String(parent.id);
5172
+ } catch (err) {
5173
+ if (!input.parentTaskId) {
5174
+ throw new Error(
5175
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
5176
+ );
5177
+ }
5178
+ throw err;
4659
5179
  }
4660
- if (!claudeJson.projects) claudeJson.projects = {};
4661
- const projects = claudeJson.projects;
4662
- const trustDir = opts?.cwd ?? projectDir;
4663
- if (!projects[trustDir]) projects[trustDir] = {};
4664
- projects[trustDir].hasTrustDialogAccepted = true;
4665
- writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
4666
- } catch {
4667
5180
  }
4668
- try {
4669
- const settingsDir = path18.join(os6.homedir(), ".claude", "projects");
4670
- const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4671
- const projSettingsDir = path18.join(settingsDir, normalizedKey);
4672
- const settingsPath = path18.join(projSettingsDir, "settings.json");
4673
- let settings = {};
5181
+ let warning;
5182
+ const dupCheck = await client.execute({
5183
+ sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
5184
+ args: [input.title, input.assignedTo]
5185
+ });
5186
+ if (dupCheck.rows.length > 0) {
5187
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
5188
+ }
5189
+ if (input.baseDir) {
4674
5190
  try {
4675
- settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
5191
+ await mkdir4(path18.join(input.baseDir, "exe", "output"), { recursive: true });
5192
+ await mkdir4(path18.join(input.baseDir, "exe", "research"), { recursive: true });
5193
+ await ensureArchitectureDoc(input.baseDir, input.projectName);
5194
+ await ensureGitignoreExe(input.baseDir);
4676
5195
  } catch {
4677
5196
  }
4678
- const perms = settings.permissions ?? {};
4679
- const allow = perms.allow ?? [];
4680
- const toolNames = [
4681
- "recall_my_memory",
4682
- "store_memory",
4683
- "create_task",
4684
- "update_task",
4685
- "list_tasks",
4686
- "get_task",
4687
- "ask_team_memory",
4688
- "store_behavior",
4689
- "get_identity",
4690
- "send_message"
4691
- ];
4692
- const requiredTools = expandDualPrefixTools(toolNames);
4693
- let changed = false;
4694
- for (const tool of requiredTools) {
4695
- if (!allow.includes(tool)) {
4696
- allow.push(tool);
4697
- changed = true;
4698
- }
4699
- }
4700
- if (changed) {
4701
- perms.allow = allow;
4702
- settings.permissions = perms;
4703
- mkdirSync6(projSettingsDir, { recursive: true });
4704
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4705
- }
5197
+ }
5198
+ const complexity = input.complexity ?? "standard";
5199
+ let sessionScope = null;
5200
+ try {
5201
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
5202
+ sessionScope = resolveExeSession2();
4706
5203
  } catch {
4707
5204
  }
4708
- const spawnCwd = opts?.cwd ?? projectDir;
4709
- const useExeAgent = !!(opts?.model && opts?.provider);
4710
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
4711
- const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4712
- let identityFlag = "";
4713
- let behaviorsFlag = "";
4714
- let legacyFallbackWarned = false;
4715
- if (!useExeAgent && !useBinSymlink) {
4716
- const identityPath2 = path18.join(
4717
- os6.homedir(),
4718
- ".exe-os",
4719
- "identity",
4720
- `${employeeName}.md`
4721
- );
4722
- _resetCcAgentSupportCache();
4723
- const hasAgentFlag = claudeSupportsAgentFlag();
4724
- if (hasAgentFlag) {
4725
- identityFlag = ` --agent ${employeeName}`;
4726
- } else if (existsSync14(identityPath2)) {
4727
- identityFlag = ` --append-system-prompt-file ${identityPath2}`;
4728
- legacyFallbackWarned = true;
4729
- }
4730
- const behaviorsFile = exportBehaviorsSync(
4731
- employeeName,
4732
- path18.basename(spawnCwd),
4733
- sessionName
4734
- );
4735
- if (behaviorsFile) {
4736
- behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
4737
- }
5205
+ await client.execute({
5206
+ 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)
5207
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5208
+ args: [
5209
+ id,
5210
+ input.title,
5211
+ input.assignedTo,
5212
+ input.assignedBy,
5213
+ input.projectName,
5214
+ input.priority,
5215
+ initialStatus,
5216
+ taskFile,
5217
+ blockedById,
5218
+ parentTaskId,
5219
+ input.reviewer ?? null,
5220
+ input.context,
5221
+ complexity,
5222
+ input.budgetTokens ?? null,
5223
+ input.budgetFallbackModel ?? null,
5224
+ 0,
5225
+ null,
5226
+ sessionScope,
5227
+ now,
5228
+ now
5229
+ ]
5230
+ });
5231
+ return {
5232
+ id,
5233
+ title: input.title,
5234
+ assignedTo: input.assignedTo,
5235
+ assignedBy: input.assignedBy,
5236
+ projectName: input.projectName,
5237
+ priority: input.priority,
5238
+ status: initialStatus,
5239
+ taskFile,
5240
+ createdAt: now,
5241
+ updatedAt: now,
5242
+ warning,
5243
+ budgetTokens: input.budgetTokens ?? null,
5244
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
5245
+ tokensUsed: 0,
5246
+ tokensWarnedAt: null
5247
+ };
5248
+ }
5249
+ async function listTasks(input) {
5250
+ const client = getClient();
5251
+ const conditions = [];
5252
+ const args = [];
5253
+ if (input.assignedTo) {
5254
+ conditions.push("assigned_to = ?");
5255
+ args.push(input.assignedTo);
5256
+ }
5257
+ if (input.status) {
5258
+ conditions.push("status = ?");
5259
+ args.push(input.status);
5260
+ } else {
5261
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
4738
5262
  }
4739
- if (legacyFallbackWarned) {
4740
- process.stderr.write(
4741
- `[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.
4742
- `
4743
- );
5263
+ if (input.projectName) {
5264
+ conditions.push("project_name = ?");
5265
+ args.push(input.projectName);
5266
+ }
5267
+ if (input.priority) {
5268
+ conditions.push("priority = ?");
5269
+ args.push(input.priority);
4744
5270
  }
4745
- let sessionContextFlag = "";
4746
5271
  try {
4747
- const ctxDir = path18.join(os6.homedir(), ".exe-os", "session-cache");
4748
- mkdirSync6(ctxDir, { recursive: true });
4749
- const ctxFile = path18.join(ctxDir, `session-context-${sessionName}.md`);
4750
- const ctxContent = [
4751
- `## Session Context`,
4752
- `You are running in tmux session: ${sessionName}.`,
4753
- `Your parent exe session is ${exeSession}.`,
4754
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
4755
- ].join("\n");
4756
- writeFileSync7(ctxFile, ctxContent);
4757
- sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5272
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
5273
+ const session = resolveExeSession2();
5274
+ if (session) {
5275
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
5276
+ args.push(session);
5277
+ }
4758
5278
  } catch {
4759
5279
  }
4760
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
4761
- if (ccProvider !== DEFAULT_PROVIDER) {
4762
- const cfg = PROVIDER_TABLE[ccProvider];
4763
- if (cfg?.apiKeyEnv) {
4764
- const keyVal = process.env[cfg.apiKeyEnv];
4765
- if (keyVal) {
4766
- envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
4767
- }
5280
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5281
+ const result = await client.execute({
5282
+ 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`,
5283
+ args
5284
+ });
5285
+ return result.rows.map((r) => ({
5286
+ id: String(r.id),
5287
+ title: String(r.title),
5288
+ assignedTo: String(r.assigned_to),
5289
+ assignedBy: String(r.assigned_by),
5290
+ projectName: String(r.project_name),
5291
+ priority: String(r.priority),
5292
+ status: String(r.status),
5293
+ taskFile: String(r.task_file),
5294
+ createdAt: String(r.created_at),
5295
+ updatedAt: String(r.updated_at),
5296
+ checkpointCount: Number(r.checkpoint_count ?? 0),
5297
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
5298
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
5299
+ tokensUsed: Number(r.tokens_used ?? 0),
5300
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
5301
+ }));
5302
+ }
5303
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
5304
+ if (!taskContext) return null;
5305
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
5306
+ try {
5307
+ const since = new Date(taskCreatedAt).toISOString();
5308
+ const branch = execSync8(
5309
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
5310
+ { encoding: "utf8", timeout: 3e3 }
5311
+ ).trim();
5312
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
5313
+ const commitCount = execSync8(
5314
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
5315
+ { encoding: "utf8", timeout: 5e3 }
5316
+ ).trim();
5317
+ const count = parseInt(commitCount, 10);
5318
+ if (count === 0) {
5319
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
4768
5320
  }
5321
+ return null;
5322
+ } catch {
5323
+ return null;
4769
5324
  }
4770
- let spawnCommand;
4771
- if (useExeAgent) {
4772
- spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
4773
- } else if (useBinSymlink) {
4774
- const binName = `${employeeName}-${ccProvider}`;
5325
+ }
5326
+ async function updateTaskStatus(input) {
5327
+ const client = getClient();
5328
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5329
+ const row = await resolveTask(client, input.taskId);
5330
+ const taskId = String(row.id);
5331
+ const taskFile = String(row.task_file);
5332
+ if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
4775
5333
  process.stderr.write(
4776
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
5334
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
4777
5335
  `
4778
5336
  );
4779
- spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
4780
- } else {
4781
- spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
4782
- }
4783
- const spawnResult = transport.spawn(sessionName, {
4784
- cwd: spawnCwd,
4785
- command: spawnCommand
4786
- });
4787
- if (spawnResult.error) {
4788
- releaseSpawnLock2(sessionName);
4789
- return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4790
5337
  }
4791
- transport.pipeLog(sessionName, logFile);
4792
- try {
4793
- const mySession = getMySession();
4794
- const dispatchInfo = path18.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4795
- writeFileSync7(dispatchInfo, JSON.stringify({
4796
- dispatchedBy: mySession,
4797
- rootExe: exeSession,
4798
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
4799
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
4800
- }));
4801
- } catch {
5338
+ if (input.status === "done") {
5339
+ const existingRow = await client.execute({
5340
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
5341
+ args: [taskId]
5342
+ });
5343
+ if (existingRow.rows.length > 0) {
5344
+ const ctx = existingRow.rows[0];
5345
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
5346
+ if (warning) {
5347
+ input.result = input.result ? `\u26A0\uFE0F ${warning}
5348
+
5349
+ ${input.result}` : `\u26A0\uFE0F ${warning}`;
5350
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
5351
+ `);
5352
+ }
5353
+ }
4802
5354
  }
4803
- let booted = false;
4804
- for (let i = 0; i < 30; i++) {
4805
- try {
4806
- execSync8("sleep 0.5");
4807
- } catch {
5355
+ if (input.status === "in_progress") {
5356
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
5357
+ const claim = await client.execute({
5358
+ sql: `UPDATE tasks
5359
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
5360
+ WHERE id = ? AND status = 'open'`,
5361
+ args: [tmuxSession, now, taskId]
5362
+ });
5363
+ if (claim.rowsAffected === 0) {
5364
+ const current = await client.execute({
5365
+ sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
5366
+ args: [taskId]
5367
+ });
5368
+ const cur = current.rows[0];
5369
+ const status = cur?.status ?? "unknown";
5370
+ const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
5371
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
4808
5372
  }
4809
5373
  try {
4810
- const pane = transport.capturePane(sessionName);
4811
- if (useExeAgent) {
4812
- if (pane.includes("[exe-agent]") || pane.includes("online")) {
4813
- booted = true;
4814
- break;
4815
- }
4816
- } else {
4817
- if (pane.includes("Claude Code") || pane.includes("\u276F")) {
4818
- booted = true;
4819
- break;
4820
- }
4821
- }
5374
+ await writeCheckpoint({
5375
+ taskId,
5376
+ step: "claimed",
5377
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
5378
+ });
4822
5379
  } catch {
4823
5380
  }
5381
+ return { row, taskFile, now, taskId };
4824
5382
  }
4825
- if (!booted) {
4826
- releaseSpawnLock2(sessionName);
4827
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
5383
+ if (input.result) {
5384
+ await client.execute({
5385
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
5386
+ args: [input.status, input.result, now, taskId]
5387
+ });
5388
+ } else {
5389
+ await client.execute({
5390
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
5391
+ args: [input.status, now, taskId]
5392
+ });
5393
+ }
5394
+ try {
5395
+ await writeCheckpoint({
5396
+ taskId,
5397
+ step: `status_transition:${input.status}`,
5398
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
5399
+ });
5400
+ } catch {
5401
+ }
5402
+ return { row, taskFile, now, taskId };
5403
+ }
5404
+ async function deleteTaskCore(taskId, _baseDir) {
5405
+ const client = getClient();
5406
+ const row = await resolveTask(client, taskId);
5407
+ const id = String(row.id);
5408
+ const taskFile = String(row.task_file);
5409
+ const assignedTo = String(row.assigned_to);
5410
+ const assignedBy = String(row.assigned_by);
5411
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
5412
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
5413
+ return { taskFile, assignedTo, assignedBy, taskSlug };
5414
+ }
5415
+ async function ensureArchitectureDoc(baseDir, projectName) {
5416
+ const archPath = path18.join(baseDir, "exe", "ARCHITECTURE.md");
5417
+ try {
5418
+ if (existsSync14(archPath)) return;
5419
+ const template = [
5420
+ `# ${projectName} \u2014 System Architecture`,
5421
+ "",
5422
+ "> Employees: read this before every task. Update it when you change system structure.",
5423
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
5424
+ "",
5425
+ "## Overview",
5426
+ "",
5427
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
5428
+ "",
5429
+ "## Key Components",
5430
+ "",
5431
+ "<!-- List the major modules, services, or subsystems. -->",
5432
+ "",
5433
+ "## Data Flow",
5434
+ "",
5435
+ "<!-- How does data move through the system? What writes where? -->",
5436
+ "",
5437
+ "## Invariants",
5438
+ "",
5439
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
5440
+ "",
5441
+ "## Dependencies",
5442
+ "",
5443
+ "<!-- What depends on what? If I change X, what else is affected? -->",
5444
+ ""
5445
+ ].join("\n");
5446
+ await writeFile4(archPath, template, "utf-8");
5447
+ } catch {
4828
5448
  }
4829
- if (!useExeAgent) {
4830
- try {
4831
- transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
4832
- } catch {
5449
+ }
5450
+ async function ensureGitignoreExe(baseDir) {
5451
+ const gitignorePath = path18.join(baseDir, ".gitignore");
5452
+ try {
5453
+ if (existsSync14(gitignorePath)) {
5454
+ const content = readFileSync12(gitignorePath, "utf-8");
5455
+ if (/^\/?exe\/?$/m.test(content)) return;
5456
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
5457
+ } else {
5458
+ await writeFile4(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
4833
5459
  }
5460
+ } catch {
4834
5461
  }
4835
- registerSession({
4836
- windowName: sessionName,
4837
- agentId: employeeName,
4838
- projectDir: spawnCwd,
4839
- parentExe: exeSession,
4840
- pid: 0,
4841
- registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4842
- });
4843
- releaseSpawnLock2(sessionName);
4844
- return { sessionName };
4845
5462
  }
4846
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
4847
- var init_tmux_routing = __esm({
4848
- "src/lib/tmux-routing.ts"() {
5463
+ var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
5464
+ var init_tasks_crud = __esm({
5465
+ "src/lib/tasks-crud.ts"() {
4849
5466
  "use strict";
4850
- init_session_registry();
4851
- init_session_key();
4852
- init_transport();
4853
- init_cc_agent_support();
4854
- init_mcp_prefix();
4855
- init_provider_table();
4856
- init_intercom_queue();
4857
- init_plan_limits();
4858
- SPAWN_LOCK_DIR = path18.join(os6.homedir(), ".exe-os", "spawn-locks");
4859
- SESSION_CACHE = path18.join(os6.homedir(), ".exe-os", "session-cache");
4860
- BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4861
- INTERCOM_DEBOUNCE_MS = 3e4;
4862
- INTERCOM_LOG2 = path18.join(os6.homedir(), ".exe-os", "intercom.log");
4863
- DEBOUNCE_FILE = path18.join(SESSION_CACHE, "intercom-debounce.json");
4864
- DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4865
- BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
5467
+ init_database();
5468
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
5469
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
4866
5470
  }
4867
5471
  });
4868
5472
 
@@ -5037,6 +5641,7 @@ var init_tasks_review = __esm({
5037
5641
  init_tasks_crud();
5038
5642
  init_tmux_routing();
5039
5643
  init_session_key();
5644
+ init_state_bus();
5040
5645
  }
5041
5646
  });
5042
5647
 
@@ -5159,13 +5764,12 @@ function assertSessionScope(actionType, targetProject) {
5159
5764
  };
5160
5765
  }
5161
5766
  process.stderr.write(
5162
- `[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
5767
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
5163
5768
  `
5164
5769
  );
5165
5770
  return {
5166
- allowed: true,
5167
- // v1: warn-only, don't block
5168
- reason: "cross_session_granted",
5771
+ allowed: false,
5772
+ reason: "cross_session_denied",
5169
5773
  currentProject,
5170
5774
  targetProject,
5171
5775
  targetSession: findSessionForProject(targetProject)?.windowName
@@ -5191,8 +5795,9 @@ async function dispatchTaskToEmployee(input) {
5191
5795
  try {
5192
5796
  const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
5193
5797
  const check = assertSessionScope2("dispatch_task", input.projectName);
5194
- if (check.reason === "cross_session_granted") {
5798
+ if (check.reason === "cross_session_denied") {
5195
5799
  crossProject = true;
5800
+ return { dispatched: "skipped", crossProject: true };
5196
5801
  }
5197
5802
  } catch {
5198
5803
  }
@@ -5249,10 +5854,10 @@ var init_tasks_notify = __esm({
5249
5854
  });
5250
5855
 
5251
5856
  // src/lib/behaviors.ts
5252
- import crypto7 from "crypto";
5857
+ import crypto8 from "crypto";
5253
5858
  async function storeBehavior(opts) {
5254
5859
  const client = getClient();
5255
- const id = crypto7.randomUUID();
5860
+ const id = crypto8.randomUUID();
5256
5861
  const now = (/* @__PURE__ */ new Date()).toISOString();
5257
5862
  await client.execute({
5258
5863
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -5309,7 +5914,7 @@ __export(skill_learning_exports, {
5309
5914
  storeTrajectory: () => storeTrajectory,
5310
5915
  sweepTrajectories: () => sweepTrajectories
5311
5916
  });
5312
- import crypto8 from "crypto";
5917
+ import crypto9 from "crypto";
5313
5918
  async function extractTrajectory(taskId, agentId) {
5314
5919
  const client = getClient();
5315
5920
  const result = await client.execute({
@@ -5338,11 +5943,11 @@ async function extractTrajectory(taskId, agentId) {
5338
5943
  return signature;
5339
5944
  }
5340
5945
  function hashSignature(signature) {
5341
- return crypto8.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
5946
+ return crypto9.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
5342
5947
  }
5343
5948
  async function storeTrajectory(opts) {
5344
5949
  const client = getClient();
5345
- const id = crypto8.randomUUID();
5950
+ const id = crypto9.randomUUID();
5346
5951
  const now = (/* @__PURE__ */ new Date()).toISOString();
5347
5952
  const signatureHash = hashSignature(opts.signature);
5348
5953
  await client.execute({
@@ -5683,6 +6288,13 @@ async function updateTask(input) {
5683
6288
  await cascadeUnblock(taskId, input.baseDir, now);
5684
6289
  } catch {
5685
6290
  }
6291
+ orgBus.emit({
6292
+ type: "task_completed",
6293
+ taskId,
6294
+ employee: String(row.assigned_to),
6295
+ result: input.result ?? "",
6296
+ timestamp: now
6297
+ });
5686
6298
  if (row.parent_task_id) {
5687
6299
  try {
5688
6300
  await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
@@ -5750,6 +6362,7 @@ var init_tasks = __esm({
5750
6362
  init_database();
5751
6363
  init_config();
5752
6364
  init_notifications();
6365
+ init_state_bus();
5753
6366
  init_tasks_crud();
5754
6367
  init_tasks_review();
5755
6368
  init_tasks_crud();
@@ -5759,87 +6372,6 @@ var init_tasks = __esm({
5759
6372
  }
5760
6373
  });
5761
6374
 
5762
- // src/lib/session-kill-telemetry.ts
5763
- import crypto9 from "crypto";
5764
- var init_session_kill_telemetry = __esm({
5765
- "src/lib/session-kill-telemetry.ts"() {
5766
- "use strict";
5767
- init_database();
5768
- }
5769
- });
5770
-
5771
- // src/lib/capacity-monitor.ts
5772
- function resumeTaskTitle(agentId) {
5773
- return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
5774
- }
5775
- function buildResumeContext(agentId, openTasks) {
5776
- const taskList = openTasks.map(
5777
- (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
5778
- ).join("\n");
5779
- return [
5780
- "## Context",
5781
- "",
5782
- `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
5783
- "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
5784
- "",
5785
- `You have ${openTasks.length} open task(s). Work through them in priority order:`,
5786
- "",
5787
- taskList,
5788
- "",
5789
- "Read each task file and chain through them. Build and commit after each one."
5790
- ].join("\n");
5791
- }
5792
- async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
5793
- const client = getClient();
5794
- const now = (/* @__PURE__ */ new Date()).toISOString();
5795
- const context = buildResumeContext(agentId, openTasks);
5796
- const existing = await client.execute({
5797
- sql: `SELECT id FROM tasks
5798
- WHERE assigned_to = ?
5799
- AND title LIKE ?
5800
- AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
5801
- ORDER BY created_at DESC
5802
- LIMIT 1`,
5803
- args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
5804
- });
5805
- if (existing.rows.length > 0) {
5806
- const taskId = String(existing.rows[0].id);
5807
- await client.execute({
5808
- sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
5809
- args: [context, now, taskId]
5810
- });
5811
- return { created: false, taskId };
5812
- }
5813
- const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
5814
- const task = await createTask2({
5815
- title: resumeTaskTitle(agentId),
5816
- assignedTo: agentId,
5817
- assignedBy: "system",
5818
- projectName: projectDir.split("/").pop() ?? "unknown",
5819
- priority: "p0",
5820
- context,
5821
- baseDir: projectDir
5822
- });
5823
- return { created: true, taskId: task.id };
5824
- }
5825
- var RELAUNCH_COOLDOWN_MS, RESUME_TITLE_PREFIX, RESUME_TITLE_LIKE_PATTERN, RESUME_ACTIVE_STATUSES, CONFIRMATION_WINDOW_MS;
5826
- var init_capacity_monitor = __esm({
5827
- "src/lib/capacity-monitor.ts"() {
5828
- "use strict";
5829
- init_session_registry();
5830
- init_transport();
5831
- init_notifications();
5832
- init_database();
5833
- init_session_kill_telemetry();
5834
- init_tmux_routing();
5835
- RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
5836
- RESUME_TITLE_PREFIX = "RESUME:";
5837
- RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
5838
- RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
5839
- CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
5840
- }
5841
- });
5842
-
5843
6375
  // src/lib/messaging.ts
5844
6376
  import crypto10 from "crypto";
5845
6377
  function generateUlid() {
@@ -6040,7 +6572,7 @@ __export(consolidation_exports, {
6040
6572
  selectUnconsolidated: () => selectUnconsolidated,
6041
6573
  storeConsolidation: () => storeConsolidation
6042
6574
  });
6043
- import { randomUUID as randomUUID4 } from "crypto";
6575
+ import { randomUUID as randomUUID5 } from "crypto";
6044
6576
  async function selectUnconsolidated(client, limit = 200) {
6045
6577
  const result = await client.execute({
6046
6578
  sql: `SELECT id, agent_id, project_name, tool_name, raw_text, timestamp
@@ -6136,7 +6668,7 @@ async function consolidateCluster(cluster, model) {
6136
6668
  return textBlock?.text ?? "";
6137
6669
  }
6138
6670
  async function storeConsolidation(client, cluster, synthesisText, embedFn) {
6139
- const consolidatedId = randomUUID4();
6671
+ const consolidatedId = randomUUID5();
6140
6672
  const now = (/* @__PURE__ */ new Date()).toISOString();
6141
6673
  const rawText = `CONSOLIDATION [${cluster.dateRange}, ${cluster.projectName}]:
6142
6674
 
@@ -6161,7 +6693,7 @@ ${synthesisText}`;
6161
6693
  const linkStmts = sourceIds.map((sourceId) => ({
6162
6694
  sql: `INSERT INTO consolidations (id, consolidated_memory_id, source_memory_id, created_at)
6163
6695
  VALUES (?, ?, ?, ?)`,
6164
- args: [randomUUID4(), consolidatedId, sourceId, now]
6696
+ args: [randomUUID5(), consolidatedId, sourceId, now]
6165
6697
  }));
6166
6698
  const placeholders = sourceIds.map(() => "?").join(",");
6167
6699
  const markStmt = {
@@ -9386,7 +9918,7 @@ import { z as z29 } from "zod";
9386
9918
 
9387
9919
  // src/automation/trigger-engine.ts
9388
9920
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync10, existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
9389
- import { randomUUID as randomUUID3 } from "crypto";
9921
+ import { randomUUID as randomUUID4 } from "crypto";
9390
9922
  import path23 from "path";
9391
9923
  import os7 from "os";
9392
9924
  var TRIGGERS_PATH = path23.join(os7.homedir(), ".exe-os", "triggers.json");
@@ -9412,7 +9944,7 @@ function saveTriggers(triggers) {
9412
9944
  function createNewTrigger(input) {
9413
9945
  const triggers = loadTriggers();
9414
9946
  const trigger = {
9415
- id: randomUUID3().slice(0, 8),
9947
+ id: randomUUID4().slice(0, 8),
9416
9948
  ...input
9417
9949
  };
9418
9950
  triggers.push(trigger);
@@ -9813,6 +10345,7 @@ import { existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as
9813
10345
  import path25 from "path";
9814
10346
 
9815
10347
  // src/lib/employee-templates.ts
10348
+ init_global_procedures();
9816
10349
  var CLIENT_COO_TEMPLATE = `---
9817
10350
  role: client-coo
9818
10351
  title: Chief Operating Officer
@@ -11381,6 +11914,157 @@ Consolidated summaries stored as tier-1 (importance=9) memories.`
11381
11914
  );
11382
11915
  }
11383
11916
 
11917
+ // src/mcp/tools/store-global-procedure.ts
11918
+ init_global_procedures();
11919
+ init_active_agent();
11920
+ import { z as z41 } from "zod";
11921
+ function registerStoreGlobalProcedure(server2) {
11922
+ server2.registerTool(
11923
+ "store_global_procedure",
11924
+ {
11925
+ title: "Store Global Procedure",
11926
+ 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.",
11927
+ inputSchema: {
11928
+ title: z41.string().describe("Short title for the procedure"),
11929
+ content: z41.string().max(500).describe("The procedure content \u2014 clear, actionable instruction"),
11930
+ priority: z41.enum(["p0", "p1", "p2"]).optional().describe("Priority tier. p0 = always (default). p1 = standard. p2 = nice-to-have."),
11931
+ domain: z41.string().optional().describe("Category: workflow, code-style, communication, architecture, testing, security")
11932
+ }
11933
+ },
11934
+ async ({ title, content, priority, domain }) => {
11935
+ const caller = getActiveAgent();
11936
+ const allowed = caller.agentId === "default" || caller.agentRole === "COO";
11937
+ if (!allowed) {
11938
+ return {
11939
+ content: [{
11940
+ type: "text",
11941
+ text: `Permission denied. Only exe or founder sessions can create global procedures. You are "${caller.agentId}".`
11942
+ }],
11943
+ isError: true
11944
+ };
11945
+ }
11946
+ const id = await storeGlobalProcedure({
11947
+ title,
11948
+ content,
11949
+ priority: priority ?? "p0",
11950
+ domain: domain ?? void 0
11951
+ });
11952
+ return {
11953
+ content: [{
11954
+ type: "text",
11955
+ text: `Global procedure stored.
11956
+ ID: ${id}
11957
+ Title: ${title}
11958
+ Priority: ${priority ?? "p0"}
11959
+ Domain: ${domain ?? "none"}`
11960
+ }]
11961
+ };
11962
+ }
11963
+ );
11964
+ }
11965
+
11966
+ // src/mcp/tools/list-global-procedures.ts
11967
+ init_global_procedures();
11968
+ function registerListGlobalProcedures(server2) {
11969
+ server2.registerTool(
11970
+ "list_global_procedures",
11971
+ {
11972
+ title: "List Global Procedures",
11973
+ description: "List all active organization-wide procedures (Layer 0). These supersede identity, expertise, and experience.",
11974
+ inputSchema: {}
11975
+ },
11976
+ async () => {
11977
+ const procedures = await loadGlobalProcedures();
11978
+ if (procedures.length === 0) {
11979
+ return {
11980
+ content: [{
11981
+ type: "text",
11982
+ text: "No active global procedures."
11983
+ }]
11984
+ };
11985
+ }
11986
+ const lines = procedures.map(
11987
+ (p) => `[${p.id}] (${p.priority}) ${p.title}${p.domain ? ` [${p.domain}]` : ""}
11988
+ ${p.content}`
11989
+ );
11990
+ return {
11991
+ content: [{
11992
+ type: "text",
11993
+ text: `Active global procedures (${procedures.length}):
11994
+
11995
+ ${lines.join("\n\n")}`
11996
+ }]
11997
+ };
11998
+ }
11999
+ );
12000
+ }
12001
+
12002
+ // src/mcp/tools/deactivate-global-procedure.ts
12003
+ init_global_procedures();
12004
+ init_active_agent();
12005
+ init_database();
12006
+ import { z as z42 } from "zod";
12007
+ function registerDeactivateGlobalProcedure(server2) {
12008
+ server2.registerTool(
12009
+ "deactivate_global_procedure",
12010
+ {
12011
+ title: "Deactivate Global Procedure",
12012
+ 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.",
12013
+ inputSchema: {
12014
+ procedure_id: z42.string().describe("UUID of the global procedure to deactivate")
12015
+ }
12016
+ },
12017
+ async ({ procedure_id }) => {
12018
+ const caller = getActiveAgent();
12019
+ const allowed = caller.agentId === "default" || caller.agentRole === "COO";
12020
+ if (!allowed) {
12021
+ return {
12022
+ content: [{
12023
+ type: "text",
12024
+ text: `Permission denied. Only exe or founder sessions can deactivate global procedures. You are "${caller.agentId}".`
12025
+ }],
12026
+ isError: true
12027
+ };
12028
+ }
12029
+ const client = getClient();
12030
+ const result = await client.execute({
12031
+ sql: "SELECT id, title, content, priority, domain FROM global_procedures WHERE id = ?",
12032
+ args: [procedure_id]
12033
+ });
12034
+ if (result.rows.length === 0) {
12035
+ return {
12036
+ content: [{
12037
+ type: "text",
12038
+ text: `Global procedure not found: ${procedure_id}`
12039
+ }],
12040
+ isError: true
12041
+ };
12042
+ }
12043
+ const row = result.rows[0];
12044
+ const wasActive = await deactivateGlobalProcedure(procedure_id);
12045
+ if (!wasActive) {
12046
+ return {
12047
+ content: [{
12048
+ type: "text",
12049
+ text: `Global procedure ${procedure_id} was already inactive.`
12050
+ }]
12051
+ };
12052
+ }
12053
+ return {
12054
+ content: [{
12055
+ type: "text",
12056
+ text: `Deactivated global procedure.
12057
+ ID: ${row.id}
12058
+ Title: ${row.title}
12059
+ Priority: ${row.priority}
12060
+ Domain: ${row.domain ?? "none"}
12061
+ Content: ${row.content}`
12062
+ }]
12063
+ };
12064
+ }
12065
+ );
12066
+ }
12067
+
11384
12068
  // src/lib/telemetry.ts
11385
12069
  var ENABLED = process.env.EXE_TELEMETRY === "1";
11386
12070
  var initialized = false;
@@ -11485,6 +12169,9 @@ registerDeployClient(server);
11485
12169
  registerQueryConversations(server);
11486
12170
  registerLoadSkill(server);
11487
12171
  registerConsolidateMemories(server);
12172
+ registerStoreGlobalProcedure(server);
12173
+ registerListGlobalProcedures(server);
12174
+ registerDeactivateGlobalProcedure(server);
11488
12175
  try {
11489
12176
  await initStore();
11490
12177
  process.stderr.write("[exe-os] MCP server starting...\n");