@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
package/dist/lib/tasks.js CHANGED
@@ -284,446 +284,68 @@ var init_notifications = __esm({
284
284
  }
285
285
  });
286
286
 
287
- // src/lib/tasks-crud.ts
288
- import crypto2 from "crypto";
289
- import path3 from "path";
290
- import { execSync } from "child_process";
291
- import { mkdir as mkdir2, writeFile as writeFile2, appendFile } from "fs/promises";
292
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
293
- async function writeCheckpoint(input) {
294
- const client = getClient();
295
- const row = await resolveTask(client, input.taskId);
296
- const taskId = String(row.id);
297
- const now = (/* @__PURE__ */ new Date()).toISOString();
298
- const blockedByIds = [];
299
- if (row.blocked_by) {
300
- blockedByIds.push(String(row.blocked_by));
301
- }
302
- const checkpoint = {
303
- step: input.step,
304
- context_summary: input.contextSummary,
305
- files_touched: input.filesTouched ?? [],
306
- blocked_by_ids: blockedByIds,
307
- last_checkpoint_at: now
308
- };
309
- const result = await client.execute({
310
- sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
311
- args: [JSON.stringify(checkpoint), now, taskId]
312
- });
313
- if (result.rowsAffected === 0) {
314
- throw new Error(`Checkpoint write failed: task ${taskId} not found`);
315
- }
316
- const countResult = await client.execute({
317
- sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
318
- args: [taskId]
319
- });
320
- const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
321
- return { checkpointCount };
322
- }
323
- function extractParentFromContext(contextBody) {
324
- if (!contextBody) return null;
325
- const match = contextBody.match(
326
- /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
327
- );
328
- return match ? match[1].toLowerCase() : null;
329
- }
330
- function slugify(title) {
331
- return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
332
- }
333
- async function resolveTask(client, identifier) {
334
- let result = await client.execute({
335
- sql: "SELECT * FROM tasks WHERE id = ?",
336
- args: [identifier]
337
- });
338
- if (result.rows.length === 1) return result.rows[0];
339
- result = await client.execute({
340
- sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
341
- args: [`%${identifier}%`]
342
- });
343
- if (result.rows.length === 1) return result.rows[0];
344
- if (result.rows.length > 1) {
345
- const exact = result.rows.filter(
346
- (r) => String(r.task_file).endsWith(`/${identifier}.md`)
347
- );
348
- if (exact.length === 1) return exact[0];
349
- const candidates = exact.length > 1 ? exact : result.rows;
350
- const active = candidates.filter(
351
- (r) => !["done", "cancelled"].includes(String(r.status))
352
- );
353
- if (active.length === 1) return active[0];
354
- const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
355
- throw new Error(
356
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
357
- );
358
- }
359
- result = await client.execute({
360
- sql: "SELECT * FROM tasks WHERE title LIKE ?",
361
- args: [`%${identifier}%`]
362
- });
363
- if (result.rows.length === 1) return result.rows[0];
364
- if (result.rows.length > 1) {
365
- const active = result.rows.filter(
366
- (r) => !["done", "cancelled"].includes(String(r.status))
367
- );
368
- if (active.length === 1) return active[0];
369
- const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
370
- throw new Error(
371
- `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
372
- );
373
- }
374
- throw new Error(`Task not found: ${identifier}`);
375
- }
376
- async function createTaskCore(input) {
377
- const client = getClient();
378
- const id = crypto2.randomUUID();
379
- const now = (/* @__PURE__ */ new Date()).toISOString();
380
- const slug = slugify(input.title);
381
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
382
- let blockedById = null;
383
- const initialStatus = input.blockedBy ? "blocked" : "open";
384
- if (input.blockedBy) {
385
- const blocker = await resolveTask(client, input.blockedBy);
386
- blockedById = String(blocker.id);
387
- }
388
- let parentTaskId = null;
389
- let parentRef = input.parentTaskId;
390
- if (!parentRef) {
391
- const extracted = extractParentFromContext(input.context);
392
- if (extracted) {
393
- parentRef = extracted;
394
- process.stderr.write(
395
- "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
396
- );
397
- }
398
- }
399
- if (parentRef) {
400
- try {
401
- const parent = await resolveTask(client, parentRef);
402
- parentTaskId = String(parent.id);
403
- } catch (err) {
404
- if (!input.parentTaskId) {
405
- throw new Error(
406
- `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
407
- );
408
- }
409
- throw err;
410
- }
411
- }
412
- let warning;
413
- const dupCheck = await client.execute({
414
- sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
415
- args: [input.title, input.assignedTo]
416
- });
417
- if (dupCheck.rows.length > 0) {
418
- warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
419
- }
420
- if (input.baseDir) {
421
- try {
422
- await mkdir2(path3.join(input.baseDir, "exe", "output"), { recursive: true });
423
- await mkdir2(path3.join(input.baseDir, "exe", "research"), { recursive: true });
424
- await ensureArchitectureDoc(input.baseDir, input.projectName);
425
- await ensureGitignoreExe(input.baseDir);
426
- } catch {
427
- }
428
- }
429
- const complexity = input.complexity ?? "standard";
430
- await client.execute({
431
- 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)
432
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
433
- args: [
434
- id,
435
- input.title,
436
- input.assignedTo,
437
- input.assignedBy,
438
- input.projectName,
439
- input.priority,
440
- initialStatus,
441
- taskFile,
442
- blockedById,
443
- parentTaskId,
444
- input.reviewer ?? null,
445
- input.context,
446
- complexity,
447
- input.budgetTokens ?? null,
448
- input.budgetFallbackModel ?? null,
449
- 0,
450
- null,
451
- now,
452
- now
453
- ]
454
- });
455
- return {
456
- id,
457
- title: input.title,
458
- assignedTo: input.assignedTo,
459
- assignedBy: input.assignedBy,
460
- projectName: input.projectName,
461
- priority: input.priority,
462
- status: initialStatus,
463
- taskFile,
464
- createdAt: now,
465
- updatedAt: now,
466
- warning,
467
- budgetTokens: input.budgetTokens ?? null,
468
- budgetFallbackModel: input.budgetFallbackModel ?? null,
469
- tokensUsed: 0,
470
- tokensWarnedAt: null
471
- };
472
- }
473
- async function listTasks(input) {
474
- const client = getClient();
475
- const conditions = [];
476
- const args = [];
477
- if (input.assignedTo) {
478
- conditions.push("assigned_to = ?");
479
- args.push(input.assignedTo);
480
- }
481
- if (input.status) {
482
- conditions.push("status = ?");
483
- args.push(input.status);
484
- } else {
485
- conditions.push("status IN ('open', 'in_progress', 'blocked')");
486
- }
487
- if (input.projectName) {
488
- conditions.push("project_name = ?");
489
- args.push(input.projectName);
490
- }
491
- if (input.priority) {
492
- conditions.push("priority = ?");
493
- args.push(input.priority);
494
- }
495
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
496
- const result = await client.execute({
497
- 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`,
498
- args
499
- });
500
- return result.rows.map((r) => ({
501
- id: String(r.id),
502
- title: String(r.title),
503
- assignedTo: String(r.assigned_to),
504
- assignedBy: String(r.assigned_by),
505
- projectName: String(r.project_name),
506
- priority: String(r.priority),
507
- status: String(r.status),
508
- taskFile: String(r.task_file),
509
- createdAt: String(r.created_at),
510
- updatedAt: String(r.updated_at),
511
- checkpointCount: Number(r.checkpoint_count ?? 0),
512
- budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
513
- budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
514
- tokensUsed: Number(r.tokens_used ?? 0),
515
- tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
516
- }));
517
- }
518
- function checkStaleCompletion(taskContext, taskCreatedAt) {
519
- if (!taskContext) return null;
520
- if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
521
- try {
522
- const since = new Date(taskCreatedAt).toISOString();
523
- const branch = execSync(
524
- "git rev-parse --abbrev-ref HEAD 2>/dev/null",
525
- { encoding: "utf8", timeout: 3e3 }
526
- ).trim();
527
- const branchArg = branch && branch !== "HEAD" ? branch : "";
528
- const commitCount = execSync(
529
- `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
530
- { encoding: "utf8", timeout: 5e3 }
531
- ).trim();
532
- const count = parseInt(commitCount, 10);
533
- if (count === 0) {
534
- return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
535
- }
536
- return null;
537
- } catch {
538
- return null;
539
- }
540
- }
541
- async function updateTaskStatus(input) {
542
- const client = getClient();
543
- const now = (/* @__PURE__ */ new Date()).toISOString();
544
- const row = await resolveTask(client, input.taskId);
545
- const taskId = String(row.id);
546
- const taskFile = String(row.task_file);
547
- if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
548
- process.stderr.write(
549
- `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
550
- `
551
- );
552
- }
553
- if (input.status === "done") {
554
- const existingRow = await client.execute({
555
- sql: "SELECT context, created_at FROM tasks WHERE id = ?",
556
- args: [taskId]
557
- });
558
- if (existingRow.rows.length > 0) {
559
- const ctx = existingRow.rows[0];
560
- const warning = checkStaleCompletion(ctx.context, ctx.created_at);
561
- if (warning) {
562
- input.result = input.result ? `\u26A0\uFE0F ${warning}
563
-
564
- ${input.result}` : `\u26A0\uFE0F ${warning}`;
565
- process.stderr.write(`[tasks] ${warning} (task: ${taskId})
566
- `);
567
- }
568
- }
569
- }
570
- if (input.status === "in_progress") {
571
- const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
572
- const claim = await client.execute({
573
- sql: `UPDATE tasks
574
- SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
575
- WHERE id = ? AND status = 'open'`,
576
- args: [tmuxSession, now, taskId]
577
- });
578
- if (claim.rowsAffected === 0) {
579
- const current = await client.execute({
580
- sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
581
- args: [taskId]
582
- });
583
- const cur = current.rows[0];
584
- const status = cur?.status ?? "unknown";
585
- const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
586
- throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
587
- }
588
- try {
589
- await writeCheckpoint({
590
- taskId,
591
- step: "claimed",
592
- contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
593
- });
594
- } catch {
595
- }
596
- return { row, taskFile, now, taskId };
597
- }
598
- if (input.result) {
599
- await client.execute({
600
- sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
601
- args: [input.status, input.result, now, taskId]
602
- });
603
- } else {
604
- await client.execute({
605
- sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
606
- args: [input.status, now, taskId]
607
- });
608
- }
609
- try {
610
- await writeCheckpoint({
611
- taskId,
612
- step: `status_transition:${input.status}`,
613
- contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
614
- });
615
- } catch {
616
- }
617
- return { row, taskFile, now, taskId };
618
- }
619
- async function deleteTaskCore(taskId, _baseDir) {
620
- const client = getClient();
621
- const row = await resolveTask(client, taskId);
622
- const id = String(row.id);
623
- const taskFile = String(row.task_file);
624
- const assignedTo = String(row.assigned_to);
625
- const assignedBy = String(row.assigned_by);
626
- await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
627
- const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
628
- return { taskFile, assignedTo, assignedBy, taskSlug };
629
- }
630
- async function ensureArchitectureDoc(baseDir, projectName) {
631
- const archPath = path3.join(baseDir, "exe", "ARCHITECTURE.md");
632
- try {
633
- if (existsSync3(archPath)) return;
634
- const template = [
635
- `# ${projectName} \u2014 System Architecture`,
636
- "",
637
- "> Employees: read this before every task. Update it when you change system structure.",
638
- `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
639
- "",
640
- "## Overview",
641
- "",
642
- "<!-- Describe what this system does, its main components, and how they connect. -->",
643
- "",
644
- "## Key Components",
645
- "",
646
- "<!-- List the major modules, services, or subsystems. -->",
647
- "",
648
- "## Data Flow",
649
- "",
650
- "<!-- How does data move through the system? What writes where? -->",
651
- "",
652
- "## Invariants",
653
- "",
654
- "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
655
- "",
656
- "## Dependencies",
657
- "",
658
- "<!-- What depends on what? If I change X, what else is affected? -->",
659
- ""
660
- ].join("\n");
661
- await writeFile2(archPath, template, "utf-8");
662
- } catch {
663
- }
664
- }
665
- async function ensureGitignoreExe(baseDir) {
666
- const gitignorePath = path3.join(baseDir, ".gitignore");
667
- try {
668
- if (existsSync3(gitignorePath)) {
669
- const content = readFileSync3(gitignorePath, "utf-8");
670
- if (/^\/?exe\/?$/m.test(content)) return;
671
- await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
672
- } else {
673
- await writeFile2(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
674
- }
675
- } catch {
676
- }
677
- }
678
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
679
- var init_tasks_crud = __esm({
680
- "src/lib/tasks-crud.ts"() {
681
- "use strict";
682
- init_database();
683
- DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
684
- TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
685
- }
686
- });
687
-
688
- // src/lib/employees.ts
689
- import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
690
- import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4 } from "fs";
691
- import { execSync as execSync2 } from "child_process";
692
- import path4 from "path";
693
- function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
694
- if (!existsSync4(employeesPath)) return [];
695
- try {
696
- return JSON.parse(readFileSync4(employeesPath, "utf-8"));
697
- } catch {
698
- return [];
699
- }
700
- }
701
- function getEmployee(employees, name) {
702
- return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
703
- }
704
- function isMultiInstance(agentName, employees) {
705
- const roster = employees ?? loadEmployeesSync();
706
- const emp = getEmployee(roster, agentName);
707
- if (!emp) return false;
708
- return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
709
- }
710
- var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
711
- var init_employees = __esm({
712
- "src/lib/employees.ts"() {
287
+ // src/lib/state-bus.ts
288
+ var StateBus, orgBus;
289
+ var init_state_bus = __esm({
290
+ "src/lib/state-bus.ts"() {
713
291
  "use strict";
714
- init_config();
715
- EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
716
- MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
292
+ StateBus = class {
293
+ handlers = /* @__PURE__ */ new Map();
294
+ globalHandlers = /* @__PURE__ */ new Set();
295
+ /** Emit an event to all subscribers */
296
+ emit(event) {
297
+ const typeHandlers = this.handlers.get(event.type);
298
+ if (typeHandlers) {
299
+ for (const handler of typeHandlers) {
300
+ try {
301
+ handler(event);
302
+ } catch {
303
+ }
304
+ }
305
+ }
306
+ for (const handler of this.globalHandlers) {
307
+ try {
308
+ handler(event);
309
+ } catch {
310
+ }
311
+ }
312
+ }
313
+ /** Subscribe to a specific event type */
314
+ on(type, handler) {
315
+ if (!this.handlers.has(type)) {
316
+ this.handlers.set(type, /* @__PURE__ */ new Set());
317
+ }
318
+ this.handlers.get(type).add(handler);
319
+ }
320
+ /** Subscribe to ALL events */
321
+ onAny(handler) {
322
+ this.globalHandlers.add(handler);
323
+ }
324
+ /** Unsubscribe from a specific event type */
325
+ off(type, handler) {
326
+ this.handlers.get(type)?.delete(handler);
327
+ }
328
+ /** Unsubscribe from ALL events */
329
+ offAny(handler) {
330
+ this.globalHandlers.delete(handler);
331
+ }
332
+ /** Remove all listeners */
333
+ clear() {
334
+ this.handlers.clear();
335
+ this.globalHandlers.clear();
336
+ }
337
+ };
338
+ orgBus = new StateBus();
717
339
  }
718
340
  });
719
341
 
720
342
  // src/lib/session-registry.ts
721
- import { readFileSync as readFileSync5, writeFileSync, mkdirSync, existsSync as existsSync5 } from "fs";
722
- import path5 from "path";
343
+ import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync as existsSync3 } from "fs";
344
+ import path3 from "path";
723
345
  import os3 from "os";
724
346
  function registerSession(entry) {
725
- const dir = path5.dirname(REGISTRY_PATH);
726
- if (!existsSync5(dir)) {
347
+ const dir = path3.dirname(REGISTRY_PATH);
348
+ if (!existsSync3(dir)) {
727
349
  mkdirSync(dir, { recursive: true });
728
350
  }
729
351
  const sessions = listSessions();
@@ -737,7 +359,7 @@ function registerSession(entry) {
737
359
  }
738
360
  function listSessions() {
739
361
  try {
740
- const raw = readFileSync5(REGISTRY_PATH, "utf8");
362
+ const raw = readFileSync3(REGISTRY_PATH, "utf8");
741
363
  return JSON.parse(raw);
742
364
  } catch {
743
365
  return [];
@@ -747,18 +369,18 @@ var REGISTRY_PATH;
747
369
  var init_session_registry = __esm({
748
370
  "src/lib/session-registry.ts"() {
749
371
  "use strict";
750
- REGISTRY_PATH = path5.join(os3.homedir(), ".exe-os", "session-registry.json");
372
+ REGISTRY_PATH = path3.join(os3.homedir(), ".exe-os", "session-registry.json");
751
373
  }
752
374
  });
753
375
 
754
376
  // src/lib/session-key.ts
755
- import { execSync as execSync3 } from "child_process";
377
+ import { execSync } from "child_process";
756
378
  function getSessionKey() {
757
379
  if (_cached) return _cached;
758
380
  let pid = process.ppid;
759
381
  for (let i = 0; i < 10; i++) {
760
382
  try {
761
- const info = execSync3(`ps -p ${pid} -o ppid=,comm=`, {
383
+ const info = execSync(`ps -p ${pid} -o ppid=,comm=`, {
762
384
  encoding: "utf8",
763
385
  timeout: 2e3
764
386
  }).trim();
@@ -877,782 +499,1654 @@ var init_tmux_transport = __esm({
877
499
  }
878
500
  });
879
501
 
880
- // src/lib/transport.ts
881
- function getTransport() {
882
- if (!_transport) {
883
- const { TmuxTransport: TmuxTransport2 } = (init_tmux_transport(), __toCommonJS(tmux_transport_exports));
884
- _transport = new TmuxTransport2();
502
+ // src/lib/transport.ts
503
+ function getTransport() {
504
+ if (!_transport) {
505
+ const { TmuxTransport: TmuxTransport2 } = (init_tmux_transport(), __toCommonJS(tmux_transport_exports));
506
+ _transport = new TmuxTransport2();
507
+ }
508
+ return _transport;
509
+ }
510
+ var _transport;
511
+ var init_transport = __esm({
512
+ "src/lib/transport.ts"() {
513
+ "use strict";
514
+ _transport = null;
515
+ }
516
+ });
517
+
518
+ // src/lib/cc-agent-support.ts
519
+ import { execSync as execSync2 } from "child_process";
520
+ function _resetCcAgentSupportCache() {
521
+ _cachedSupport = null;
522
+ }
523
+ function claudeSupportsAgentFlag() {
524
+ if (_cachedSupport !== null) return _cachedSupport;
525
+ try {
526
+ const helpOutput = execSync2("claude --help 2>&1", {
527
+ encoding: "utf-8",
528
+ timeout: 5e3
529
+ });
530
+ _cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
531
+ } catch {
532
+ _cachedSupport = false;
533
+ }
534
+ return _cachedSupport;
535
+ }
536
+ var _cachedSupport;
537
+ var init_cc_agent_support = __esm({
538
+ "src/lib/cc-agent-support.ts"() {
539
+ "use strict";
540
+ _cachedSupport = null;
541
+ }
542
+ });
543
+
544
+ // src/lib/mcp-prefix.ts
545
+ function expandDualPrefixTools(shortNames) {
546
+ const out = [];
547
+ for (const name of shortNames) {
548
+ for (const prefix of MCP_TOOL_PREFIXES) {
549
+ out.push(prefix + name);
550
+ }
551
+ }
552
+ return out;
553
+ }
554
+ var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
555
+ var init_mcp_prefix = __esm({
556
+ "src/lib/mcp-prefix.ts"() {
557
+ "use strict";
558
+ MCP_PRIMARY_KEY = "exe-os";
559
+ MCP_LEGACY_KEY = "exe-mem";
560
+ MCP_TOOL_PREFIXES = [
561
+ `mcp__${MCP_PRIMARY_KEY}__`,
562
+ `mcp__${MCP_LEGACY_KEY}__`
563
+ ];
564
+ }
565
+ });
566
+
567
+ // src/lib/provider-table.ts
568
+ function detectActiveProvider(env = process.env) {
569
+ const baseUrl = env.ANTHROPIC_BASE_URL;
570
+ if (!baseUrl) return DEFAULT_PROVIDER;
571
+ for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
572
+ if (cfg.baseUrl === baseUrl) return name;
573
+ }
574
+ return DEFAULT_PROVIDER;
575
+ }
576
+ var PROVIDER_TABLE, DEFAULT_PROVIDER;
577
+ var init_provider_table = __esm({
578
+ "src/lib/provider-table.ts"() {
579
+ "use strict";
580
+ PROVIDER_TABLE = {
581
+ opencode: {
582
+ baseUrl: "https://opencode.ai/zen/go",
583
+ apiKeyEnv: "OPENCODE_API_KEY",
584
+ defaultModel: "minimax-m2.7"
585
+ }
586
+ };
587
+ DEFAULT_PROVIDER = "default";
588
+ }
589
+ });
590
+
591
+ // src/lib/intercom-queue.ts
592
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
593
+ import path4 from "path";
594
+ import os4 from "os";
595
+ function ensureDir() {
596
+ const dir = path4.dirname(QUEUE_PATH);
597
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
598
+ }
599
+ function readQueue() {
600
+ try {
601
+ if (!existsSync4(QUEUE_PATH)) return [];
602
+ return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
603
+ } catch {
604
+ return [];
605
+ }
606
+ }
607
+ function writeQueue(queue) {
608
+ ensureDir();
609
+ const tmp = `${QUEUE_PATH}.tmp`;
610
+ writeFileSync2(tmp, JSON.stringify(queue, null, 2));
611
+ renameSync2(tmp, QUEUE_PATH);
612
+ }
613
+ function queueIntercom(targetSession, reason) {
614
+ const queue = readQueue();
615
+ const existing = queue.find((q) => q.targetSession === targetSession);
616
+ if (existing) {
617
+ existing.attempts++;
618
+ existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
619
+ existing.reason = reason;
620
+ } else {
621
+ queue.push({
622
+ targetSession,
623
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
624
+ attempts: 0,
625
+ reason
626
+ });
627
+ }
628
+ writeQueue(queue);
629
+ }
630
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
631
+ var init_intercom_queue = __esm({
632
+ "src/lib/intercom-queue.ts"() {
633
+ "use strict";
634
+ QUEUE_PATH = path4.join(os4.homedir(), ".exe-os", "intercom-queue.json");
635
+ TTL_MS = 60 * 60 * 1e3;
636
+ INTERCOM_LOG = path4.join(os4.homedir(), ".exe-os", "intercom.log");
637
+ }
638
+ });
639
+
640
+ // src/lib/employees.ts
641
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
642
+ import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5 } from "fs";
643
+ import { execSync as execSync3 } from "child_process";
644
+ import path5 from "path";
645
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
646
+ if (!existsSync5(employeesPath)) return [];
647
+ try {
648
+ return JSON.parse(readFileSync5(employeesPath, "utf-8"));
649
+ } catch {
650
+ return [];
651
+ }
652
+ }
653
+ function getEmployee(employees, name) {
654
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
655
+ }
656
+ function isMultiInstance(agentName, employees) {
657
+ const roster = employees ?? loadEmployeesSync();
658
+ const emp = getEmployee(roster, agentName);
659
+ if (!emp) return false;
660
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
661
+ }
662
+ var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
663
+ var init_employees = __esm({
664
+ "src/lib/employees.ts"() {
665
+ "use strict";
666
+ init_config();
667
+ EMPLOYEES_PATH = path5.join(EXE_AI_DIR, "exe-employees.json");
668
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
669
+ }
670
+ });
671
+
672
+ // src/lib/license.ts
673
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
674
+ import { randomUUID } from "crypto";
675
+ import path6 from "path";
676
+ import { jwtVerify, importSPKI } from "jose";
677
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
678
+ var init_license = __esm({
679
+ "src/lib/license.ts"() {
680
+ "use strict";
681
+ init_config();
682
+ LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
683
+ CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
684
+ DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
685
+ PLAN_LIMITS = {
686
+ free: { devices: 1, employees: 1, memories: 5e3 },
687
+ pro: { devices: 2, employees: 5, memories: 1e5 },
688
+ team: { devices: 10, employees: 20, memories: 1e6 },
689
+ agency: { devices: 50, employees: 100, memories: 1e7 },
690
+ enterprise: { devices: -1, employees: -1, memories: -1 }
691
+ };
692
+ }
693
+ });
694
+
695
+ // src/lib/plan-limits.ts
696
+ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
697
+ import path7 from "path";
698
+ function getLicenseSync() {
699
+ try {
700
+ if (!existsSync7(CACHE_PATH2)) return freeLicense();
701
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
702
+ if (!raw.token || typeof raw.token !== "string") return freeLicense();
703
+ const parts = raw.token.split(".");
704
+ if (parts.length !== 3) return freeLicense();
705
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
706
+ const plan = payload.plan ?? "free";
707
+ const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
708
+ return {
709
+ valid: true,
710
+ plan,
711
+ email: payload.sub ?? "",
712
+ expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
713
+ deviceLimit: limits.devices,
714
+ employeeLimit: limits.employees,
715
+ memoryLimit: limits.memories
716
+ };
717
+ } catch {
718
+ return freeLicense();
719
+ }
720
+ }
721
+ function freeLicense() {
722
+ const limits = PLAN_LIMITS.free;
723
+ return {
724
+ valid: true,
725
+ plan: "free",
726
+ email: "",
727
+ expiresAt: null,
728
+ deviceLimit: limits.devices,
729
+ employeeLimit: limits.employees,
730
+ memoryLimit: limits.memories
731
+ };
732
+ }
733
+ function assertEmployeeLimitSync(rosterPath) {
734
+ const license = getLicenseSync();
735
+ if (license.employeeLimit < 0) return;
736
+ const filePath = rosterPath ?? EMPLOYEES_PATH;
737
+ let count = 0;
738
+ try {
739
+ if (existsSync7(filePath)) {
740
+ const raw = readFileSync7(filePath, "utf8");
741
+ const employees = JSON.parse(raw);
742
+ count = Array.isArray(employees) ? employees.length : 0;
743
+ }
744
+ } catch {
745
+ throw new PlanLimitError(
746
+ `Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
747
+ );
748
+ }
749
+ if (count >= license.employeeLimit) {
750
+ throw new PlanLimitError(
751
+ `Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
752
+ );
753
+ }
754
+ }
755
+ var PlanLimitError, CACHE_PATH2;
756
+ var init_plan_limits = __esm({
757
+ "src/lib/plan-limits.ts"() {
758
+ "use strict";
759
+ init_database();
760
+ init_employees();
761
+ init_license();
762
+ init_config();
763
+ PlanLimitError = class extends Error {
764
+ constructor(message) {
765
+ super(message);
766
+ this.name = "PlanLimitError";
767
+ }
768
+ };
769
+ CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
770
+ }
771
+ });
772
+
773
+ // src/lib/session-kill-telemetry.ts
774
+ import crypto2 from "crypto";
775
+ async function recordSessionKill(input) {
776
+ try {
777
+ const client = getClient();
778
+ await client.execute({
779
+ sql: `INSERT INTO session_kills
780
+ (id, session_name, agent_id, killed_at, reason,
781
+ ticks_idle, estimated_tokens_saved)
782
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
783
+ args: [
784
+ crypto2.randomUUID(),
785
+ input.sessionName,
786
+ input.agentId,
787
+ (/* @__PURE__ */ new Date()).toISOString(),
788
+ input.reason,
789
+ input.ticksIdle ?? null,
790
+ input.estimatedTokensSaved ?? null
791
+ ]
792
+ });
793
+ } catch (err) {
794
+ process.stderr.write(
795
+ `[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
796
+ `
797
+ );
885
798
  }
886
- return _transport;
887
799
  }
888
- var _transport;
889
- var init_transport = __esm({
890
- "src/lib/transport.ts"() {
800
+ var init_session_kill_telemetry = __esm({
801
+ "src/lib/session-kill-telemetry.ts"() {
891
802
  "use strict";
892
- _transport = null;
803
+ init_database();
893
804
  }
894
805
  });
895
806
 
896
- // src/lib/cc-agent-support.ts
897
- import { execSync as execSync4 } from "child_process";
898
- function _resetCcAgentSupportCache() {
899
- _cachedSupport = null;
807
+ // src/lib/capacity-monitor.ts
808
+ var capacity_monitor_exports = {};
809
+ __export(capacity_monitor_exports, {
810
+ CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
811
+ _resetLastRelaunchCache: () => _resetLastRelaunchCache,
812
+ _resetPendingCapacityKills: () => _resetPendingCapacityKills,
813
+ confirmCapacityKill: () => confirmCapacityKill,
814
+ createOrRefreshResumeTask: () => createOrRefreshResumeTask,
815
+ extractContextPercent: () => extractContextPercent,
816
+ isAtCapacity: () => isAtCapacity,
817
+ isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
818
+ pollCapacityDead: () => pollCapacityDead
819
+ });
820
+ function resumeTaskTitle(agentId) {
821
+ return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
822
+ }
823
+ function buildResumeContext(agentId, openTasks) {
824
+ const taskList = openTasks.map(
825
+ (r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
826
+ ).join("\n");
827
+ return [
828
+ "## Context",
829
+ "",
830
+ `${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
831
+ "Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
832
+ "",
833
+ `You have ${openTasks.length} open task(s). Work through them in priority order:`,
834
+ "",
835
+ taskList,
836
+ "",
837
+ "Read each task file and chain through them. Build and commit after each one."
838
+ ].join("\n");
839
+ }
840
+ function filterPaneContent(paneOutput) {
841
+ return paneOutput.split("\n").filter((line) => {
842
+ if (CONTENT_LINE_PREFIX.test(line)) return false;
843
+ for (const marker of CONTENT_LINE_MARKERS) {
844
+ if (line.includes(marker)) return false;
845
+ }
846
+ for (const re of SOURCE_CODE_MARKERS) {
847
+ if (re.test(line)) return false;
848
+ }
849
+ return true;
850
+ }).join("\n");
851
+ }
852
+ function extractContextPercent(paneOutput) {
853
+ const match = paneOutput.match(CC_CONTEXT_BAR_RE);
854
+ if (!match) return null;
855
+ const parsed = Number.parseInt(match[2], 10);
856
+ return Number.isFinite(parsed) ? parsed : null;
857
+ }
858
+ function isAtCapacity(paneOutput) {
859
+ const filtered = filterPaneContent(paneOutput);
860
+ return CAPACITY_PATTERNS.some((p) => p.test(filtered));
861
+ }
862
+ function confirmCapacityKill(agentId, now = Date.now()) {
863
+ const pendingSince = _pendingCapacityKill.get(agentId);
864
+ if (pendingSince === void 0) {
865
+ _pendingCapacityKill.set(agentId, now);
866
+ return false;
867
+ }
868
+ if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
869
+ _pendingCapacityKill.set(agentId, now);
870
+ return false;
871
+ }
872
+ _pendingCapacityKill.delete(agentId);
873
+ return true;
900
874
  }
901
- function claudeSupportsAgentFlag() {
902
- if (_cachedSupport !== null) return _cachedSupport;
903
- try {
904
- const helpOutput = execSync4("claude --help 2>&1", {
905
- encoding: "utf-8",
906
- timeout: 5e3
875
+ function _resetPendingCapacityKills() {
876
+ _pendingCapacityKill.clear();
877
+ }
878
+ function _resetLastRelaunchCache() {
879
+ _lastRelaunch.clear();
880
+ }
881
+ async function lastResumeCreatedAtMs(agentId) {
882
+ const client = getClient();
883
+ const result = await client.execute({
884
+ sql: `SELECT MAX(created_at) AS last_created_at
885
+ FROM tasks
886
+ WHERE assigned_to = ? AND title LIKE ?`,
887
+ args: [agentId, `${RESUME_TITLE_PREFIX} %`]
888
+ });
889
+ const raw = result.rows[0]?.last_created_at;
890
+ if (raw === null || raw === void 0) return null;
891
+ const parsed = Date.parse(String(raw));
892
+ return Number.isNaN(parsed) ? null : parsed;
893
+ }
894
+ async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
895
+ const cached = _lastRelaunch.get(agentId);
896
+ if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
897
+ const persisted = await lastResumeCreatedAtMs(agentId);
898
+ if (persisted === null) return false;
899
+ if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
900
+ _lastRelaunch.set(agentId, persisted);
901
+ return true;
902
+ }
903
+ async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
904
+ const client = getClient();
905
+ const now = (/* @__PURE__ */ new Date()).toISOString();
906
+ const context = buildResumeContext(agentId, openTasks);
907
+ const existing = await client.execute({
908
+ sql: `SELECT id FROM tasks
909
+ WHERE assigned_to = ?
910
+ AND title LIKE ?
911
+ AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
912
+ ORDER BY created_at DESC
913
+ LIMIT 1`,
914
+ args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
915
+ });
916
+ if (existing.rows.length > 0) {
917
+ const taskId = String(existing.rows[0].id);
918
+ await client.execute({
919
+ sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
920
+ args: [context, now, taskId]
907
921
  });
908
- _cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
922
+ return { created: false, taskId };
923
+ }
924
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
925
+ const task = await createTask2({
926
+ title: resumeTaskTitle(agentId),
927
+ assignedTo: agentId,
928
+ assignedBy: "system",
929
+ projectName: projectDir.split("/").pop() ?? "unknown",
930
+ priority: "p0",
931
+ context,
932
+ baseDir: projectDir
933
+ });
934
+ return { created: true, taskId: task.id };
935
+ }
936
+ async function pollCapacityDead() {
937
+ const transport = getTransport();
938
+ const relaunched = [];
939
+ const registered = listSessions().filter(
940
+ (s) => s.agentId !== "exe"
941
+ );
942
+ if (registered.length === 0) return [];
943
+ let liveSessions;
944
+ try {
945
+ liveSessions = transport.listSessions();
909
946
  } catch {
910
- _cachedSupport = false;
947
+ return [];
911
948
  }
912
- return _cachedSupport;
949
+ for (const entry of registered) {
950
+ const { windowName, agentId, projectDir } = entry;
951
+ if (!liveSessions.includes(windowName)) continue;
952
+ if (await isWithinRelaunchCooldown(agentId)) continue;
953
+ let pane;
954
+ try {
955
+ pane = transport.capturePane(windowName, 15);
956
+ } catch {
957
+ continue;
958
+ }
959
+ if (!isAtCapacity(pane)) continue;
960
+ const ctxPct = extractContextPercent(pane);
961
+ if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
962
+ process.stderr.write(
963
+ `[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
964
+ `
965
+ );
966
+ continue;
967
+ }
968
+ if (!confirmCapacityKill(agentId)) {
969
+ process.stderr.write(
970
+ `[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
971
+ `
972
+ );
973
+ continue;
974
+ }
975
+ const verify = await verifyPaneAtCapacity(windowName);
976
+ if (!verify.atCapacity) {
977
+ process.stderr.write(
978
+ `[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
979
+ `
980
+ );
981
+ void recordSessionKill({
982
+ sessionName: windowName,
983
+ agentId,
984
+ reason: "capacity_false_positive_blocked"
985
+ });
986
+ continue;
987
+ }
988
+ process.stderr.write(
989
+ `[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
990
+ `
991
+ );
992
+ try {
993
+ transport.kill(windowName);
994
+ void recordSessionKill({
995
+ sessionName: windowName,
996
+ agentId,
997
+ reason: "capacity"
998
+ });
999
+ const client = getClient();
1000
+ const openTasks = await client.execute({
1001
+ sql: `SELECT id, title, priority, task_file, status
1002
+ FROM tasks
1003
+ WHERE assigned_to = ? AND status IN ('open', 'in_progress')
1004
+ ORDER BY
1005
+ CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
1006
+ created_at ASC
1007
+ LIMIT 10`,
1008
+ args: [agentId]
1009
+ });
1010
+ if (openTasks.rows.length === 0) {
1011
+ process.stderr.write(
1012
+ `[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
1013
+ `
1014
+ );
1015
+ continue;
1016
+ }
1017
+ const { created } = await createOrRefreshResumeTask(
1018
+ agentId,
1019
+ projectDir,
1020
+ openTasks.rows
1021
+ );
1022
+ if (created) {
1023
+ await writeNotification({
1024
+ agentId: "system",
1025
+ agentRole: "daemon",
1026
+ event: "capacity_relaunch",
1027
+ project: projectDir.split("/").pop() ?? "unknown",
1028
+ summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
1029
+ });
1030
+ }
1031
+ _lastRelaunch.set(agentId, Date.now());
1032
+ if (created) relaunched.push(agentId);
1033
+ } catch (err) {
1034
+ process.stderr.write(
1035
+ `[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
1036
+ `
1037
+ );
1038
+ }
1039
+ }
1040
+ return relaunched;
913
1041
  }
914
- var _cachedSupport;
915
- var init_cc_agent_support = __esm({
916
- "src/lib/cc-agent-support.ts"() {
1042
+ 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;
1043
+ var init_capacity_monitor = __esm({
1044
+ "src/lib/capacity-monitor.ts"() {
917
1045
  "use strict";
918
- _cachedSupport = null;
1046
+ init_session_registry();
1047
+ init_transport();
1048
+ init_notifications();
1049
+ init_database();
1050
+ init_session_kill_telemetry();
1051
+ init_tmux_routing();
1052
+ CAPACITY_PATTERNS = [
1053
+ /conversation is too long/i,
1054
+ /maximum context length/i,
1055
+ /context window.*(?:limit|exceed|full)/i,
1056
+ /reached.*(?:token|context).*limit/i
1057
+ ];
1058
+ CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
1059
+ CONTENT_LINE_MARKERS = [
1060
+ "RESUME:",
1061
+ "intercom",
1062
+ "capacity-monitor",
1063
+ "CAPACITY_PATTERNS",
1064
+ "isAtCapacity",
1065
+ "CONTENT_LINE_MARKERS",
1066
+ "pollCapacityDead",
1067
+ "confirmCapacityKill",
1068
+ "session_kills",
1069
+ "capacity-monitor.test"
1070
+ ];
1071
+ SOURCE_CODE_MARKERS = [
1072
+ /["'`/].*(?:maximum context length|conversation is too long)/i,
1073
+ /(?:maximum context length|conversation is too long).*["'`/]/i
1074
+ ];
1075
+ RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
1076
+ _lastRelaunch = /* @__PURE__ */ new Map();
1077
+ RESUME_TITLE_PREFIX = "RESUME:";
1078
+ RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
1079
+ RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
1080
+ CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
1081
+ _pendingCapacityKill = /* @__PURE__ */ new Map();
1082
+ CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
1083
+ CTX_FLOOR_PERCENT = 50;
919
1084
  }
920
1085
  });
921
1086
 
922
- // src/lib/mcp-prefix.ts
923
- function expandDualPrefixTools(shortNames) {
924
- const out = [];
925
- for (const name of shortNames) {
926
- for (const prefix of MCP_TOOL_PREFIXES) {
927
- out.push(prefix + name);
1087
+ // src/lib/tmux-routing.ts
1088
+ var tmux_routing_exports = {};
1089
+ __export(tmux_routing_exports, {
1090
+ acquireSpawnLock: () => acquireSpawnLock,
1091
+ employeeSessionName: () => employeeSessionName,
1092
+ ensureEmployee: () => ensureEmployee,
1093
+ extractRootExe: () => extractRootExe,
1094
+ findFreeInstance: () => findFreeInstance,
1095
+ getDispatchedBy: () => getDispatchedBy,
1096
+ getMySession: () => getMySession,
1097
+ getParentExe: () => getParentExe,
1098
+ getSessionState: () => getSessionState,
1099
+ isEmployeeAlive: () => isEmployeeAlive,
1100
+ isExeSession: () => isExeSession,
1101
+ isSessionBusy: () => isSessionBusy,
1102
+ notifyParentExe: () => notifyParentExe,
1103
+ parseParentExe: () => parseParentExe,
1104
+ registerParentExe: () => registerParentExe,
1105
+ releaseSpawnLock: () => releaseSpawnLock,
1106
+ resolveExeSession: () => resolveExeSession,
1107
+ sendIntercom: () => sendIntercom,
1108
+ spawnEmployee: () => spawnEmployee,
1109
+ verifyPaneAtCapacity: () => verifyPaneAtCapacity
1110
+ });
1111
+ import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1112
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync8, appendFileSync } from "fs";
1113
+ import path8 from "path";
1114
+ import os5 from "os";
1115
+ import { fileURLToPath } from "url";
1116
+ import { unlinkSync as unlinkSync2 } from "fs";
1117
+ function spawnLockPath(sessionName) {
1118
+ return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1119
+ }
1120
+ function isProcessAlive(pid) {
1121
+ try {
1122
+ process.kill(pid, 0);
1123
+ return true;
1124
+ } catch {
1125
+ return false;
1126
+ }
1127
+ }
1128
+ function acquireSpawnLock(sessionName) {
1129
+ if (!existsSync8(SPAWN_LOCK_DIR)) {
1130
+ mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
1131
+ }
1132
+ const lockFile = spawnLockPath(sessionName);
1133
+ if (existsSync8(lockFile)) {
1134
+ try {
1135
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1136
+ const age = Date.now() - lock.timestamp;
1137
+ if (isProcessAlive(lock.pid) && age < 6e4) {
1138
+ return false;
1139
+ }
1140
+ } catch {
928
1141
  }
929
1142
  }
930
- return out;
1143
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1144
+ return true;
931
1145
  }
932
- var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
933
- var init_mcp_prefix = __esm({
934
- "src/lib/mcp-prefix.ts"() {
935
- "use strict";
936
- MCP_PRIMARY_KEY = "exe-os";
937
- MCP_LEGACY_KEY = "exe-mem";
938
- MCP_TOOL_PREFIXES = [
939
- `mcp__${MCP_PRIMARY_KEY}__`,
940
- `mcp__${MCP_LEGACY_KEY}__`
941
- ];
1146
+ function releaseSpawnLock(sessionName) {
1147
+ try {
1148
+ unlinkSync2(spawnLockPath(sessionName));
1149
+ } catch {
942
1150
  }
943
- });
944
-
945
- // src/lib/provider-table.ts
946
- function detectActiveProvider(env = process.env) {
947
- const baseUrl = env.ANTHROPIC_BASE_URL;
948
- if (!baseUrl) return DEFAULT_PROVIDER;
949
- for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
950
- if (cfg.baseUrl === baseUrl) return name;
1151
+ }
1152
+ function resolveBehaviorsExporterScript() {
1153
+ try {
1154
+ const thisFile = fileURLToPath(import.meta.url);
1155
+ const scriptPath = path8.join(
1156
+ path8.dirname(thisFile),
1157
+ "..",
1158
+ "bin",
1159
+ "exe-export-behaviors.js"
1160
+ );
1161
+ return existsSync8(scriptPath) ? scriptPath : null;
1162
+ } catch {
1163
+ return null;
1164
+ }
1165
+ }
1166
+ function exportBehaviorsSync(agentId, projectName, sessionKey) {
1167
+ const script = resolveBehaviorsExporterScript();
1168
+ if (!script) return null;
1169
+ try {
1170
+ const output = execFileSync2(
1171
+ process.execPath,
1172
+ [script, agentId, projectName, sessionKey],
1173
+ { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
1174
+ ).trim();
1175
+ return output.length > 0 ? output : null;
1176
+ } catch (err) {
1177
+ process.stderr.write(
1178
+ `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
1179
+ `
1180
+ );
1181
+ return null;
1182
+ }
1183
+ }
1184
+ function getMySession() {
1185
+ return getTransport().getMySession();
1186
+ }
1187
+ function employeeSessionName(employee, exeSession, instance) {
1188
+ if (!/^exe\d+$/.test(exeSession)) {
1189
+ const root = extractRootExe(exeSession);
1190
+ if (root) {
1191
+ process.stderr.write(
1192
+ `[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
1193
+ `
1194
+ );
1195
+ exeSession = root;
1196
+ } else {
1197
+ throw new Error(
1198
+ `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
1199
+ );
1200
+ }
1201
+ }
1202
+ const suffix = instance != null && instance > 0 ? String(instance) : "";
1203
+ const name = `${employee}${suffix}-${exeSession}`;
1204
+ if (!VALID_SESSION_NAME.test(name)) {
1205
+ throw new Error(
1206
+ `Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
1207
+ );
1208
+ }
1209
+ return name;
1210
+ }
1211
+ function parseParentExe(sessionName, agentId) {
1212
+ const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1213
+ const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
1214
+ const match = sessionName.match(regex);
1215
+ return match?.[1] ?? null;
1216
+ }
1217
+ function extractRootExe(name) {
1218
+ const match = name.match(/(exe\d+)$/);
1219
+ return match?.[1] ?? null;
1220
+ }
1221
+ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1222
+ if (!existsSync8(SESSION_CACHE)) {
1223
+ mkdirSync4(SESSION_CACHE, { recursive: true });
951
1224
  }
952
- return DEFAULT_PROVIDER;
1225
+ const rootExe = extractRootExe(parentExe) ?? parentExe;
1226
+ const filePath = path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1227
+ writeFileSync4(filePath, JSON.stringify({
1228
+ parentExe: rootExe,
1229
+ dispatchedBy: dispatchedBy || rootExe,
1230
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1231
+ }));
953
1232
  }
954
- var PROVIDER_TABLE, DEFAULT_PROVIDER;
955
- var init_provider_table = __esm({
956
- "src/lib/provider-table.ts"() {
957
- "use strict";
958
- PROVIDER_TABLE = {
959
- opencode: {
960
- baseUrl: "https://opencode.ai/zen/go",
961
- apiKeyEnv: "OPENCODE_API_KEY",
962
- defaultModel: "minimax-m2.7"
963
- }
964
- };
965
- DEFAULT_PROVIDER = "default";
1233
+ function getParentExe(sessionKey) {
1234
+ try {
1235
+ const data = JSON.parse(readFileSync8(path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1236
+ return data.parentExe || null;
1237
+ } catch {
1238
+ return null;
966
1239
  }
967
- });
968
-
969
- // src/lib/intercom-queue.ts
970
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
971
- import path6 from "path";
972
- import os4 from "os";
973
- function ensureDir() {
974
- const dir = path6.dirname(QUEUE_PATH);
975
- if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
976
1240
  }
977
- function readQueue() {
1241
+ function getDispatchedBy(sessionKey) {
978
1242
  try {
979
- if (!existsSync6(QUEUE_PATH)) return [];
980
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
1243
+ const data = JSON.parse(readFileSync8(
1244
+ path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1245
+ "utf8"
1246
+ ));
1247
+ return data.dispatchedBy ?? data.parentExe ?? null;
981
1248
  } catch {
982
- return [];
1249
+ return null;
983
1250
  }
984
1251
  }
985
- function writeQueue(queue) {
986
- ensureDir();
987
- const tmp = `${QUEUE_PATH}.tmp`;
988
- writeFileSync2(tmp, JSON.stringify(queue, null, 2));
989
- renameSync2(tmp, QUEUE_PATH);
1252
+ function resolveExeSession() {
1253
+ const mySession = getMySession();
1254
+ if (!mySession) return null;
1255
+ try {
1256
+ const key = getSessionKey();
1257
+ const parentExe = getParentExe(key);
1258
+ if (parentExe) {
1259
+ return extractRootExe(parentExe) ?? parentExe;
1260
+ }
1261
+ } catch {
1262
+ }
1263
+ return extractRootExe(mySession) ?? mySession;
990
1264
  }
991
- function queueIntercom(targetSession, reason) {
992
- const queue = readQueue();
993
- const existing = queue.find((q) => q.targetSession === targetSession);
994
- if (existing) {
995
- existing.attempts++;
996
- existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
997
- existing.reason = reason;
998
- } else {
999
- queue.push({
1000
- targetSession,
1001
- queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
1002
- attempts: 0,
1003
- reason
1004
- });
1265
+ function isEmployeeAlive(sessionName) {
1266
+ return getTransport().isAlive(sessionName);
1267
+ }
1268
+ function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
1269
+ const base = employeeSessionName(employeeName, exeSession);
1270
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
1271
+ for (let i = 2; i <= maxInstances; i++) {
1272
+ const candidate = employeeSessionName(employeeName, exeSession, i);
1273
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
1005
1274
  }
1006
- writeQueue(queue);
1275
+ return null;
1007
1276
  }
1008
- var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
1009
- var init_intercom_queue = __esm({
1010
- "src/lib/intercom-queue.ts"() {
1011
- "use strict";
1012
- QUEUE_PATH = path6.join(os4.homedir(), ".exe-os", "intercom-queue.json");
1013
- TTL_MS = 60 * 60 * 1e3;
1014
- INTERCOM_LOG = path6.join(os4.homedir(), ".exe-os", "intercom.log");
1277
+ async function verifyPaneAtCapacity(sessionName) {
1278
+ const transport = getTransport();
1279
+ if (!transport.isAlive(sessionName)) {
1280
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
1015
1281
  }
1016
- });
1017
-
1018
- // src/lib/license.ts
1019
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
1020
- import { randomUUID } from "crypto";
1021
- import path7 from "path";
1022
- import { jwtVerify, importSPKI } from "jose";
1023
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1024
- var init_license = __esm({
1025
- "src/lib/license.ts"() {
1026
- "use strict";
1027
- init_config();
1028
- LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
1029
- CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
1030
- DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
1031
- PLAN_LIMITS = {
1032
- free: { devices: 1, employees: 1, memories: 5e3 },
1033
- pro: { devices: 2, employees: 5, memories: 1e5 },
1034
- team: { devices: 10, employees: 20, memories: 1e6 },
1035
- agency: { devices: 50, employees: 100, memories: 1e7 },
1036
- enterprise: { devices: -1, employees: -1, memories: -1 }
1282
+ let pane;
1283
+ try {
1284
+ pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
1285
+ } catch (err) {
1286
+ return {
1287
+ atCapacity: false,
1288
+ reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
1037
1289
  };
1038
1290
  }
1039
- });
1040
-
1041
- // src/lib/plan-limits.ts
1042
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
1043
- import path8 from "path";
1044
- function getLicenseSync() {
1045
- try {
1046
- if (!existsSync8(CACHE_PATH2)) return freeLicense();
1047
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
1048
- if (!raw.token || typeof raw.token !== "string") return freeLicense();
1049
- const parts = raw.token.split(".");
1050
- if (parts.length !== 3) return freeLicense();
1051
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
1052
- const plan = payload.plan ?? "free";
1053
- const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
1291
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
1292
+ if (!isAtCapacity2(pane)) {
1054
1293
  return {
1055
- valid: true,
1056
- plan,
1057
- email: payload.sub ?? "",
1058
- expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
1059
- deviceLimit: limits.devices,
1060
- employeeLimit: limits.employees,
1061
- memoryLimit: limits.memories
1294
+ atCapacity: false,
1295
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
1062
1296
  };
1063
- } catch {
1064
- return freeLicense();
1065
1297
  }
1066
- }
1067
- function freeLicense() {
1068
- const limits = PLAN_LIMITS.free;
1069
1298
  return {
1070
- valid: true,
1071
- plan: "free",
1072
- email: "",
1073
- expiresAt: null,
1074
- deviceLimit: limits.devices,
1075
- employeeLimit: limits.employees,
1076
- memoryLimit: limits.memories
1299
+ atCapacity: true,
1300
+ reason: "capacity banner matched in recent pane output"
1077
1301
  };
1078
1302
  }
1079
- function assertEmployeeLimitSync(rosterPath) {
1080
- const license = getLicenseSync();
1081
- if (license.employeeLimit < 0) return;
1082
- const filePath = rosterPath ?? EMPLOYEES_PATH;
1083
- let count = 0;
1303
+ function readDebounceState() {
1084
1304
  try {
1085
- if (existsSync8(filePath)) {
1086
- const raw = readFileSync8(filePath, "utf8");
1087
- const employees = JSON.parse(raw);
1088
- count = Array.isArray(employees) ? employees.length : 0;
1089
- }
1305
+ if (!existsSync8(DEBOUNCE_FILE)) return {};
1306
+ return JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1090
1307
  } catch {
1091
- throw new PlanLimitError(
1092
- `Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
1093
- );
1308
+ return {};
1094
1309
  }
1095
- if (count >= license.employeeLimit) {
1096
- throw new PlanLimitError(
1097
- `Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
1098
- );
1310
+ }
1311
+ function writeDebounceState(state) {
1312
+ try {
1313
+ if (!existsSync8(SESSION_CACHE)) mkdirSync4(SESSION_CACHE, { recursive: true });
1314
+ writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
1315
+ } catch {
1099
1316
  }
1100
1317
  }
1101
- var PlanLimitError, CACHE_PATH2;
1102
- var init_plan_limits = __esm({
1103
- "src/lib/plan-limits.ts"() {
1104
- "use strict";
1105
- init_database();
1106
- init_employees();
1107
- init_license();
1108
- init_config();
1109
- PlanLimitError = class extends Error {
1110
- constructor(message) {
1111
- super(message);
1112
- this.name = "PlanLimitError";
1318
+ function isDebounced(targetSession) {
1319
+ const state = readDebounceState();
1320
+ const lastSent = state[targetSession] ?? 0;
1321
+ return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
1322
+ }
1323
+ function recordDebounce(targetSession) {
1324
+ const state = readDebounceState();
1325
+ state[targetSession] = Date.now();
1326
+ const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
1327
+ for (const key of Object.keys(state)) {
1328
+ if ((state[key] ?? 0) < cutoff) delete state[key];
1329
+ }
1330
+ writeDebounceState(state);
1331
+ }
1332
+ function logIntercom(msg) {
1333
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
1334
+ `;
1335
+ process.stderr.write(`[intercom] ${msg}
1336
+ `);
1337
+ try {
1338
+ appendFileSync(INTERCOM_LOG2, line);
1339
+ } catch {
1340
+ }
1341
+ }
1342
+ function getSessionState(sessionName) {
1343
+ const transport = getTransport();
1344
+ if (!transport.isAlive(sessionName)) return "offline";
1345
+ try {
1346
+ const pane = transport.capturePane(sessionName, 5);
1347
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
1348
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
1349
+ return "no_claude";
1113
1350
  }
1114
- };
1115
- CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
1351
+ }
1352
+ if (/Running…/.test(pane)) return "tool";
1353
+ if (BUSY_PATTERN.test(pane)) return "thinking";
1354
+ return "idle";
1355
+ } catch {
1356
+ return "offline";
1116
1357
  }
1117
- });
1118
-
1119
- // src/lib/tmux-routing.ts
1120
- import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
1121
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync9, appendFileSync } from "fs";
1122
- import path9 from "path";
1123
- import os5 from "os";
1124
- import { fileURLToPath } from "url";
1125
- import { unlinkSync as unlinkSync2 } from "fs";
1126
- function spawnLockPath(sessionName) {
1127
- return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1128
1358
  }
1129
- function isProcessAlive(pid) {
1359
+ function isSessionBusy(sessionName) {
1360
+ const state = getSessionState(sessionName);
1361
+ return state === "thinking" || state === "tool";
1362
+ }
1363
+ function isExeSession(sessionName) {
1364
+ return /^exe\d*$/.test(sessionName);
1365
+ }
1366
+ function sendIntercom(targetSession) {
1367
+ const transport = getTransport();
1368
+ if (isExeSession(targetSession)) {
1369
+ logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
1370
+ return "skipped_exe";
1371
+ }
1372
+ if (isDebounced(targetSession)) {
1373
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
1374
+ return "debounced";
1375
+ }
1130
1376
  try {
1131
- process.kill(pid, 0);
1132
- return true;
1377
+ const sessions = transport.listSessions();
1378
+ if (!sessions.includes(targetSession)) {
1379
+ logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
1380
+ return "failed";
1381
+ }
1382
+ const sessionState = getSessionState(targetSession);
1383
+ if (sessionState === "no_claude") {
1384
+ queueIntercom(targetSession, "claude not running in session");
1385
+ recordDebounce(targetSession);
1386
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
1387
+ return "queued";
1388
+ }
1389
+ if (sessionState === "thinking" || sessionState === "tool") {
1390
+ queueIntercom(targetSession, "session busy at send time");
1391
+ recordDebounce(targetSession);
1392
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
1393
+ return "queued";
1394
+ }
1395
+ if (transport.isPaneInCopyMode(targetSession)) {
1396
+ logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
1397
+ transport.sendKeys(targetSession, "q");
1398
+ }
1399
+ transport.sendKeys(targetSession, "/exe-intercom");
1400
+ recordDebounce(targetSession);
1401
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
1402
+ return "delivered";
1133
1403
  } catch {
1134
- return false;
1404
+ logIntercom(`FAIL \u2192 ${targetSession}`);
1405
+ return "failed";
1135
1406
  }
1136
1407
  }
1137
- function acquireSpawnLock(sessionName) {
1138
- if (!existsSync9(SPAWN_LOCK_DIR)) {
1139
- mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
1408
+ function notifyParentExe(sessionKey) {
1409
+ const target = getDispatchedBy(sessionKey);
1410
+ if (!target) {
1411
+ process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
1412
+ `);
1413
+ return false;
1140
1414
  }
1141
- const lockFile = spawnLockPath(sessionName);
1142
- if (existsSync9(lockFile)) {
1143
- try {
1144
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1145
- const age = Date.now() - lock.timestamp;
1146
- if (isProcessAlive(lock.pid) && age < 6e4) {
1147
- return false;
1148
- }
1149
- } catch {
1415
+ process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
1416
+ `);
1417
+ const result = sendIntercom(target);
1418
+ if (result === "failed") {
1419
+ const rootExe = resolveExeSession();
1420
+ if (rootExe && rootExe !== target) {
1421
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
1422
+ `);
1423
+ const fallback = sendIntercom(rootExe);
1424
+ return fallback !== "failed";
1150
1425
  }
1426
+ return false;
1151
1427
  }
1152
- writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1153
1428
  return true;
1154
1429
  }
1155
- function releaseSpawnLock(sessionName) {
1156
- try {
1157
- unlinkSync2(spawnLockPath(sessionName));
1158
- } catch {
1159
- }
1160
- }
1161
- function resolveBehaviorsExporterScript() {
1162
- try {
1163
- const thisFile = fileURLToPath(import.meta.url);
1164
- const scriptPath = path9.join(
1165
- path9.dirname(thisFile),
1166
- "..",
1167
- "bin",
1168
- "exe-export-behaviors.js"
1169
- );
1170
- return existsSync9(scriptPath) ? scriptPath : null;
1171
- } catch {
1172
- return null;
1430
+ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1431
+ if (employeeName === "exe") {
1432
+ return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
1173
1433
  }
1174
- }
1175
- function exportBehaviorsSync(agentId, projectName, sessionKey) {
1176
- const script = resolveBehaviorsExporterScript();
1177
- if (!script) return null;
1178
1434
  try {
1179
- const output = execFileSync2(
1180
- process.execPath,
1181
- [script, agentId, projectName, sessionKey],
1182
- { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
1183
- ).trim();
1184
- return output.length > 0 ? output : null;
1435
+ assertEmployeeLimitSync();
1185
1436
  } catch (err) {
1186
- process.stderr.write(
1187
- `[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
1437
+ if (err instanceof PlanLimitError) {
1438
+ return { status: "failed", sessionName: "", error: err.message };
1439
+ }
1440
+ }
1441
+ if (/-exe\d*$/.test(employeeName)) {
1442
+ const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
1443
+ return {
1444
+ status: "failed",
1445
+ sessionName: "",
1446
+ error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
1447
+ };
1448
+ }
1449
+ if (!/^exe\d+$/.test(exeSession)) {
1450
+ const root = extractRootExe(exeSession);
1451
+ if (root) {
1452
+ process.stderr.write(
1453
+ `[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
1188
1454
  `
1455
+ );
1456
+ exeSession = root;
1457
+ } else {
1458
+ return {
1459
+ status: "failed",
1460
+ sessionName: "",
1461
+ error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
1462
+ };
1463
+ }
1464
+ }
1465
+ let effectiveInstance = opts?.instance;
1466
+ if (effectiveInstance === void 0 && opts?.autoInstance) {
1467
+ const free = findFreeInstance(
1468
+ employeeName,
1469
+ exeSession,
1470
+ opts.maxAutoInstances ?? 10
1189
1471
  );
1190
- return null;
1472
+ if (free === null) {
1473
+ return {
1474
+ status: "failed",
1475
+ sessionName: employeeSessionName(employeeName, exeSession),
1476
+ error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
1477
+ };
1478
+ }
1479
+ effectiveInstance = free === 0 ? void 0 : free;
1191
1480
  }
1192
- }
1193
- function getMySession() {
1194
- return getTransport().getMySession();
1195
- }
1196
- function employeeSessionName(employee, exeSession, instance) {
1197
- const suffix = instance != null && instance > 0 ? String(instance) : "";
1198
- return `${employee}${suffix}-${exeSession}`;
1199
- }
1200
- function extractRootExe(name) {
1201
- const match = name.match(/(exe\d+)$/);
1202
- return match?.[1] ?? null;
1203
- }
1204
- function getParentExe(sessionKey) {
1205
- try {
1206
- const data = JSON.parse(readFileSync9(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1207
- return data.parentExe || null;
1208
- } catch {
1209
- return null;
1481
+ const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
1482
+ if (isEmployeeAlive(sessionName)) {
1483
+ const result2 = sendIntercom(sessionName);
1484
+ if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
1485
+ return { status: "intercom_sent", sessionName };
1486
+ }
1487
+ if (result2 === "delivered") {
1488
+ return { status: "intercom_unprocessed", sessionName };
1489
+ }
1490
+ return { status: "failed", sessionName, error: "intercom delivery failed" };
1210
1491
  }
1211
- }
1212
- function getDispatchedBy(sessionKey) {
1213
- try {
1214
- const data = JSON.parse(readFileSync9(
1215
- path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1216
- "utf8"
1217
- ));
1218
- return data.dispatchedBy ?? data.parentExe ?? null;
1219
- } catch {
1220
- return null;
1492
+ const spawnOpts = { ...opts, instance: effectiveInstance };
1493
+ const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
1494
+ if (result.error) {
1495
+ return { status: "failed", sessionName, error: result.error };
1221
1496
  }
1497
+ return { status: "spawned", sessionName };
1222
1498
  }
1223
- function resolveExeSession() {
1224
- const mySession = getMySession();
1225
- if (!mySession) return null;
1499
+ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1500
+ const transport = getTransport();
1501
+ const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1502
+ const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1503
+ const logDir = path8.join(os5.homedir(), ".exe-os", "session-logs");
1504
+ const logFile = path8.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1505
+ if (!existsSync8(logDir)) {
1506
+ mkdirSync4(logDir, { recursive: true });
1507
+ }
1508
+ transport.kill(sessionName);
1509
+ let cleanupSuffix = "";
1226
1510
  try {
1227
- const key = getSessionKey();
1228
- const parentExe = getParentExe(key);
1229
- if (parentExe) {
1230
- return extractRootExe(parentExe) ?? parentExe;
1511
+ const thisFile = fileURLToPath(import.meta.url);
1512
+ const cleanupScript = path8.join(path8.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1513
+ if (existsSync8(cleanupScript)) {
1514
+ cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
1231
1515
  }
1232
1516
  } catch {
1233
1517
  }
1234
- return extractRootExe(mySession) ?? mySession;
1235
- }
1236
- function isEmployeeAlive(sessionName) {
1237
- return getTransport().isAlive(sessionName);
1238
- }
1239
- function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
1240
- const base = employeeSessionName(employeeName, exeSession);
1241
- if (!isAlive(base) && acquireSpawnLock(base)) return 0;
1242
- for (let i = 2; i <= maxInstances; i++) {
1243
- const candidate = employeeSessionName(employeeName, exeSession, i);
1244
- if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
1245
- }
1246
- return null;
1247
- }
1248
- function readDebounceState() {
1249
1518
  try {
1250
- if (!existsSync9(DEBOUNCE_FILE)) return {};
1251
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
1519
+ const claudeJsonPath = path8.join(os5.homedir(), ".claude.json");
1520
+ let claudeJson = {};
1521
+ try {
1522
+ claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
1523
+ } catch {
1524
+ }
1525
+ if (!claudeJson.projects) claudeJson.projects = {};
1526
+ const projects = claudeJson.projects;
1527
+ const trustDir = opts?.cwd ?? projectDir;
1528
+ if (!projects[trustDir]) projects[trustDir] = {};
1529
+ projects[trustDir].hasTrustDialogAccepted = true;
1530
+ writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1252
1531
  } catch {
1253
- return {};
1254
1532
  }
1255
- }
1256
- function writeDebounceState(state) {
1257
1533
  try {
1258
- if (!existsSync9(SESSION_CACHE)) mkdirSync4(SESSION_CACHE, { recursive: true });
1259
- writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
1534
+ const settingsDir = path8.join(os5.homedir(), ".claude", "projects");
1535
+ const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1536
+ const projSettingsDir = path8.join(settingsDir, normalizedKey);
1537
+ const settingsPath = path8.join(projSettingsDir, "settings.json");
1538
+ let settings = {};
1539
+ try {
1540
+ settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
1541
+ } catch {
1542
+ }
1543
+ const perms = settings.permissions ?? {};
1544
+ const allow = perms.allow ?? [];
1545
+ const toolNames = [
1546
+ "recall_my_memory",
1547
+ "store_memory",
1548
+ "create_task",
1549
+ "update_task",
1550
+ "list_tasks",
1551
+ "get_task",
1552
+ "ask_team_memory",
1553
+ "store_behavior",
1554
+ "get_identity",
1555
+ "send_message"
1556
+ ];
1557
+ const requiredTools = expandDualPrefixTools(toolNames);
1558
+ let changed = false;
1559
+ for (const tool of requiredTools) {
1560
+ if (!allow.includes(tool)) {
1561
+ allow.push(tool);
1562
+ changed = true;
1563
+ }
1564
+ }
1565
+ if (changed) {
1566
+ perms.allow = allow;
1567
+ settings.permissions = perms;
1568
+ mkdirSync4(projSettingsDir, { recursive: true });
1569
+ writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1570
+ }
1260
1571
  } catch {
1261
1572
  }
1262
- }
1263
- function isDebounced(targetSession) {
1264
- const state = readDebounceState();
1265
- const lastSent = state[targetSession] ?? 0;
1266
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
1267
- }
1268
- function recordDebounce(targetSession) {
1269
- const state = readDebounceState();
1270
- state[targetSession] = Date.now();
1271
- const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
1272
- for (const key of Object.keys(state)) {
1273
- if ((state[key] ?? 0) < cutoff) delete state[key];
1573
+ const spawnCwd = opts?.cwd ?? projectDir;
1574
+ const useExeAgent = !!(opts?.model && opts?.provider);
1575
+ const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
1576
+ const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
1577
+ let identityFlag = "";
1578
+ let behaviorsFlag = "";
1579
+ let legacyFallbackWarned = false;
1580
+ if (!useExeAgent && !useBinSymlink) {
1581
+ const identityPath = path8.join(
1582
+ os5.homedir(),
1583
+ ".exe-os",
1584
+ "identity",
1585
+ `${employeeName}.md`
1586
+ );
1587
+ _resetCcAgentSupportCache();
1588
+ const hasAgentFlag = claudeSupportsAgentFlag();
1589
+ if (hasAgentFlag) {
1590
+ identityFlag = ` --agent ${employeeName}`;
1591
+ } else if (existsSync8(identityPath)) {
1592
+ identityFlag = ` --append-system-prompt-file ${identityPath}`;
1593
+ legacyFallbackWarned = true;
1594
+ }
1595
+ const behaviorsFile = exportBehaviorsSync(
1596
+ employeeName,
1597
+ path8.basename(spawnCwd),
1598
+ sessionName
1599
+ );
1600
+ if (behaviorsFile) {
1601
+ behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
1602
+ }
1274
1603
  }
1275
- writeDebounceState(state);
1276
- }
1277
- function logIntercom(msg) {
1278
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
1279
- `;
1280
- process.stderr.write(`[intercom] ${msg}
1281
- `);
1604
+ if (legacyFallbackWarned) {
1605
+ process.stderr.write(
1606
+ `[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.
1607
+ `
1608
+ );
1609
+ }
1610
+ let sessionContextFlag = "";
1282
1611
  try {
1283
- appendFileSync(INTERCOM_LOG2, line);
1612
+ const ctxDir = path8.join(os5.homedir(), ".exe-os", "session-cache");
1613
+ mkdirSync4(ctxDir, { recursive: true });
1614
+ const ctxFile = path8.join(ctxDir, `session-context-${sessionName}.md`);
1615
+ const ctxContent = [
1616
+ `## Session Context`,
1617
+ `You are running in tmux session: ${sessionName}.`,
1618
+ `Your parent exe session is ${exeSession}.`,
1619
+ `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
1620
+ ].join("\n");
1621
+ writeFileSync4(ctxFile, ctxContent);
1622
+ sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
1284
1623
  } catch {
1285
1624
  }
1286
- }
1287
- function getSessionState(sessionName) {
1288
- const transport = getTransport();
1289
- if (!transport.isAlive(sessionName)) return "offline";
1290
- try {
1291
- const pane = transport.capturePane(sessionName, 5);
1292
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
1293
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
1294
- return "no_claude";
1625
+ let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
1626
+ if (ccProvider !== DEFAULT_PROVIDER) {
1627
+ const cfg = PROVIDER_TABLE[ccProvider];
1628
+ if (cfg?.apiKeyEnv) {
1629
+ const keyVal = process.env[cfg.apiKeyEnv];
1630
+ if (keyVal) {
1631
+ envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
1295
1632
  }
1296
1633
  }
1297
- if (/Running…/.test(pane)) return "tool";
1298
- if (BUSY_PATTERN.test(pane)) return "thinking";
1299
- return "idle";
1300
- } catch {
1301
- return "offline";
1302
1634
  }
1303
- }
1304
- function isExeSession(sessionName) {
1305
- return /^exe\d*$/.test(sessionName);
1306
- }
1307
- function sendIntercom(targetSession) {
1308
- const transport = getTransport();
1309
- if (isExeSession(targetSession)) {
1310
- logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
1311
- return "skipped_exe";
1635
+ let spawnCommand;
1636
+ if (useExeAgent) {
1637
+ spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
1638
+ } else if (useBinSymlink) {
1639
+ const binName = `${employeeName}-${ccProvider}`;
1640
+ process.stderr.write(
1641
+ `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
1642
+ `
1643
+ );
1644
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
1645
+ } else {
1646
+ spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
1312
1647
  }
1313
- if (isDebounced(targetSession)) {
1314
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
1315
- return "debounced";
1648
+ const spawnResult = transport.spawn(sessionName, {
1649
+ cwd: spawnCwd,
1650
+ command: spawnCommand
1651
+ });
1652
+ if (spawnResult.error) {
1653
+ releaseSpawnLock(sessionName);
1654
+ return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
1316
1655
  }
1656
+ transport.pipeLog(sessionName, logFile);
1317
1657
  try {
1318
- const sessions = transport.listSessions();
1319
- if (!sessions.includes(targetSession)) {
1320
- logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
1321
- return "failed";
1322
- }
1323
- const sessionState = getSessionState(targetSession);
1324
- if (sessionState === "no_claude") {
1325
- queueIntercom(targetSession, "claude not running in session");
1326
- recordDebounce(targetSession);
1327
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
1328
- return "queued";
1329
- }
1330
- if (sessionState === "thinking" || sessionState === "tool") {
1331
- queueIntercom(targetSession, "session busy at send time");
1332
- recordDebounce(targetSession);
1333
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
1334
- return "queued";
1658
+ const mySession = getMySession();
1659
+ const dispatchInfo = path8.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1660
+ writeFileSync4(dispatchInfo, JSON.stringify({
1661
+ dispatchedBy: mySession,
1662
+ rootExe: exeSession,
1663
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
1664
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1665
+ }));
1666
+ } catch {
1667
+ }
1668
+ let booted = false;
1669
+ for (let i = 0; i < 30; i++) {
1670
+ try {
1671
+ execSync4("sleep 0.5");
1672
+ } catch {
1335
1673
  }
1336
- if (transport.isPaneInCopyMode(targetSession)) {
1337
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
1338
- transport.sendKeys(targetSession, "q");
1674
+ try {
1675
+ const pane = transport.capturePane(sessionName);
1676
+ if (useExeAgent) {
1677
+ if (pane.includes("[exe-agent]") || pane.includes("online")) {
1678
+ booted = true;
1679
+ break;
1680
+ }
1681
+ } else {
1682
+ if (pane.includes("Claude Code") || pane.includes("\u276F")) {
1683
+ booted = true;
1684
+ break;
1685
+ }
1686
+ }
1687
+ } catch {
1339
1688
  }
1340
- transport.sendKeys(targetSession, "/exe-intercom");
1341
- recordDebounce(targetSession);
1342
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
1343
- return "delivered";
1344
- } catch {
1345
- logIntercom(`FAIL \u2192 ${targetSession}`);
1346
- return "failed";
1347
1689
  }
1348
- }
1349
- function notifyParentExe(sessionKey) {
1350
- const target = getDispatchedBy(sessionKey);
1351
- if (!target) {
1352
- process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
1353
- `);
1354
- return false;
1690
+ if (!booted) {
1691
+ releaseSpawnLock(sessionName);
1692
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
1355
1693
  }
1356
- process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
1357
- `);
1358
- const result = sendIntercom(target);
1359
- if (result === "failed") {
1360
- const rootExe = resolveExeSession();
1361
- if (rootExe && rootExe !== target) {
1362
- process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
1363
- `);
1364
- const fallback = sendIntercom(rootExe);
1365
- return fallback !== "failed";
1694
+ if (!useExeAgent) {
1695
+ try {
1696
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
1697
+ } catch {
1366
1698
  }
1367
- return false;
1368
1699
  }
1369
- return true;
1700
+ registerSession({
1701
+ windowName: sessionName,
1702
+ agentId: employeeName,
1703
+ projectDir: spawnCwd,
1704
+ parentExe: exeSession,
1705
+ pid: 0,
1706
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1707
+ });
1708
+ releaseSpawnLock(sessionName);
1709
+ return { sessionName };
1370
1710
  }
1371
- function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1372
- if (employeeName === "exe") {
1373
- return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
1711
+ 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;
1712
+ var init_tmux_routing = __esm({
1713
+ "src/lib/tmux-routing.ts"() {
1714
+ "use strict";
1715
+ init_session_registry();
1716
+ init_session_key();
1717
+ init_transport();
1718
+ init_cc_agent_support();
1719
+ init_mcp_prefix();
1720
+ init_provider_table();
1721
+ init_intercom_queue();
1722
+ init_plan_limits();
1723
+ SPAWN_LOCK_DIR = path8.join(os5.homedir(), ".exe-os", "spawn-locks");
1724
+ SESSION_CACHE = path8.join(os5.homedir(), ".exe-os", "session-cache");
1725
+ BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1726
+ VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
1727
+ VERIFY_PANE_LINES = 200;
1728
+ INTERCOM_DEBOUNCE_MS = 3e4;
1729
+ INTERCOM_LOG2 = path8.join(os5.homedir(), ".exe-os", "intercom.log");
1730
+ DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
1731
+ DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
1732
+ BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
1374
1733
  }
1375
- try {
1376
- assertEmployeeLimitSync();
1377
- } catch (err) {
1378
- if (err instanceof PlanLimitError) {
1379
- return { status: "failed", sessionName: "", error: err.message };
1380
- }
1734
+ });
1735
+
1736
+ // src/lib/tasks-crud.ts
1737
+ import crypto3 from "crypto";
1738
+ import path9 from "path";
1739
+ import { execSync as execSync5 } from "child_process";
1740
+ import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1741
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1742
+ async function writeCheckpoint(input) {
1743
+ const client = getClient();
1744
+ const row = await resolveTask(client, input.taskId);
1745
+ const taskId = String(row.id);
1746
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1747
+ const blockedByIds = [];
1748
+ if (row.blocked_by) {
1749
+ blockedByIds.push(String(row.blocked_by));
1381
1750
  }
1382
- if (/-exe\d*$/.test(employeeName)) {
1383
- const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
1384
- return {
1385
- status: "failed",
1386
- sessionName: "",
1387
- error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
1388
- };
1751
+ const checkpoint = {
1752
+ step: input.step,
1753
+ context_summary: input.contextSummary,
1754
+ files_touched: input.filesTouched ?? [],
1755
+ blocked_by_ids: blockedByIds,
1756
+ last_checkpoint_at: now
1757
+ };
1758
+ const result = await client.execute({
1759
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
1760
+ args: [JSON.stringify(checkpoint), now, taskId]
1761
+ });
1762
+ if (result.rowsAffected === 0) {
1763
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
1389
1764
  }
1390
- let effectiveInstance = opts?.instance;
1391
- if (effectiveInstance === void 0 && opts?.autoInstance) {
1392
- const free = findFreeInstance(
1393
- employeeName,
1394
- exeSession,
1395
- opts.maxAutoInstances ?? 10
1765
+ const countResult = await client.execute({
1766
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
1767
+ args: [taskId]
1768
+ });
1769
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
1770
+ return { checkpointCount };
1771
+ }
1772
+ function extractParentFromContext(contextBody) {
1773
+ if (!contextBody) return null;
1774
+ const match = contextBody.match(
1775
+ /Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
1776
+ );
1777
+ return match ? match[1].toLowerCase() : null;
1778
+ }
1779
+ function slugify(title) {
1780
+ return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1781
+ }
1782
+ async function resolveTask(client, identifier) {
1783
+ let result = await client.execute({
1784
+ sql: "SELECT * FROM tasks WHERE id = ?",
1785
+ args: [identifier]
1786
+ });
1787
+ if (result.rows.length === 1) return result.rows[0];
1788
+ result = await client.execute({
1789
+ sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
1790
+ args: [`%${identifier}%`]
1791
+ });
1792
+ if (result.rows.length === 1) return result.rows[0];
1793
+ if (result.rows.length > 1) {
1794
+ const exact = result.rows.filter(
1795
+ (r) => String(r.task_file).endsWith(`/${identifier}.md`)
1796
+ );
1797
+ if (exact.length === 1) return exact[0];
1798
+ const candidates = exact.length > 1 ? exact : result.rows;
1799
+ const active = candidates.filter(
1800
+ (r) => !["done", "cancelled"].includes(String(r.status))
1801
+ );
1802
+ if (active.length === 1) return active[0];
1803
+ const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
1804
+ throw new Error(
1805
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
1396
1806
  );
1397
- if (free === null) {
1398
- return {
1399
- status: "failed",
1400
- sessionName: employeeSessionName(employeeName, exeSession),
1401
- error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
1402
- };
1403
- }
1404
- effectiveInstance = free === 0 ? void 0 : free;
1405
- }
1406
- const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
1407
- if (isEmployeeAlive(sessionName)) {
1408
- const result2 = sendIntercom(sessionName);
1409
- if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
1410
- return { status: "intercom_sent", sessionName };
1411
- }
1412
- if (result2 === "delivered") {
1413
- return { status: "intercom_unprocessed", sessionName };
1414
- }
1415
- return { status: "failed", sessionName, error: "intercom delivery failed" };
1416
1807
  }
1417
- const spawnOpts = { ...opts, instance: effectiveInstance };
1418
- const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
1419
- if (result.error) {
1420
- return { status: "failed", sessionName, error: result.error };
1808
+ result = await client.execute({
1809
+ sql: "SELECT * FROM tasks WHERE title LIKE ?",
1810
+ args: [`%${identifier}%`]
1811
+ });
1812
+ if (result.rows.length === 1) return result.rows[0];
1813
+ if (result.rows.length > 1) {
1814
+ const active = result.rows.filter(
1815
+ (r) => !["done", "cancelled"].includes(String(r.status))
1816
+ );
1817
+ if (active.length === 1) return active[0];
1818
+ const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
1819
+ throw new Error(
1820
+ `Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
1821
+ );
1421
1822
  }
1422
- return { status: "spawned", sessionName };
1823
+ throw new Error(`Task not found: ${identifier}`);
1423
1824
  }
1424
- function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1425
- const transport = getTransport();
1426
- const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1427
- const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1428
- const logDir = path9.join(os5.homedir(), ".exe-os", "session-logs");
1429
- const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1430
- if (!existsSync9(logDir)) {
1431
- mkdirSync4(logDir, { recursive: true });
1825
+ async function createTaskCore(input) {
1826
+ const client = getClient();
1827
+ const id = crypto3.randomUUID();
1828
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1829
+ const slug = slugify(input.title);
1830
+ const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
1831
+ let blockedById = null;
1832
+ const initialStatus = input.blockedBy ? "blocked" : "open";
1833
+ if (input.blockedBy) {
1834
+ const blocker = await resolveTask(client, input.blockedBy);
1835
+ blockedById = String(blocker.id);
1432
1836
  }
1433
- transport.kill(sessionName);
1434
- let cleanupSuffix = "";
1435
- try {
1436
- const thisFile = fileURLToPath(import.meta.url);
1437
- const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1438
- if (existsSync9(cleanupScript)) {
1439
- cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
1837
+ let parentTaskId = null;
1838
+ let parentRef = input.parentTaskId;
1839
+ if (!parentRef) {
1840
+ const extracted = extractParentFromContext(input.context);
1841
+ if (extracted) {
1842
+ parentRef = extracted;
1843
+ process.stderr.write(
1844
+ "[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
1845
+ );
1440
1846
  }
1441
- } catch {
1442
1847
  }
1443
- try {
1444
- const claudeJsonPath = path9.join(os5.homedir(), ".claude.json");
1445
- let claudeJson = {};
1848
+ if (parentRef) {
1446
1849
  try {
1447
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
1448
- } catch {
1850
+ const parent = await resolveTask(client, parentRef);
1851
+ parentTaskId = String(parent.id);
1852
+ } catch (err) {
1853
+ if (!input.parentTaskId) {
1854
+ throw new Error(
1855
+ `create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
1856
+ );
1857
+ }
1858
+ throw err;
1449
1859
  }
1450
- if (!claudeJson.projects) claudeJson.projects = {};
1451
- const projects = claudeJson.projects;
1452
- const trustDir = opts?.cwd ?? projectDir;
1453
- if (!projects[trustDir]) projects[trustDir] = {};
1454
- projects[trustDir].hasTrustDialogAccepted = true;
1455
- writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1456
- } catch {
1457
1860
  }
1458
- try {
1459
- const settingsDir = path9.join(os5.homedir(), ".claude", "projects");
1460
- const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1461
- const projSettingsDir = path9.join(settingsDir, normalizedKey);
1462
- const settingsPath = path9.join(projSettingsDir, "settings.json");
1463
- let settings = {};
1861
+ let warning;
1862
+ const dupCheck = await client.execute({
1863
+ sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
1864
+ args: [input.title, input.assignedTo]
1865
+ });
1866
+ if (dupCheck.rows.length > 0) {
1867
+ warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
1868
+ }
1869
+ if (input.baseDir) {
1464
1870
  try {
1465
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
1871
+ await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
1872
+ await mkdir3(path9.join(input.baseDir, "exe", "research"), { recursive: true });
1873
+ await ensureArchitectureDoc(input.baseDir, input.projectName);
1874
+ await ensureGitignoreExe(input.baseDir);
1466
1875
  } catch {
1467
1876
  }
1468
- const perms = settings.permissions ?? {};
1469
- const allow = perms.allow ?? [];
1470
- const toolNames = [
1471
- "recall_my_memory",
1472
- "store_memory",
1473
- "create_task",
1474
- "update_task",
1475
- "list_tasks",
1476
- "get_task",
1477
- "ask_team_memory",
1478
- "store_behavior",
1479
- "get_identity",
1480
- "send_message"
1481
- ];
1482
- const requiredTools = expandDualPrefixTools(toolNames);
1483
- let changed = false;
1484
- for (const tool of requiredTools) {
1485
- if (!allow.includes(tool)) {
1486
- allow.push(tool);
1487
- changed = true;
1488
- }
1489
- }
1490
- if (changed) {
1491
- perms.allow = allow;
1492
- settings.permissions = perms;
1493
- mkdirSync4(projSettingsDir, { recursive: true });
1494
- writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1495
- }
1877
+ }
1878
+ const complexity = input.complexity ?? "standard";
1879
+ let sessionScope = null;
1880
+ try {
1881
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1882
+ sessionScope = resolveExeSession2();
1496
1883
  } catch {
1497
1884
  }
1498
- const spawnCwd = opts?.cwd ?? projectDir;
1499
- const useExeAgent = !!(opts?.model && opts?.provider);
1500
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
1501
- const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
1502
- let identityFlag = "";
1503
- let behaviorsFlag = "";
1504
- let legacyFallbackWarned = false;
1505
- if (!useExeAgent && !useBinSymlink) {
1506
- const identityPath = path9.join(
1507
- os5.homedir(),
1508
- ".exe-os",
1509
- "identity",
1510
- `${employeeName}.md`
1511
- );
1512
- _resetCcAgentSupportCache();
1513
- const hasAgentFlag = claudeSupportsAgentFlag();
1514
- if (hasAgentFlag) {
1515
- identityFlag = ` --agent ${employeeName}`;
1516
- } else if (existsSync9(identityPath)) {
1517
- identityFlag = ` --append-system-prompt-file ${identityPath}`;
1518
- legacyFallbackWarned = true;
1519
- }
1520
- const behaviorsFile = exportBehaviorsSync(
1521
- employeeName,
1522
- path9.basename(spawnCwd),
1523
- sessionName
1524
- );
1525
- if (behaviorsFile) {
1526
- behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
1527
- }
1885
+ await client.execute({
1886
+ 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)
1887
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1888
+ args: [
1889
+ id,
1890
+ input.title,
1891
+ input.assignedTo,
1892
+ input.assignedBy,
1893
+ input.projectName,
1894
+ input.priority,
1895
+ initialStatus,
1896
+ taskFile,
1897
+ blockedById,
1898
+ parentTaskId,
1899
+ input.reviewer ?? null,
1900
+ input.context,
1901
+ complexity,
1902
+ input.budgetTokens ?? null,
1903
+ input.budgetFallbackModel ?? null,
1904
+ 0,
1905
+ null,
1906
+ sessionScope,
1907
+ now,
1908
+ now
1909
+ ]
1910
+ });
1911
+ return {
1912
+ id,
1913
+ title: input.title,
1914
+ assignedTo: input.assignedTo,
1915
+ assignedBy: input.assignedBy,
1916
+ projectName: input.projectName,
1917
+ priority: input.priority,
1918
+ status: initialStatus,
1919
+ taskFile,
1920
+ createdAt: now,
1921
+ updatedAt: now,
1922
+ warning,
1923
+ budgetTokens: input.budgetTokens ?? null,
1924
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
1925
+ tokensUsed: 0,
1926
+ tokensWarnedAt: null
1927
+ };
1928
+ }
1929
+ async function listTasks(input) {
1930
+ const client = getClient();
1931
+ const conditions = [];
1932
+ const args = [];
1933
+ if (input.assignedTo) {
1934
+ conditions.push("assigned_to = ?");
1935
+ args.push(input.assignedTo);
1528
1936
  }
1529
- if (legacyFallbackWarned) {
1530
- process.stderr.write(
1531
- `[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.
1532
- `
1533
- );
1937
+ if (input.status) {
1938
+ conditions.push("status = ?");
1939
+ args.push(input.status);
1940
+ } else {
1941
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
1942
+ }
1943
+ if (input.projectName) {
1944
+ conditions.push("project_name = ?");
1945
+ args.push(input.projectName);
1946
+ }
1947
+ if (input.priority) {
1948
+ conditions.push("priority = ?");
1949
+ args.push(input.priority);
1534
1950
  }
1535
- let sessionContextFlag = "";
1536
1951
  try {
1537
- const ctxDir = path9.join(os5.homedir(), ".exe-os", "session-cache");
1538
- mkdirSync4(ctxDir, { recursive: true });
1539
- const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
1540
- const ctxContent = [
1541
- `## Session Context`,
1542
- `You are running in tmux session: ${sessionName}.`,
1543
- `Your parent exe session is ${exeSession}.`,
1544
- `Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
1545
- ].join("\n");
1546
- writeFileSync4(ctxFile, ctxContent);
1547
- sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
1952
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1953
+ const session = resolveExeSession2();
1954
+ if (session) {
1955
+ conditions.push("(session_scope IS NULL OR session_scope = ?)");
1956
+ args.push(session);
1957
+ }
1548
1958
  } catch {
1549
1959
  }
1550
- let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
1551
- if (ccProvider !== DEFAULT_PROVIDER) {
1552
- const cfg = PROVIDER_TABLE[ccProvider];
1553
- if (cfg?.apiKeyEnv) {
1554
- const keyVal = process.env[cfg.apiKeyEnv];
1555
- if (keyVal) {
1556
- envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
1557
- }
1960
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1961
+ const result = await client.execute({
1962
+ 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`,
1963
+ args
1964
+ });
1965
+ return result.rows.map((r) => ({
1966
+ id: String(r.id),
1967
+ title: String(r.title),
1968
+ assignedTo: String(r.assigned_to),
1969
+ assignedBy: String(r.assigned_by),
1970
+ projectName: String(r.project_name),
1971
+ priority: String(r.priority),
1972
+ status: String(r.status),
1973
+ taskFile: String(r.task_file),
1974
+ createdAt: String(r.created_at),
1975
+ updatedAt: String(r.updated_at),
1976
+ checkpointCount: Number(r.checkpoint_count ?? 0),
1977
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
1978
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
1979
+ tokensUsed: Number(r.tokens_used ?? 0),
1980
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
1981
+ }));
1982
+ }
1983
+ function checkStaleCompletion(taskContext, taskCreatedAt) {
1984
+ if (!taskContext) return null;
1985
+ if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
1986
+ try {
1987
+ const since = new Date(taskCreatedAt).toISOString();
1988
+ const branch = execSync5(
1989
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
1990
+ { encoding: "utf8", timeout: 3e3 }
1991
+ ).trim();
1992
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
1993
+ const commitCount = execSync5(
1994
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
1995
+ { encoding: "utf8", timeout: 5e3 }
1996
+ ).trim();
1997
+ const count = parseInt(commitCount, 10);
1998
+ if (count === 0) {
1999
+ return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
1558
2000
  }
2001
+ return null;
2002
+ } catch {
2003
+ return null;
1559
2004
  }
1560
- let spawnCommand;
1561
- if (useExeAgent) {
1562
- spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
1563
- } else if (useBinSymlink) {
1564
- const binName = `${employeeName}-${ccProvider}`;
2005
+ }
2006
+ async function updateTaskStatus(input) {
2007
+ const client = getClient();
2008
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2009
+ const row = await resolveTask(client, input.taskId);
2010
+ const taskId = String(row.id);
2011
+ const taskFile = String(row.task_file);
2012
+ if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
1565
2013
  process.stderr.write(
1566
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
2014
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
1567
2015
  `
1568
2016
  );
1569
- spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
1570
- } else {
1571
- spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
1572
- }
1573
- const spawnResult = transport.spawn(sessionName, {
1574
- cwd: spawnCwd,
1575
- command: spawnCommand
1576
- });
1577
- if (spawnResult.error) {
1578
- releaseSpawnLock(sessionName);
1579
- return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
1580
2017
  }
1581
- transport.pipeLog(sessionName, logFile);
1582
- try {
1583
- const mySession = getMySession();
1584
- const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1585
- writeFileSync4(dispatchInfo, JSON.stringify({
1586
- dispatchedBy: mySession,
1587
- rootExe: exeSession,
1588
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
1589
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1590
- }));
1591
- } catch {
2018
+ if (input.status === "done") {
2019
+ const existingRow = await client.execute({
2020
+ sql: "SELECT context, created_at FROM tasks WHERE id = ?",
2021
+ args: [taskId]
2022
+ });
2023
+ if (existingRow.rows.length > 0) {
2024
+ const ctx = existingRow.rows[0];
2025
+ const warning = checkStaleCompletion(ctx.context, ctx.created_at);
2026
+ if (warning) {
2027
+ input.result = input.result ? `\u26A0\uFE0F ${warning}
2028
+
2029
+ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2030
+ process.stderr.write(`[tasks] ${warning} (task: ${taskId})
2031
+ `);
2032
+ }
2033
+ }
1592
2034
  }
1593
- let booted = false;
1594
- for (let i = 0; i < 30; i++) {
1595
- try {
1596
- execSync5("sleep 0.5");
1597
- } catch {
2035
+ if (input.status === "in_progress") {
2036
+ const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
2037
+ const claim = await client.execute({
2038
+ sql: `UPDATE tasks
2039
+ SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
2040
+ WHERE id = ? AND status = 'open'`,
2041
+ args: [tmuxSession, now, taskId]
2042
+ });
2043
+ if (claim.rowsAffected === 0) {
2044
+ const current = await client.execute({
2045
+ sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
2046
+ args: [taskId]
2047
+ });
2048
+ const cur = current.rows[0];
2049
+ const status = cur?.status ?? "unknown";
2050
+ const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2051
+ throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
1598
2052
  }
1599
2053
  try {
1600
- const pane = transport.capturePane(sessionName);
1601
- if (useExeAgent) {
1602
- if (pane.includes("[exe-agent]") || pane.includes("online")) {
1603
- booted = true;
1604
- break;
1605
- }
1606
- } else {
1607
- if (pane.includes("Claude Code") || pane.includes("\u276F")) {
1608
- booted = true;
1609
- break;
1610
- }
1611
- }
2054
+ await writeCheckpoint({
2055
+ taskId,
2056
+ step: "claimed",
2057
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
2058
+ });
1612
2059
  } catch {
1613
2060
  }
2061
+ return { row, taskFile, now, taskId };
1614
2062
  }
1615
- if (!booted) {
1616
- releaseSpawnLock(sessionName);
1617
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
2063
+ if (input.result) {
2064
+ await client.execute({
2065
+ sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
2066
+ args: [input.status, input.result, now, taskId]
2067
+ });
2068
+ } else {
2069
+ await client.execute({
2070
+ sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
2071
+ args: [input.status, now, taskId]
2072
+ });
1618
2073
  }
1619
- if (!useExeAgent) {
1620
- try {
1621
- transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
1622
- } catch {
2074
+ try {
2075
+ await writeCheckpoint({
2076
+ taskId,
2077
+ step: `status_transition:${input.status}`,
2078
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
2079
+ });
2080
+ } catch {
2081
+ }
2082
+ return { row, taskFile, now, taskId };
2083
+ }
2084
+ async function deleteTaskCore(taskId, _baseDir) {
2085
+ const client = getClient();
2086
+ const row = await resolveTask(client, taskId);
2087
+ const id = String(row.id);
2088
+ const taskFile = String(row.task_file);
2089
+ const assignedTo = String(row.assigned_to);
2090
+ const assignedBy = String(row.assigned_by);
2091
+ await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
2092
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
2093
+ return { taskFile, assignedTo, assignedBy, taskSlug };
2094
+ }
2095
+ async function ensureArchitectureDoc(baseDir, projectName) {
2096
+ const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
2097
+ try {
2098
+ if (existsSync9(archPath)) return;
2099
+ const template = [
2100
+ `# ${projectName} \u2014 System Architecture`,
2101
+ "",
2102
+ "> Employees: read this before every task. Update it when you change system structure.",
2103
+ `> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
2104
+ "",
2105
+ "## Overview",
2106
+ "",
2107
+ "<!-- Describe what this system does, its main components, and how they connect. -->",
2108
+ "",
2109
+ "## Key Components",
2110
+ "",
2111
+ "<!-- List the major modules, services, or subsystems. -->",
2112
+ "",
2113
+ "## Data Flow",
2114
+ "",
2115
+ "<!-- How does data move through the system? What writes where? -->",
2116
+ "",
2117
+ "## Invariants",
2118
+ "",
2119
+ "<!-- Rules that must never be violated. What breaks if these are wrong? -->",
2120
+ "",
2121
+ "## Dependencies",
2122
+ "",
2123
+ "<!-- What depends on what? If I change X, what else is affected? -->",
2124
+ ""
2125
+ ].join("\n");
2126
+ await writeFile3(archPath, template, "utf-8");
2127
+ } catch {
2128
+ }
2129
+ }
2130
+ async function ensureGitignoreExe(baseDir) {
2131
+ const gitignorePath = path9.join(baseDir, ".gitignore");
2132
+ try {
2133
+ if (existsSync9(gitignorePath)) {
2134
+ const content = readFileSync9(gitignorePath, "utf-8");
2135
+ if (/^\/?exe\/?$/m.test(content)) return;
2136
+ await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2137
+ } else {
2138
+ await writeFile3(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
1623
2139
  }
2140
+ } catch {
1624
2141
  }
1625
- registerSession({
1626
- windowName: sessionName,
1627
- agentId: employeeName,
1628
- projectDir: spawnCwd,
1629
- parentExe: exeSession,
1630
- pid: 0,
1631
- registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1632
- });
1633
- releaseSpawnLock(sessionName);
1634
- return { sessionName };
1635
2142
  }
1636
- var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1637
- var init_tmux_routing = __esm({
1638
- "src/lib/tmux-routing.ts"() {
2143
+ var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2144
+ var init_tasks_crud = __esm({
2145
+ "src/lib/tasks-crud.ts"() {
1639
2146
  "use strict";
1640
- init_session_registry();
1641
- init_session_key();
1642
- init_transport();
1643
- init_cc_agent_support();
1644
- init_mcp_prefix();
1645
- init_provider_table();
1646
- init_intercom_queue();
1647
- init_plan_limits();
1648
- SPAWN_LOCK_DIR = path9.join(os5.homedir(), ".exe-os", "spawn-locks");
1649
- SESSION_CACHE = path9.join(os5.homedir(), ".exe-os", "session-cache");
1650
- BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1651
- INTERCOM_DEBOUNCE_MS = 3e4;
1652
- INTERCOM_LOG2 = path9.join(os5.homedir(), ".exe-os", "intercom.log");
1653
- DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
1654
- DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
1655
- BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
2147
+ init_database();
2148
+ DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
2149
+ TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
1656
2150
  }
1657
2151
  });
1658
2152
 
@@ -1827,6 +2321,7 @@ var init_tasks_review = __esm({
1827
2321
  init_tasks_crud();
1828
2322
  init_tmux_routing();
1829
2323
  init_session_key();
2324
+ init_state_bus();
1830
2325
  }
1831
2326
  });
1832
2327
 
@@ -1991,13 +2486,12 @@ function assertSessionScope(actionType, targetProject) {
1991
2486
  };
1992
2487
  }
1993
2488
  process.stderr.write(
1994
- `[session-scope] Cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
2489
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
1995
2490
  `
1996
2491
  );
1997
2492
  return {
1998
- allowed: true,
1999
- // v1: warn-only, don't block
2000
- reason: "cross_session_granted",
2493
+ allowed: false,
2494
+ reason: "cross_session_denied",
2001
2495
  currentProject,
2002
2496
  targetProject,
2003
2497
  targetSession: findSessionForProject(targetProject)?.windowName
@@ -2023,8 +2517,9 @@ async function dispatchTaskToEmployee(input) {
2023
2517
  try {
2024
2518
  const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
2025
2519
  const check = assertSessionScope2("dispatch_task", input.projectName);
2026
- if (check.reason === "cross_session_granted") {
2520
+ if (check.reason === "cross_session_denied") {
2027
2521
  crossProject = true;
2522
+ return { dispatched: "skipped", crossProject: true };
2028
2523
  }
2029
2524
  } catch {
2030
2525
  }
@@ -2081,10 +2576,10 @@ var init_tasks_notify = __esm({
2081
2576
  });
2082
2577
 
2083
2578
  // src/lib/behaviors.ts
2084
- import crypto3 from "crypto";
2579
+ import crypto4 from "crypto";
2085
2580
  async function storeBehavior(opts) {
2086
2581
  const client = getClient();
2087
- const id = crypto3.randomUUID();
2582
+ const id = crypto4.randomUUID();
2088
2583
  const now = (/* @__PURE__ */ new Date()).toISOString();
2089
2584
  await client.execute({
2090
2585
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -2113,7 +2608,7 @@ __export(skill_learning_exports, {
2113
2608
  storeTrajectory: () => storeTrajectory,
2114
2609
  sweepTrajectories: () => sweepTrajectories
2115
2610
  });
2116
- import crypto4 from "crypto";
2611
+ import crypto5 from "crypto";
2117
2612
  async function extractTrajectory(taskId, agentId) {
2118
2613
  const client = getClient();
2119
2614
  const result = await client.execute({
@@ -2142,11 +2637,11 @@ async function extractTrajectory(taskId, agentId) {
2142
2637
  return signature;
2143
2638
  }
2144
2639
  function hashSignature(signature) {
2145
- return crypto4.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
2640
+ return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
2146
2641
  }
2147
2642
  async function storeTrajectory(opts) {
2148
2643
  const client = getClient();
2149
- const id = crypto4.randomUUID();
2644
+ const id = crypto5.randomUUID();
2150
2645
  const now = (/* @__PURE__ */ new Date()).toISOString();
2151
2646
  const signatureHash = hashSignature(opts.signature);
2152
2647
  await client.execute({
@@ -2391,6 +2886,26 @@ var init_skill_learning = __esm({
2391
2886
  });
2392
2887
 
2393
2888
  // src/lib/tasks.ts
2889
+ var tasks_exports = {};
2890
+ __export(tasks_exports, {
2891
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
2892
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
2893
+ countPendingReviews: () => countPendingReviews,
2894
+ createTask: () => createTask,
2895
+ createTaskCore: () => createTaskCore,
2896
+ deleteTask: () => deleteTask,
2897
+ deleteTaskCore: () => deleteTaskCore,
2898
+ ensureArchitectureDoc: () => ensureArchitectureDoc,
2899
+ ensureGitignoreExe: () => ensureGitignoreExe,
2900
+ getReviewChecklist: () => getReviewChecklist,
2901
+ listPendingReviews: () => listPendingReviews,
2902
+ listTasks: () => listTasks,
2903
+ resolveTask: () => resolveTask,
2904
+ slugify: () => slugify,
2905
+ updateTask: () => updateTask,
2906
+ updateTaskStatus: () => updateTaskStatus,
2907
+ writeCheckpoint: () => writeCheckpoint
2908
+ });
2394
2909
  import path13 from "path";
2395
2910
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
2396
2911
  async function createTask(input) {
@@ -2467,6 +2982,13 @@ async function updateTask(input) {
2467
2982
  await cascadeUnblock(taskId, input.baseDir, now);
2468
2983
  } catch {
2469
2984
  }
2985
+ orgBus.emit({
2986
+ type: "task_completed",
2987
+ taskId,
2988
+ employee: String(row.assigned_to),
2989
+ result: input.result ?? "",
2990
+ timestamp: now
2991
+ });
2470
2992
  if (row.parent_task_id) {
2471
2993
  try {
2472
2994
  await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
@@ -2533,6 +3055,7 @@ var init_tasks = __esm({
2533
3055
  init_database();
2534
3056
  init_config();
2535
3057
  init_notifications();
3058
+ init_state_bus();
2536
3059
  init_tasks_crud();
2537
3060
  init_tasks_review();
2538
3061
  init_tasks_crud();