@askexenow/exe-os 0.8.40 → 0.8.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/bin/backfill-conversations.js +805 -642
  2. package/dist/bin/backfill-responses.js +804 -641
  3. package/dist/bin/backfill-vectors.js +791 -634
  4. package/dist/bin/cleanup-stale-review-tasks.js +788 -631
  5. package/dist/bin/cli.js +1376 -659
  6. package/dist/bin/exe-agent.js +20 -1
  7. package/dist/bin/exe-assign.js +1503 -1343
  8. package/dist/bin/exe-boot.js +2549 -1784
  9. package/dist/bin/exe-call.js +39 -1
  10. package/dist/bin/exe-cloud.js +12 -2
  11. package/dist/bin/exe-dispatch.js +39 -2
  12. package/dist/bin/exe-doctor.js +791 -634
  13. package/dist/bin/exe-export-behaviors.js +792 -637
  14. package/dist/bin/exe-forget.js +145 -0
  15. package/dist/bin/exe-gateway.js +2501 -1846
  16. package/dist/bin/exe-heartbeat.js +147 -1
  17. package/dist/bin/exe-kill.js +795 -640
  18. package/dist/bin/exe-launch-agent.js +2168 -2008
  19. package/dist/bin/exe-link.js +44 -12
  20. package/dist/bin/exe-new-employee.js +6 -2
  21. package/dist/bin/exe-pending-messages.js +146 -1
  22. package/dist/bin/exe-pending-notifications.js +788 -631
  23. package/dist/bin/exe-pending-reviews.js +176 -1
  24. package/dist/bin/exe-rename.js +23 -0
  25. package/dist/bin/exe-review.js +490 -327
  26. package/dist/bin/exe-search.js +157 -4
  27. package/dist/bin/exe-session-cleanup.js +2487 -403
  28. package/dist/bin/exe-settings.js +2 -1
  29. package/dist/bin/exe-status.js +474 -317
  30. package/dist/bin/exe-team.js +474 -317
  31. package/dist/bin/git-sweep.js +2691 -151
  32. package/dist/bin/graph-backfill.js +794 -637
  33. package/dist/bin/graph-export.js +798 -641
  34. package/dist/bin/scan-tasks.js +2951 -44
  35. package/dist/bin/setup.js +50 -26
  36. package/dist/bin/shard-migrate.js +792 -637
  37. package/dist/bin/wiki-sync.js +794 -637
  38. package/dist/gateway/index.js +2542 -1887
  39. package/dist/hooks/bug-report-worker.js +2118 -576
  40. package/dist/hooks/commit-complete.js +2690 -150
  41. package/dist/hooks/error-recall.js +157 -4
  42. package/dist/hooks/ingest-worker.js +1455 -803
  43. package/dist/hooks/instructions-loaded.js +151 -0
  44. package/dist/hooks/notification.js +153 -2
  45. package/dist/hooks/post-compact.js +164 -0
  46. package/dist/hooks/pre-compact.js +3073 -101
  47. package/dist/hooks/pre-tool-use.js +151 -0
  48. package/dist/hooks/prompt-ingest-worker.js +1670 -1509
  49. package/dist/hooks/prompt-submit.js +2650 -1074
  50. package/dist/hooks/response-ingest-worker.js +154 -6
  51. package/dist/hooks/session-end.js +153 -2
  52. package/dist/hooks/session-start.js +157 -4
  53. package/dist/hooks/stop.js +151 -0
  54. package/dist/hooks/subagent-stop.js +155 -2
  55. package/dist/hooks/summary-worker.js +190 -21
  56. package/dist/index.js +326 -102
  57. package/dist/lib/cloud-sync.js +31 -10
  58. package/dist/lib/config.js +2 -0
  59. package/dist/lib/consolidation.js +69 -2
  60. package/dist/lib/database.js +19 -0
  61. package/dist/lib/device-registry.js +19 -0
  62. package/dist/lib/embedder.js +3 -1
  63. package/dist/lib/employee-templates.js +20 -1
  64. package/dist/lib/exe-daemon.js +285 -18
  65. package/dist/lib/hybrid-search.js +157 -4
  66. package/dist/lib/messaging.js +39 -2
  67. package/dist/lib/schedules.js +792 -637
  68. package/dist/lib/store.js +796 -636
  69. package/dist/lib/tasks.js +1485 -918
  70. package/dist/lib/tmux-routing.js +194 -10
  71. package/dist/mcp/server.js +1643 -924
  72. package/dist/mcp/tools/create-task.js +2283 -829
  73. package/dist/mcp/tools/list-tasks.js +2788 -159
  74. package/dist/mcp/tools/send-message.js +39 -2
  75. package/dist/mcp/tools/update-task.js +79 -0
  76. package/dist/runtime/index.js +280 -68
  77. package/dist/tui/App.js +1485 -645
  78. package/package.json +3 -2
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`,
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();
@@ -894,14 +516,14 @@ var init_transport = __esm({
894
516
  });
895
517
 
896
518
  // src/lib/cc-agent-support.ts
897
- import { execSync as execSync4 } from "child_process";
519
+ import { execSync as execSync2 } from "child_process";
898
520
  function _resetCcAgentSupportCache() {
899
521
  _cachedSupport = null;
900
522
  }
901
523
  function claudeSupportsAgentFlag() {
902
524
  if (_cachedSupport !== null) return _cachedSupport;
903
525
  try {
904
- const helpOutput = execSync4("claude --help 2>&1", {
526
+ const helpOutput = execSync2("claude --help 2>&1", {
905
527
  encoding: "utf-8",
906
528
  timeout: 5e3
907
529
  });
@@ -967,17 +589,17 @@ var init_provider_table = __esm({
967
589
  });
968
590
 
969
591
  // 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";
592
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
593
+ import path4 from "path";
972
594
  import os4 from "os";
973
595
  function ensureDir() {
974
- const dir = path6.dirname(QUEUE_PATH);
975
- if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
596
+ const dir = path4.dirname(QUEUE_PATH);
597
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
976
598
  }
977
599
  function readQueue() {
978
600
  try {
979
- if (!existsSync6(QUEUE_PATH)) return [];
980
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
601
+ if (!existsSync4(QUEUE_PATH)) return [];
602
+ return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
981
603
  } catch {
982
604
  return [];
983
605
  }
@@ -1009,25 +631,57 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
1009
631
  var init_intercom_queue = __esm({
1010
632
  "src/lib/intercom-queue.ts"() {
1011
633
  "use strict";
1012
- QUEUE_PATH = path6.join(os4.homedir(), ".exe-os", "intercom-queue.json");
634
+ QUEUE_PATH = path4.join(os4.homedir(), ".exe-os", "intercom-queue.json");
1013
635
  TTL_MS = 60 * 60 * 1e3;
1014
- INTERCOM_LOG = path6.join(os4.homedir(), ".exe-os", "intercom.log");
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"]);
1015
669
  }
1016
670
  });
1017
671
 
1018
672
  // src/lib/license.ts
1019
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
673
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
1020
674
  import { randomUUID } from "crypto";
1021
- import path7 from "path";
675
+ import path6 from "path";
1022
676
  import { jwtVerify, importSPKI } from "jose";
1023
677
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1024
678
  var init_license = __esm({
1025
679
  "src/lib/license.ts"() {
1026
680
  "use strict";
1027
681
  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");
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");
1031
685
  PLAN_LIMITS = {
1032
686
  free: { devices: 1, employees: 1, memories: 5e3 },
1033
687
  pro: { devices: 2, employees: 5, memories: 1e5 },
@@ -1039,12 +693,12 @@ var init_license = __esm({
1039
693
  });
1040
694
 
1041
695
  // src/lib/plan-limits.ts
1042
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
1043
- import path8 from "path";
696
+ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
697
+ import path7 from "path";
1044
698
  function getLicenseSync() {
1045
699
  try {
1046
- if (!existsSync8(CACHE_PATH2)) return freeLicense();
1047
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
700
+ if (!existsSync7(CACHE_PATH2)) return freeLicense();
701
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
1048
702
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
1049
703
  const parts = raw.token.split(".");
1050
704
  if (parts.length !== 3) return freeLicense();
@@ -1082,8 +736,8 @@ function assertEmployeeLimitSync(rosterPath) {
1082
736
  const filePath = rosterPath ?? EMPLOYEES_PATH;
1083
737
  let count = 0;
1084
738
  try {
1085
- if (existsSync8(filePath)) {
1086
- const raw = readFileSync8(filePath, "utf8");
739
+ if (existsSync7(filePath)) {
740
+ const raw = readFileSync7(filePath, "utf8");
1087
741
  const employees = JSON.parse(raw);
1088
742
  count = Array.isArray(employees) ? employees.length : 0;
1089
743
  }
@@ -1098,33 +752,370 @@ function assertEmployeeLimitSync(rosterPath) {
1098
752
  );
1099
753
  }
1100
754
  }
1101
- var PlanLimitError, CACHE_PATH2;
1102
- var init_plan_limits = __esm({
1103
- "src/lib/plan-limits.ts"() {
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
+ );
798
+ }
799
+ }
800
+ var init_session_kill_telemetry = __esm({
801
+ "src/lib/session-kill-telemetry.ts"() {
802
+ "use strict";
803
+ init_database();
804
+ }
805
+ });
806
+
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;
874
+ }
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]
921
+ });
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();
946
+ } catch {
947
+ return [];
948
+ }
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;
1041
+ }
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"() {
1104
1045
  "use strict";
1046
+ init_session_registry();
1047
+ init_transport();
1048
+ init_notifications();
1105
1049
  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";
1113
- }
1114
- };
1115
- CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
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;
1116
1084
  }
1117
1085
  });
1118
1086
 
1119
1087
  // 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";
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";
1123
1114
  import os5 from "os";
1124
1115
  import { fileURLToPath } from "url";
1125
1116
  import { unlinkSync as unlinkSync2 } from "fs";
1126
1117
  function spawnLockPath(sessionName) {
1127
- return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1118
+ return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1128
1119
  }
1129
1120
  function isProcessAlive(pid) {
1130
1121
  try {
@@ -1135,13 +1126,13 @@ function isProcessAlive(pid) {
1135
1126
  }
1136
1127
  }
1137
1128
  function acquireSpawnLock(sessionName) {
1138
- if (!existsSync9(SPAWN_LOCK_DIR)) {
1129
+ if (!existsSync8(SPAWN_LOCK_DIR)) {
1139
1130
  mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
1140
1131
  }
1141
1132
  const lockFile = spawnLockPath(sessionName);
1142
- if (existsSync9(lockFile)) {
1133
+ if (existsSync8(lockFile)) {
1143
1134
  try {
1144
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1135
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1145
1136
  const age = Date.now() - lock.timestamp;
1146
1137
  if (isProcessAlive(lock.pid) && age < 6e4) {
1147
1138
  return false;
@@ -1149,510 +1140,1013 @@ function acquireSpawnLock(sessionName) {
1149
1140
  } catch {
1150
1141
  }
1151
1142
  }
1152
- writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1143
+ writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1144
+ return true;
1145
+ }
1146
+ function releaseSpawnLock(sessionName) {
1147
+ try {
1148
+ unlinkSync2(spawnLockPath(sessionName));
1149
+ } catch {
1150
+ }
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 });
1224
+ }
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
+ }));
1232
+ }
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;
1239
+ }
1240
+ }
1241
+ function getDispatchedBy(sessionKey) {
1242
+ try {
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;
1248
+ } catch {
1249
+ return null;
1250
+ }
1251
+ }
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;
1264
+ }
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;
1274
+ }
1275
+ return null;
1276
+ }
1277
+ async function verifyPaneAtCapacity(sessionName) {
1278
+ const transport = getTransport();
1279
+ if (!transport.isAlive(sessionName)) {
1280
+ return { atCapacity: false, reason: `session ${sessionName} is not alive` };
1281
+ }
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)}`
1289
+ };
1290
+ }
1291
+ const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
1292
+ if (!isAtCapacity2(pane)) {
1293
+ return {
1294
+ atCapacity: false,
1295
+ reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
1296
+ };
1297
+ }
1298
+ return {
1299
+ atCapacity: true,
1300
+ reason: "capacity banner matched in recent pane output"
1301
+ };
1302
+ }
1303
+ function readDebounceState() {
1304
+ try {
1305
+ if (!existsSync8(DEBOUNCE_FILE)) return {};
1306
+ return JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1307
+ } catch {
1308
+ return {};
1309
+ }
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 {
1316
+ }
1317
+ }
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";
1350
+ }
1351
+ }
1352
+ if (/Running…/.test(pane)) return "tool";
1353
+ if (BUSY_PATTERN.test(pane)) return "thinking";
1354
+ return "idle";
1355
+ } catch {
1356
+ return "offline";
1357
+ }
1358
+ }
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
+ }
1376
+ try {
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";
1403
+ } catch {
1404
+ logIntercom(`FAIL \u2192 ${targetSession}`);
1405
+ return "failed";
1406
+ }
1407
+ }
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;
1414
+ }
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";
1425
+ }
1426
+ return false;
1427
+ }
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";
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 {
1329
1673
  }
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";
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 {
1335
1688
  }
1336
- if (transport.isPaneInCopyMode(targetSession)) {
1337
- logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
1338
- transport.sendKeys(targetSession, "q");
1689
+ }
1690
+ if (!booted) {
1691
+ releaseSpawnLock(sessionName);
1692
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
1693
+ }
1694
+ if (!useExeAgent) {
1695
+ try {
1696
+ transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
1697
+ } catch {
1339
1698
  }
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
1699
  }
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 };
1348
1710
  }
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;
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…/;
1355
1733
  }
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";
1366
- }
1367
- return false;
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));
1368
1750
  }
1369
- return true;
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`);
1764
+ }
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 };
1370
1771
  }
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" };
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.`
1806
+ );
1807
+ }
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
+ );
1822
+ }
1823
+ throw new Error(`Task not found: ${identifier}`);
1824
+ }
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);
1374
1836
  }
1375
- try {
1376
- assertEmployeeLimitSync();
1377
- } catch (err) {
1378
- if (err instanceof PlanLimitError) {
1379
- return { status: "failed", sessionName: "", error: err.message };
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
+ );
1380
1846
  }
1381
1847
  }
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
- };
1389
- }
1390
- let effectiveInstance = opts?.instance;
1391
- if (effectiveInstance === void 0 && opts?.autoInstance) {
1392
- const free = findFreeInstance(
1393
- employeeName,
1394
- exeSession,
1395
- opts.maxAutoInstances ?? 10
1396
- );
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
- };
1848
+ if (parentRef) {
1849
+ try {
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;
1403
1859
  }
1404
- effectiveInstance = free === 0 ? void 0 : free;
1405
1860
  }
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 };
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) {
1870
+ try {
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);
1875
+ } catch {
1414
1876
  }
1415
- return { status: "failed", sessionName, error: "intercom delivery failed" };
1416
1877
  }
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 };
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();
1883
+ } catch {
1421
1884
  }
1422
- return { status: "spawned", sessionName };
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
+ };
1423
1928
  }
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 });
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);
1432
1936
  }
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}"`;
1440
- }
1441
- } catch {
1937
+ if (input.status) {
1938
+ conditions.push("status = ?");
1939
+ args.push(input.status);
1940
+ } else {
1941
+ conditions.push("status IN ('open', 'in_progress', 'blocked')");
1442
1942
  }
1443
- try {
1444
- const claudeJsonPath = path9.join(os5.homedir(), ".claude.json");
1445
- let claudeJson = {};
1446
- try {
1447
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
1448
- } catch {
1449
- }
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 {
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);
1457
1950
  }
1458
1951
  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 = {};
1464
- try {
1465
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
1466
- } catch {
1467
- }
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");
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);
1495
1957
  }
1496
1958
  } catch {
1497
1959
  }
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}`;
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.";
1527
2000
  }
2001
+ return null;
2002
+ } catch {
2003
+ return null;
1528
2004
  }
1529
- if (legacyFallbackWarned) {
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-")) {
1530
2013
  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.
2014
+ `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
1532
2015
  `
1533
2016
  );
1534
2017
  }
1535
- let sessionContextFlag = "";
1536
- 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}`;
1548
- } catch {
1549
- }
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}`;
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
+ `);
1557
2032
  }
1558
2033
  }
1559
2034
  }
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}`;
1565
- process.stderr.write(
1566
- `[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
1567
- `
1568
- );
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
- }
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 {
1592
- }
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
 
@@ -1686,6 +2180,34 @@ async function listPendingReviews(limit) {
1686
2180
  });
1687
2181
  return result.rows;
1688
2182
  }
2183
+ async function cleanupOrphanedReviews() {
2184
+ const client = getClient();
2185
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2186
+ const r1 = await client.execute({
2187
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
2188
+ WHERE status = 'needs_review'
2189
+ AND assigned_by = 'system'
2190
+ AND title LIKE 'Review:%'
2191
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
2192
+ args: [now]
2193
+ });
2194
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
2195
+ const r2 = await client.execute({
2196
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
2197
+ WHERE status = 'needs_review'
2198
+ AND result IS NOT NULL
2199
+ AND updated_at < ?`,
2200
+ args: [now, staleThreshold]
2201
+ });
2202
+ const total = r1.rowsAffected + r2.rowsAffected;
2203
+ if (total > 0) {
2204
+ process.stderr.write(
2205
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
2206
+ `
2207
+ );
2208
+ }
2209
+ return total;
2210
+ }
1689
2211
  function getReviewChecklist(role, agent, taskSlug) {
1690
2212
  const roleLower = role.toLowerCase();
1691
2213
  if (roleLower.includes("engineer") || roleLower === "principal engineer") {
@@ -1799,6 +2321,7 @@ var init_tasks_review = __esm({
1799
2321
  init_tasks_crud();
1800
2322
  init_tmux_routing();
1801
2323
  init_session_key();
2324
+ init_state_bus();
1802
2325
  }
1803
2326
  });
1804
2327
 
@@ -1963,13 +2486,12 @@ function assertSessionScope(actionType, targetProject) {
1963
2486
  };
1964
2487
  }
1965
2488
  process.stderr.write(
1966
- `[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}"
1967
2490
  `
1968
2491
  );
1969
2492
  return {
1970
- allowed: true,
1971
- // v1: warn-only, don't block
1972
- reason: "cross_session_granted",
2493
+ allowed: false,
2494
+ reason: "cross_session_denied",
1973
2495
  currentProject,
1974
2496
  targetProject,
1975
2497
  targetSession: findSessionForProject(targetProject)?.windowName
@@ -1995,8 +2517,9 @@ async function dispatchTaskToEmployee(input) {
1995
2517
  try {
1996
2518
  const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
1997
2519
  const check = assertSessionScope2("dispatch_task", input.projectName);
1998
- if (check.reason === "cross_session_granted") {
2520
+ if (check.reason === "cross_session_denied") {
1999
2521
  crossProject = true;
2522
+ return { dispatched: "skipped", crossProject: true };
2000
2523
  }
2001
2524
  } catch {
2002
2525
  }
@@ -2053,10 +2576,10 @@ var init_tasks_notify = __esm({
2053
2576
  });
2054
2577
 
2055
2578
  // src/lib/behaviors.ts
2056
- import crypto3 from "crypto";
2579
+ import crypto4 from "crypto";
2057
2580
  async function storeBehavior(opts) {
2058
2581
  const client = getClient();
2059
- const id = crypto3.randomUUID();
2582
+ const id = crypto4.randomUUID();
2060
2583
  const now = (/* @__PURE__ */ new Date()).toISOString();
2061
2584
  await client.execute({
2062
2585
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -2085,7 +2608,7 @@ __export(skill_learning_exports, {
2085
2608
  storeTrajectory: () => storeTrajectory,
2086
2609
  sweepTrajectories: () => sweepTrajectories
2087
2610
  });
2088
- import crypto4 from "crypto";
2611
+ import crypto5 from "crypto";
2089
2612
  async function extractTrajectory(taskId, agentId) {
2090
2613
  const client = getClient();
2091
2614
  const result = await client.execute({
@@ -2114,11 +2637,11 @@ async function extractTrajectory(taskId, agentId) {
2114
2637
  return signature;
2115
2638
  }
2116
2639
  function hashSignature(signature) {
2117
- 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);
2118
2641
  }
2119
2642
  async function storeTrajectory(opts) {
2120
2643
  const client = getClient();
2121
- const id = crypto4.randomUUID();
2644
+ const id = crypto5.randomUUID();
2122
2645
  const now = (/* @__PURE__ */ new Date()).toISOString();
2123
2646
  const signatureHash = hashSignature(opts.signature);
2124
2647
  await client.execute({
@@ -2363,6 +2886,26 @@ var init_skill_learning = __esm({
2363
2886
  });
2364
2887
 
2365
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
+ });
2366
2909
  import path13 from "path";
2367
2910
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
2368
2911
  async function createTask(input) {
@@ -2411,6 +2954,21 @@ async function updateTask(input) {
2411
2954
  });
2412
2955
  } catch {
2413
2956
  }
2957
+ try {
2958
+ const client = getClient();
2959
+ const cascaded = await client.execute({
2960
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
2961
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
2962
+ args: [now, taskId]
2963
+ });
2964
+ if (cascaded.rowsAffected > 0) {
2965
+ process.stderr.write(
2966
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
2967
+ `
2968
+ );
2969
+ }
2970
+ } catch {
2971
+ }
2414
2972
  }
2415
2973
  const isTerminal = input.status === "done" || input.status === "needs_review";
2416
2974
  if (isTerminal) {
@@ -2424,6 +2982,13 @@ async function updateTask(input) {
2424
2982
  await cascadeUnblock(taskId, input.baseDir, now);
2425
2983
  } catch {
2426
2984
  }
2985
+ orgBus.emit({
2986
+ type: "task_completed",
2987
+ taskId,
2988
+ employee: String(row.assigned_to),
2989
+ result: input.result ?? "",
2990
+ timestamp: now
2991
+ });
2427
2992
  if (row.parent_task_id) {
2428
2993
  try {
2429
2994
  await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
@@ -2490,6 +3055,7 @@ var init_tasks = __esm({
2490
3055
  init_database();
2491
3056
  init_config();
2492
3057
  init_notifications();
3058
+ init_state_bus();
2493
3059
  init_tasks_crud();
2494
3060
  init_tasks_review();
2495
3061
  init_tasks_crud();
@@ -2500,6 +3066,7 @@ var init_tasks = __esm({
2500
3066
  });
2501
3067
  init_tasks();
2502
3068
  export {
3069
+ cleanupOrphanedReviews,
2503
3070
  countNewPendingReviewsSince,
2504
3071
  countPendingReviews,
2505
3072
  createTask,