bosun 0.42.2 → 0.42.4

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 (49) hide show
  1. package/.env.example +9 -0
  2. package/agent/agent-event-bus.mjs +10 -0
  3. package/agent/agent-supervisor.mjs +20 -0
  4. package/bosun-tui.mjs +107 -105
  5. package/cli.mjs +10 -0
  6. package/config/config.mjs +25 -0
  7. package/config/executor-config.mjs +124 -1
  8. package/infra/container-runner.mjs +565 -1
  9. package/infra/monitor.mjs +18 -0
  10. package/infra/tracing.mjs +544 -240
  11. package/infra/tui-bridge.mjs +13 -1
  12. package/kanban/kanban-adapter.mjs +128 -4
  13. package/lib/repo-map.mjs +114 -3
  14. package/package.json +11 -4
  15. package/server/ui-server.mjs +3 -0
  16. package/task/task-archiver.mjs +18 -6
  17. package/task/task-attachments.mjs +14 -10
  18. package/task/task-cli.mjs +24 -4
  19. package/task/task-executor.mjs +19 -0
  20. package/task/task-store.mjs +194 -37
  21. package/telegram/telegram-bot.mjs +4 -1
  22. package/tui/app.mjs +131 -171
  23. package/tui/components/status-header.mjs +178 -75
  24. package/tui/lib/header-config.mjs +68 -0
  25. package/tui/lib/ws-bridge.mjs +61 -9
  26. package/tui/screens/agents.mjs +127 -0
  27. package/tui/screens/tasks.mjs +1 -48
  28. package/ui/app.js +8 -5
  29. package/ui/components/kanban-board.js +65 -3
  30. package/ui/components/session-list.js +18 -32
  31. package/ui/demo-defaults.js +52 -2
  32. package/ui/modules/session-api.js +100 -0
  33. package/ui/modules/state.js +71 -15
  34. package/ui/tabs/workflows.js +25 -1
  35. package/ui/tui/App.js +298 -0
  36. package/ui/tui/TasksScreen.js +564 -0
  37. package/ui/tui/constants.js +55 -0
  38. package/ui/tui/tasks-screen-helpers.js +301 -0
  39. package/ui/tui/useTasks.js +61 -0
  40. package/ui/tui/useWebSocket.js +166 -0
  41. package/ui/tui/useWorkflows.js +30 -0
  42. package/workflow/workflow-engine.mjs +412 -7
  43. package/workflow/workflow-nodes.mjs +616 -75
  44. package/workflow-templates/agents.mjs +3 -0
  45. package/workflow-templates/planning.mjs +7 -0
  46. package/workflow-templates/sub-workflows.mjs +5 -0
  47. package/workflow-templates/task-execution.mjs +3 -0
  48. package/workspace/command-diagnostics.mjs +1 -1
  49. package/workspace/context-cache.mjs +182 -9
@@ -534,6 +534,8 @@ export const BACKEND_AGENT_TEMPLATE = (() => {
534
534
  node("plan-work", "agent.run_planner", "Plan Implementation", {
535
535
  prompt: "Analyze the task requirements and create a step-by-step implementation plan. Identify which files need to be modified, what tests need to be written, and any API contracts to maintain.",
536
536
  outputVariable: "plan",
537
+ repoMapQuery: "{{taskTitle}} {{taskDescription}}",
538
+ repoMapFileLimit: 8,
537
539
  }, { x: 400, y: 180 }),
538
540
 
539
541
  node("write-tests", "action.run_agent", "Write Tests First", {
@@ -1045,3 +1047,4 @@ export const MEETING_SUBWORKFLOW_CHAIN_TEMPLATE = {
1045
1047
  requiredTemplates: ["template-task-planner"],
1046
1048
  },
1047
1049
  };
1050
+
@@ -64,6 +64,8 @@ export const TASK_PLANNER_TEMPLATE = {
64
64
  taskCount: "{{taskCount}}",
65
65
  context: "{{plannerContext}}",
66
66
  prompt: "{{prompt}}",
67
+ repoMapQuery: "{{plannerContext}} {{prompt}}",
68
+ repoMapFileLimit: 8,
67
69
  dedup: true,
68
70
  timeoutMs: 960000,
69
71
  agentTimeoutMs: 900000,
@@ -182,6 +184,8 @@ export const TASK_REPLENISH_TEMPLATE = {
182
184
  taskCount: "{{taskCount}}",
183
185
  context: "{{plannerContext}}",
184
186
  prompt: "{{prompt}}",
187
+ repoMapQuery: "{{plannerContext}} {{prompt}}",
188
+ repoMapFileLimit: 8,
185
189
  timeoutMs: 960000,
186
190
  agentTimeoutMs: 900000,
187
191
  maxRetries: 0,
@@ -988,3 +992,6 @@ Rules:
988
992
  },
989
993
  },
990
994
  };
995
+
996
+
997
+
@@ -143,6 +143,7 @@ export function makeAgentPlanExecuteVerifySub(opts) {
143
143
  mode: "plan",
144
144
  executionRole: "architect",
145
145
  repoMapQuery: "{{taskTitle}} {{taskDescription}}",
146
+ repoMapFileLimit: 8,
146
147
  ...defaults,
147
148
  }, { x: 400, y: 0 }),
148
149
  node("implement", "action.run_agent", "Implement", {
@@ -150,6 +151,7 @@ export function makeAgentPlanExecuteVerifySub(opts) {
150
151
  executionRole: "editor",
151
152
  architectPlan: "{{$ctx.getNodeOutput('plan')?.summary || $ctx.getNodeOutput('plan')?.output || ''}}",
152
153
  repoMapQuery: "{{taskTitle}} {{taskDescription}}",
154
+ repoMapFileLimit: 8,
153
155
  ...defaults,
154
156
  }, { x: 400, y: 160 }),
155
157
  node("verify", "action.run_agent", "Verify", {
@@ -157,6 +159,7 @@ export function makeAgentPlanExecuteVerifySub(opts) {
157
159
  executionRole: "editor",
158
160
  architectPlan: "{{$ctx.getNodeOutput('plan')?.summary || $ctx.getNodeOutput('plan')?.output || ''}}",
159
161
  repoMapQuery: "{{taskTitle}} {{taskDescription}}",
162
+ repoMapFileLimit: 8,
160
163
  ...defaults,
161
164
  }, { x: 400, y: 320 }),
162
165
  ],
@@ -241,3 +244,5 @@ export const PR_CHECK_HANDOFF_SUB = subWorkflow(
241
244
  },
242
245
  );
243
246
 
247
+
248
+
@@ -39,6 +39,7 @@ function withArchitectEditorPhases(phases = []) {
39
39
  ...phase,
40
40
  extra: {
41
41
  repoMapQuery: "{{taskTitle}} {{taskDescription}}",
42
+ repoMapFileLimit: 8,
42
43
  ...(index === 0
43
44
  ? { mode: "plan", executionRole: "architect" }
44
45
  : { executionRole: "editor", architectPlan }),
@@ -422,3 +423,5 @@ Commit changes with descriptive messages.`,
422
423
  });
423
424
 
424
425
 
426
+
427
+
@@ -124,7 +124,7 @@ function normalizeCommandSignature(command = "", args = []) {
124
124
 
125
125
  function resolveCommandKind(commandLine = "", output = "") {
126
126
  const lower = `${commandLine}\n${output}`.toLowerCase();
127
- if (/\bdotnet\s+test\b/.test(lower)) return { family: "test", runner: "dotnet-test" };
127
+ if (/\bdotnet\s+test\b/.test(lower)) return { family: "build", runner: "dotnet-test" };
128
128
  if (/\b(?:python(?:3)?\s+-m\s+pytest|pytest)\b/.test(lower)) return { family: "test", runner: "pytest" };
129
129
  if (/\bvitest\b/.test(lower)) return { family: "test", runner: "vitest" };
130
130
  if (/\bjest\b/.test(lower)) return { family: "test", runner: "jest" };
@@ -515,7 +515,43 @@ function normalizeCommandToken(token) {
515
515
  }
516
516
 
517
517
  function tokenizeCommandLine(commandLine) {
518
- return String(commandLine || "").match(/(?:"[^"]*"|'[^']*'|\S+)/g) || [];
518
+ const input = String(commandLine || "");
519
+ const tokens = [];
520
+ let index = 0;
521
+
522
+ while (index < input.length) {
523
+ while (index < input.length) {
524
+ const code = input.charCodeAt(index);
525
+ if (code !== 9 && code !== 10 && code !== 11 && code !== 12 && code !== 13 && code !== 32) break;
526
+ index += 1;
527
+ }
528
+ if (index >= input.length) break;
529
+
530
+ const quote = input[index];
531
+ if (quote === '"' || quote === "'") {
532
+ const start = index;
533
+ index += 1;
534
+ while (index < input.length && input[index] !== quote) {
535
+ index += 1;
536
+ }
537
+ if (index < input.length) {
538
+ index += 1;
539
+ tokens.push(input.slice(start, index));
540
+ continue;
541
+ }
542
+ index = start;
543
+ }
544
+
545
+ const start = index;
546
+ while (index < input.length) {
547
+ const code = input.charCodeAt(index);
548
+ if (code === 9 || code === 10 || code === 11 || code === 12 || code === 13 || code === 32) break;
549
+ index += 1;
550
+ }
551
+ tokens.push(input.slice(start, index));
552
+ }
553
+
554
+ return tokens;
519
555
  }
520
556
 
521
557
  function extractNestedCommandToken(token) {
@@ -601,22 +637,120 @@ function isLikelyStructuredOutput(text, item) {
601
637
  );
602
638
  }
603
639
 
640
+ function hasSourceDiagnosticLine(text) {
641
+ const lines = String(text || "").split(/\r?\n/);
642
+ for (const rawLine of lines) {
643
+ const line = rawLine.trim();
644
+ if (!line) continue;
645
+
646
+ const severityMatch = /\b(?:error|warning)\b/i.exec(line);
647
+ if (!severityMatch) continue;
648
+
649
+ const prefix = line.slice(0, severityMatch.index).trimEnd();
650
+ if (!prefix) continue;
651
+
652
+ const colonIndex = prefix.indexOf(":");
653
+ const filePart = (colonIndex === -1 ? prefix : prefix.slice(0, colonIndex)).trim();
654
+ if (!/\.(?:cs|fs|vb|ts|tsx|js|jsx|java|kt|go|rs|cpp|c|h|hpp)$/i.test(filePart)) continue;
655
+
656
+ if (colonIndex !== -1) {
657
+ const location = prefix.slice(colonIndex + 1).trim();
658
+ if (location && !/^\d+(?::\d+)?$/.test(location)) continue;
659
+ }
660
+
661
+ return true;
662
+ }
663
+
664
+ return false;
665
+ }
666
+
667
+ function hasBuildDiagnosticSignals(item) {
668
+ const text = [item?.aggregated_output, item?.output, item?.text]
669
+ .filter((value) => typeof value === "string" && value.trim())
670
+ .join("\n");
671
+ if (!text) return false;
672
+ return (
673
+ /\b(?:error|warning)\s+(?:TS|CS|MSB|NU)\d+\b/i.test(text)
674
+ || hasSourceDiagnosticLine(text)
675
+ || /\b(?:Build FAILED|Cannot find name|not assignable to type|The build failed\.)\b/i.test(text)
676
+ );
677
+ }
678
+
604
679
  function classifyLiveFamily(commandFamily, item) {
605
- const cmd = String(commandFamily || "");
606
- const full = `${cmd} ${extractCommandLine(item)}`.toLowerCase();
680
+ const cmd = String(commandFamily || "").trim().toLowerCase();
681
+ const commandLine = extractCommandLine(item);
682
+ const full = `${cmd} ${commandLine}`.toLowerCase();
683
+ const hasBuildDiagnostics = hasBuildDiagnosticSignals(item);
684
+
607
685
  if (["grep", "rg", "find", "findstr", "select-string", "ag", "ack", "sift", "fd", "where", "which", "ls", "dir", "tree", "gci", "get-childitem"].includes(cmd) || /git\s+grep\b/.test(full)) return "search";
608
686
  if (cmd === "git") return "git";
609
- if (["go", "pytest", "cargo", "gradle", "maven", "mvn", "javac", "tsc", "jest", "vitest", "deno", "bun", "make", "cmake", "bazel", "buck", "nx", "turbo", "rush", "dotnet", "msbuild", "xunit", "nunit", "ctest"].includes(cmd)) return "build";
610
- if (["npm", "pnpm", "yarn", "node", "python", "python3", "pip", "pip3", "poetry", "composer", "bundle", "npx"].includes(cmd)) {
611
- return /test|build|install|lint|run|pytest|jest|vitest|mocha|ava|unittest|coverage|compile/.test(full) ? "build" : "generic";
687
+
688
+ if (/\b(dotnet|go|pytest|cargo|gradle|maven|mvn|javac|tsc|jest|vitest|deno|make|cmake|bazel|buck|nx|turbo|rush|msbuild|xunit|nunit|ctest)\b/.test(full)) {
689
+ if (/\b(test|tests|pytest|vitest|jest|xunit|nunit|ctest)\b/.test(full)) return hasBuildDiagnostics ? "build" : "test";
690
+ if (/\b(publish|deploy|release|ship|rollout|upgrade)\b/.test(full)) return "deploy";
691
+ return "build";
692
+ }
693
+
694
+ if (["npm", "pnpm", "yarn", "bun", "npx", "pip", "pip3", "poetry", "composer", "bundle"].includes(cmd)) {
695
+ if (/\b(install|add|update|upgrade|remove|uninstall|ci|dedupe|audit|outdated|list)\b/.test(full)) return "package-manager";
696
+ if (/\b(test|coverage|jest|vitest|mocha|ava|unittest)\b/.test(full)) return hasBuildDiagnostics ? "build" : "test";
697
+ if (/\b(build|compile|bundle|pack|publish)\b/.test(full)) return "build";
612
698
  }
699
+
613
700
  if (["journalctl", "tail", "get-content"].includes(cmd)) return "logs";
614
701
  if (["docker", "kubectl"].includes(cmd) && /logs?\b|tail\b|follow\b|-f\b/.test(full)) return "logs";
615
- if (["docker", "kubectl", "helm", "terraform", "ansible", "ansible-playbook", "systemctl"].includes(cmd)) return "ops";
702
+ if (["docker", "kubectl", "helm", "terraform", "ansible", "ansible-playbook", "systemctl"].includes(cmd)) {
703
+ if (/\b(up|apply|deploy|rollout|upgrade|install|release|push|publish|compose)\b/.test(full)) return "deploy";
704
+ return "ops";
705
+ }
706
+
707
+ if (hasBuildDiagnostics) return "build";
708
+
616
709
  return "generic";
617
710
  }
618
711
 
619
- function collectSignalIndices(lines, predicate, radius = 0, limit = Infinity) {
712
+ function chooseLiveBudgetPolicy(family, analysis, opts) {
713
+ const targetChars = Math.max(200, Number(opts?.liveToolCompactionTargetChars) || 1800);
714
+ const lineCount = Number(analysis?.lineCount) || 0;
715
+ const savedPct = Number(analysis?.savedPct) || 0;
716
+ const policy = {
717
+ family,
718
+ reason: "inline_summary",
719
+ budget: {
720
+ targetChars,
721
+ decision: "inline_summary",
722
+ retrievable: false,
723
+ },
724
+ why: [`family:${family}`, `lines:${lineCount}`, `savedPct:${savedPct}`],
725
+ };
726
+
727
+ if (["package-manager", "deploy", "logs"].includes(family)) {
728
+ policy.reason = "artifact_summary";
729
+ policy.budget.decision = "artifact_summary";
730
+ policy.budget.retrievable = true;
731
+ policy.why.push("large-noisy-output");
732
+ } else if (family === "git") {
733
+ policy.reason = "structured_delta";
734
+ policy.budget.decision = "structured_delta";
735
+ policy.budget.retrievable = true;
736
+ policy.why.push("delta-friendly-family");
737
+ } else if (family === "search") {
738
+ policy.reason = "inline_excerpt";
739
+ policy.budget.decision = "inline_excerpt";
740
+ policy.why.push("match-oriented-family");
741
+ } else if (family === "test") {
742
+ policy.reason = "inline_summary";
743
+ policy.budget.decision = "inline_summary";
744
+ policy.why.push("failure-signals-prioritized");
745
+ } else if (family === "build") {
746
+ policy.reason = "summary_with_delta";
747
+ policy.budget.decision = "summary_with_delta";
748
+ policy.budget.retrievable = true;
749
+ policy.why.push("diagnostics-over-progress");
750
+ }
751
+
752
+ return policy;
753
+ }function collectSignalIndices(lines, predicate, radius = 0, limit = Infinity) {
620
754
  const out = new Set();
621
755
  let count = 0;
622
756
  for (let index = 0; index < lines.length; index += 1) {
@@ -1026,6 +1160,7 @@ async function compactStandaloneToolItem(
1026
1160
  }
1027
1161
 
1028
1162
  const analysis = analyzeLiveToolOutput(item, opts);
1163
+ const liveBudgetPolicy = analysis ? chooseLiveBudgetPolicy(analysis.family, analysis, opts) : null;
1029
1164
  if (analysis) {
1030
1165
  const logId = await writeToCache(item, extractToolName(item), extractArgsPreview(item));
1031
1166
  const diagnostic = await analyzeCommandDiagnosticForItem(item, logId);
@@ -1035,6 +1170,7 @@ async function compactStandaloneToolItem(
1035
1170
  _liveCompacted: true,
1036
1171
  _liveCompactionFamily: analysis.family,
1037
1172
  _liveCompactionCommandFamily: analysis.commandFamily,
1173
+ _liveCompactionPolicy: liveBudgetPolicy,
1038
1174
  };
1039
1175
  if (diagnostic) compactedItem._commandDiagnostics = diagnostic;
1040
1176
  setItemText(
@@ -1066,6 +1202,16 @@ async function compactStandaloneToolItem(
1066
1202
  _liveCompacted: true,
1067
1203
  _liveCompactionFamily: "generic",
1068
1204
  _liveCompactionCommandFamily: extractCommandFamily(item),
1205
+ _liveCompactionPolicy: {
1206
+ family: "generic",
1207
+ reason: "signal_excerpt",
1208
+ budget: {
1209
+ targetChars: Math.max(200, Number(opts?.liveToolCompactionTargetChars) || 1800),
1210
+ decision: "inline_excerpt",
1211
+ retrievable: true,
1212
+ },
1213
+ why: ["family:generic", "fallback-signal-excerpt"],
1214
+ },
1069
1215
  };
1070
1216
  if (diagnostic) compactedItem._commandDiagnostics = diagnostic;
1071
1217
  setItemText(
@@ -1322,6 +1468,16 @@ function applyImmediateGitCompression(item, logId, opts) {
1322
1468
  _liveCompacted: true,
1323
1469
  _liveCompactionFamily: "git",
1324
1470
  _liveCompactionCommandFamily: extractCommandFamily(item),
1471
+ _liveCompactionPolicy: {
1472
+ family: "generic",
1473
+ reason: "signal_excerpt",
1474
+ budget: {
1475
+ targetChars: Math.max(200, Number(opts?.liveToolCompactionTargetChars) || 1800),
1476
+ decision: "inline_excerpt",
1477
+ retrievable: true,
1478
+ },
1479
+ why: ["family:generic", "fallback-signal-excerpt"],
1480
+ },
1325
1481
  };
1326
1482
  setItemText(compressed, compressImmediateGitText(getItemText(item), logId, opts));
1327
1483
  return compressed;
@@ -2797,7 +2953,19 @@ export async function maybeCompressSessionItems(
2797
2953
  "../config/context-shredding-config.mjs"
2798
2954
  );
2799
2955
 
2800
- const shreddingOpts = resolveContextShreddingOptions(sessionType, agentType);
2956
+ const shreddingOpts = {
2957
+ ...resolveContextShreddingOptions(sessionType, agentType),
2958
+ ...Object.fromEntries(Object.entries({
2959
+ liveToolCompactionEnabled: arguments[1]?.liveToolCompactionEnabled,
2960
+ liveToolCompactionMode: arguments[1]?.liveToolCompactionMode,
2961
+ liveToolCompactionMinChars: arguments[1]?.liveToolCompactionMinChars,
2962
+ liveToolCompactionTargetChars: arguments[1]?.liveToolCompactionTargetChars,
2963
+ liveToolCompactionMinSavingsPct: arguments[1]?.liveToolCompactionMinSavingsPct,
2964
+ liveToolCompactionMinRuntimeMs: arguments[1]?.liveToolCompactionMinRuntimeMs,
2965
+ liveToolCompactionBlockStructured: arguments[1]?.liveToolCompactionBlockStructured,
2966
+ liveToolCompactionAllowCommands: arguments[1]?.liveToolCompactionAllowCommands,
2967
+ }).filter(([, value]) => value !== undefined)),
2968
+ };
2801
2969
  if (shreddingOpts?._skip === true && !force) return items;
2802
2970
 
2803
2971
  const usagePct = estimateContextUsagePct(items);
@@ -2830,3 +2998,8 @@ export async function maybeCompressSessionItems(
2830
2998
 
2831
2999
  return compressedItems;
2832
3000
  }
3001
+
3002
+
3003
+
3004
+
3005
+