bosun 0.41.8 → 0.41.10

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 (58) hide show
  1. package/.env.example +1 -1
  2. package/README.md +23 -1
  3. package/agent/agent-event-bus.mjs +31 -2
  4. package/agent/agent-pool.mjs +275 -40
  5. package/agent/agent-prompts.mjs +9 -1
  6. package/agent/agent-supervisor.mjs +22 -0
  7. package/agent/autofix.mjs +1 -1
  8. package/agent/primary-agent.mjs +115 -5
  9. package/cli.mjs +3 -2
  10. package/config/config.mjs +47 -33
  11. package/config/context-shredding-config.mjs +1 -1
  12. package/config/repo-root.mjs +41 -33
  13. package/desktop/main.mjs +350 -25
  14. package/desktop/preload.cjs +8 -0
  15. package/desktop/preload.mjs +19 -0
  16. package/entrypoint.mjs +332 -0
  17. package/git/sdk-conflict-resolver.mjs +1 -1
  18. package/infra/health-status.mjs +72 -0
  19. package/infra/library-manager.mjs +58 -1
  20. package/infra/maintenance.mjs +1 -2
  21. package/infra/monitor.mjs +26 -8
  22. package/infra/session-tracker.mjs +30 -3
  23. package/package.json +12 -4
  24. package/server/bosun-mcp-server.mjs +1004 -0
  25. package/server/setup-web-server.mjs +288 -259
  26. package/server/ui-server.mjs +1323 -26
  27. package/shell/claude-shell.mjs +14 -1
  28. package/shell/codex-config.mjs +1 -1
  29. package/shell/codex-model-profiles.mjs +170 -30
  30. package/shell/codex-shell.mjs +63 -18
  31. package/shell/opencode-providers.mjs +20 -8
  32. package/task/task-executor.mjs +28 -0
  33. package/task/task-store.mjs +13 -4
  34. package/telegram/telegram-sentinel.mjs +54 -3
  35. package/tools/list-todos.mjs +7 -1
  36. package/ui/app.js +3 -2
  37. package/ui/components/agent-selector.js +127 -0
  38. package/ui/components/session-list.js +15 -10
  39. package/ui/demo-defaults.js +334 -336
  40. package/ui/modules/router.js +2 -0
  41. package/ui/modules/state.js +13 -5
  42. package/ui/tabs/chat.js +3 -0
  43. package/ui/tabs/library.js +284 -52
  44. package/ui/tabs/tasks.js +5 -13
  45. package/ui/tabs/workflows.js +766 -3
  46. package/workflow/workflow-engine.mjs +246 -5
  47. package/workflow/workflow-nodes/definitions.mjs +37 -0
  48. package/workflow/workflow-nodes.mjs +1014 -184
  49. package/workflow/workflow-templates.mjs +0 -5
  50. package/workflow-templates/_helpers.mjs +253 -0
  51. package/workflow-templates/agents.mjs +199 -226
  52. package/workflow-templates/github.mjs +106 -16
  53. package/workflow-templates/sub-workflows.mjs +233 -0
  54. package/workflow-templates/task-execution.mjs +125 -471
  55. package/workflow-templates/task-lifecycle.mjs +11 -48
  56. package/workspace/command-diagnostics.mjs +460 -0
  57. package/workspace/context-cache.mjs +396 -28
  58. package/workspace/worktree-manager.mjs +1 -1
@@ -187,6 +187,14 @@ async function getFs() {
187
187
  return _fsPromises;
188
188
  }
189
189
 
190
+ let _commandDiagnosticsMod = null;
191
+ async function getCommandDiagnosticsMod() {
192
+ if (!_commandDiagnosticsMod) {
193
+ _commandDiagnosticsMod = await import("./command-diagnostics.mjs");
194
+ }
195
+ return _commandDiagnosticsMod;
196
+ }
197
+
190
198
  async function ensureCacheDir() {
191
199
  const fs = await getFs();
192
200
  await fs.mkdir(TOOL_LOG_DIR, { recursive: true });
@@ -463,14 +471,24 @@ function extractArgsPreview(item) {
463
471
  }
464
472
 
465
473
  const LIVE_TOOL_RETRIEVE_PLACEHOLDER = "__BOSUN_TOOL_LOG__";
466
- const LIVE_ERROR_REGEX = /\b(error|errors|fatal|failed|failure|panic|traceback|exception|undefined|denied|not found|enoent|eacces)\b/i;
474
+ const LIVE_ERROR_REGEX = /\b(error|errors|fatal|failed|failure|panic|traceback|exception|undefined|denied|not found|enoent|eacces|segmentation fault|assertion|unhandled|stack trace|msb\d+|nu\d+|cs\d+|ts\d+)\b/i;
467
475
  const LIVE_WARN_REGEX = /\b(warn|warning|deprecated)\b/i;
468
- const LIVE_SUMMARY_REGEX = /\b(summary|total|totals|passed|failed|skipped|collected|found|matched|changed|insertions|deletions|done in|finished|ran \d+ tests?|test suites|packages? audited|up to date|build failed|completed)\b/i;
469
- const LIVE_STATUS_REGEX = /^(FAIL|ERROR|warning|fatal|M\s|A\s|D\s|R\s|\?\?|@@|diff --git|--- |\+\+\+ )/;
476
+ const LIVE_SUMMARY_REGEX = /\b(summary|total|totals|passed|failed|skipped|collected|found|matched|changed|insertions|deletions|done in|finished|ran \d+ tests?|test suites|packages? audited|up to date|build failed|completed|build succeeded|test run|tests run|total tests|passed!|failed!|restore completed|restore failed|time elapsed)\b/i;
477
+ const LIVE_STATUS_REGEX = /^(FAIL|ERROR|warning|fatal|M\s|A\s|D\s|R\s|\?\?|@@|diff --git|--- |\+\+\+ |> |xUnit\.net|Test Run Failed|Test Run Successful|Failed!)/i;
470
478
  const LIVE_STRUCTURED_FLAG_REGEX = /(^|\s)(--json|--format(?:=|\s+)json|-json\b|-o(?:=|\s+)json|--output(?:=|\s+)json|{{json\s+\.}})/i;
471
479
  const LIVE_FILE_REF_REGEX = /((?:[A-Za-z]:)?[.~/\\\w-]+(?:[\\/][^:\s]+)+(?::\d+(?::\d+)?)?)/;
472
480
  const LIVE_SHELL_WRAPPERS = new Set(["bash", "sh", "zsh", "pwsh", "powershell", "cmd"]);
473
481
  const LIVE_ENV_WRAPPERS = new Set(["env", "command", "time", "nohup"]);
482
+ const GENERIC_SIGNAL_MARKER = "\n...[selected signal lines]...\n";
483
+ const GENERIC_OMITTED_MARKER = "\n...[middle content omitted]...\n";
484
+
485
+ function escapeRegExp(value) {
486
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
487
+ }
488
+
489
+ function uniqueValues(values = []) {
490
+ return [...new Set(values)];
491
+ }
474
492
 
475
493
  function extractCommandLine(item) {
476
494
  if (!item || typeof item !== "object") return "";
@@ -588,7 +606,7 @@ function classifyLiveFamily(commandFamily, item) {
588
606
  const full = `${cmd} ${extractCommandLine(item)}`.toLowerCase();
589
607
  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";
590
608
  if (cmd === "git") return "git";
591
- if (["go", "pytest", "cargo", "gradle", "maven", "mvn", "javac", "tsc", "jest", "vitest", "deno", "bun", "make", "cmake", "bazel", "buck", "nx", "turbo", "rush"].includes(cmd)) return "build";
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";
592
610
  if (["npm", "pnpm", "yarn", "node", "python", "python3", "pip", "pip3", "poetry", "composer", "bundle", "npx"].includes(cmd)) {
593
611
  return /test|build|install|lint|run|pytest|jest|vitest|mocha|ava|unittest|coverage|compile/.test(full) ? "build" : "generic";
594
612
  }
@@ -639,12 +657,103 @@ function buildSearchFileSummary(lines) {
639
657
  .map(([file, count]) => `${file} (${count})`);
640
658
  }
641
659
 
660
+ function normalizeSearchTerm(value) {
661
+ return String(value || "").replace(/^['"`]+|['"`]+$/g, "").trim();
662
+ }
663
+
664
+ function isHighSignalSearchTerm(term) {
665
+ const normalized = normalizeSearchTerm(term);
666
+ if (normalized.length < 4) return false;
667
+ if (/^(error|errors|failed|failure|exception|warning|warnings|runtime|type|assertion|network|permission|configuration)$/i.test(normalized)) {
668
+ return false;
669
+ }
670
+ return true;
671
+ }
672
+
673
+ function scoreSearchTerm(term) {
674
+ const normalized = normalizeSearchTerm(term);
675
+ let score = normalized.length;
676
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(normalized)) score += 80;
677
+ if (/^(TS|CS|MSB|NU)\d+$/i.test(normalized)) score += 70;
678
+ if (normalized.includes("/") || normalized.includes("\\")) score += 50;
679
+ if (/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/.test(normalized)) score += 40;
680
+ if (/['"`]/.test(term)) score += 30;
681
+ return score;
682
+ }
683
+
684
+ function collectCandidateSearchTerms(value) {
685
+ const normalized = String(value || "").trim();
686
+ if (!normalized) return [];
687
+ const candidates = [];
688
+
689
+ for (const match of normalized.matchAll(/['"`]([^'"`]{4,})['"`]/g)) {
690
+ candidates.push(match[1]);
691
+ }
692
+ for (const match of normalized.matchAll(/\b(?:TS|CS|MSB|NU)\d+\b/gi)) {
693
+ candidates.push(match[0]);
694
+ }
695
+ for (const match of normalized.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) {
696
+ candidates.push(match[0]);
697
+ }
698
+ for (const match of normalized.matchAll(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g)) {
699
+ candidates.push(match[0]);
700
+ }
701
+ for (const match of normalized.matchAll(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/g)) {
702
+ candidates.push(match[0]);
703
+ }
704
+
705
+ const detail = normalized.split(":").slice(1).join(":").trim();
706
+ if (detail.length >= 8) candidates.push(detail);
707
+
708
+ return uniqueValues(candidates)
709
+ .filter(isHighSignalSearchTerm)
710
+ .sort((left, right) => scoreSearchTerm(right) - scoreSearchTerm(left))
711
+ .slice(0, 6);
712
+ }
713
+
714
+ function collectDiagnosticSearchTerms(lines, family) {
715
+ const sourceLines = lines.filter((line) =>
716
+ LIVE_ERROR_REGEX.test(line) ||
717
+ LIVE_STATUS_REGEX.test(line) ||
718
+ LIVE_SUMMARY_REGEX.test(line) ||
719
+ (family === "build" && !!extractFileKey(line))
720
+ );
721
+ return uniqueValues(
722
+ sourceLines.flatMap((line) => collectCandidateSearchTerms(line)),
723
+ ).slice(0, 10);
724
+ }
725
+
726
+ function collectExactSignalLines(input) {
727
+ const deduped = new Set();
728
+ for (const line of String(input || "").split("\n")) {
729
+ const trimmed = line.trimEnd();
730
+ if (!trimmed) continue;
731
+ if (LIVE_ERROR_REGEX.test(trimmed) || LIVE_WARN_REGEX.test(trimmed) || LIVE_SUMMARY_REGEX.test(trimmed) || LIVE_STATUS_REGEX.test(trimmed) || extractFileKey(trimmed)) {
732
+ deduped.add(trimmed);
733
+ }
734
+ }
735
+ return [...deduped].slice(0, 120);
736
+ }
737
+
738
+ function isLowValuePassLine(line) {
739
+ const normalized = String(line || "").trim();
740
+ if (!normalized) return false;
741
+ return /^Passed!\s+\S+/i.test(normalized) || /^Test run for .+\.dll/i.test(normalized);
742
+ }
743
+
642
744
  function pickSelectedLines(lines, family, mode) {
643
745
  const maxLines = mode === "aggressive" ? 12 : 20;
644
746
  const errorSet = collectSignalIndices(lines, (line) => LIVE_ERROR_REGEX.test(line) || LIVE_STATUS_REGEX.test(line), family === "build" ? 1 : 0, mode === "aggressive" ? 8 : 12);
645
747
  const warnSet = collectSignalIndices(lines, (line) => LIVE_WARN_REGEX.test(line), 0, mode === "aggressive" ? 4 : 6);
646
- const summarySet = collectSignalIndices(lines, (line) => LIVE_SUMMARY_REGEX.test(line), 0, mode === "aggressive" ? 5 : 8);
748
+ const summarySet = collectSignalIndices(lines, (line) => LIVE_SUMMARY_REGEX.test(line) && !isLowValuePassLine(line), 0, mode === "aggressive" ? 5 : 8);
647
749
  const fileSet = collectSignalIndices(lines, (line) => !!extractFileKey(line), 0, family === "search" ? maxLines : 8);
750
+ const diagnosticTerms = collectDiagnosticSearchTerms(lines, family);
751
+ const termSet = collectSignalIndices(
752
+ lines,
753
+ (line) => diagnosticTerms.some((term) => new RegExp(escapeRegExp(term), "i").test(line)),
754
+ family === "build" ? 1 : 0,
755
+ mode === "aggressive" ? 8 : 12,
756
+ );
648
757
  const selected = new Set();
649
758
  const addIndices = (iterable) => {
650
759
  for (const idx of iterable) {
@@ -654,6 +763,7 @@ function pickSelectedLines(lines, family, mode) {
654
763
  addIndices(errorSet);
655
764
  addIndices(warnSet);
656
765
  addIndices(summarySet);
766
+ addIndices(termSet);
657
767
  if (family === "search" || family === "git") addIndices(fileSet);
658
768
  if ((family === "logs" || family === "ops") && selected.size < maxLines) {
659
769
  for (let i = Math.max(0, lines.length - (mode === "aggressive" ? 8 : 12)); i < lines.length && selected.size < maxLines; i += 1) {
@@ -673,6 +783,75 @@ function pickSelectedLines(lines, family, mode) {
673
783
  return [...selected].sort((a, b) => a - b).map((idx) => lines[idx]).filter(Boolean);
674
784
  }
675
785
 
786
+ function renderGenericSignalExcerptText(item, logRef, opts) {
787
+ const originalText = getItemText(item);
788
+ if (typeof originalText !== "string" || !originalText) return "";
789
+ const maxChars = Math.max(
790
+ Number(opts?.liveToolCompactionTargetChars) || 1800,
791
+ 900,
792
+ );
793
+ if (originalText.length <= maxChars) return originalText;
794
+
795
+ const signalLines = collectExactSignalLines(originalText).join("\n");
796
+ let headChars = Math.min(Math.max(240, Math.floor(maxChars * 0.5)), maxChars);
797
+ let tailChars = Math.min(Math.max(160, Math.floor(maxChars * 0.2)), maxChars);
798
+
799
+ while (headChars + tailChars + GENERIC_OMITTED_MARKER.length > maxChars) {
800
+ if (headChars >= tailChars && headChars > 120) {
801
+ headChars = Math.max(120, headChars - 80);
802
+ } else if (tailChars > 80) {
803
+ tailChars = Math.max(80, tailChars - 80);
804
+ } else {
805
+ break;
806
+ }
807
+ }
808
+
809
+ const head = originalText.slice(0, headChars);
810
+ const tail = originalText.slice(originalText.length - tailChars);
811
+ const retrieve = `bosun --tool-log ${logRef}`;
812
+ const budget =
813
+ maxChars -
814
+ head.length -
815
+ tail.length -
816
+ GENERIC_OMITTED_MARKER.length -
817
+ retrieve.length -
818
+ 64;
819
+
820
+ const signalSnippet = budget > 0 && signalLines
821
+ ? signalLines.slice(0, Math.max(0, budget))
822
+ : "";
823
+
824
+ const note = `\n\n[Signal-first excerpt — full output: ${retrieve}]`;
825
+ return [
826
+ head,
827
+ GENERIC_OMITTED_MARKER,
828
+ signalSnippet ? `${GENERIC_SIGNAL_MARKER}${signalSnippet}` : "",
829
+ tail,
830
+ note,
831
+ ]
832
+ .join("")
833
+ .slice(0, maxChars + note.length)
834
+ .trimEnd();
835
+ }
836
+
837
+ function appendCommandDiagnosticFooter(baseText, diagnostic, opts) {
838
+ if (!diagnostic || typeof diagnostic !== "object") return baseText;
839
+ const { renderCommandDiagnosticFooter } = diagnostic._helpers || {};
840
+ const footerText = typeof renderCommandDiagnosticFooter === "function"
841
+ ? renderCommandDiagnosticFooter(diagnostic)
842
+ : "";
843
+ if (!footerText) return baseText;
844
+ const maxChars = Math.max(
845
+ Number(opts?.liveToolCompactionTargetChars) || 1800,
846
+ 900,
847
+ );
848
+ const footer = `\n\n${footerText}`;
849
+ const combined = `${String(baseText || "").trimEnd()}${footer}`;
850
+ if (combined.length <= maxChars + 320) return combined;
851
+ const headBudget = Math.max(240, maxChars - footer.length - 80);
852
+ return `${String(baseText || "").slice(0, headBudget).trimEnd()}\n\n[...summary trimmed for diagnostics...]\n\n${footerText}`;
853
+ }
854
+
676
855
  function renderLiveCompactionText(analysis, logRef, opts) {
677
856
  const retrieve = `bosun --tool-log ${logRef}`;
678
857
  const header = `[Live-compacted ${analysis.family}] ${analysis.commandLabel} -> ${analysis.lineCount} lines / ${charLabel(analysis.originalText)}, saved ~${analysis.savedPct}% | Full output: ${retrieve}`;
@@ -717,12 +896,40 @@ function renderLiveCompactionText(analysis, logRef, opts) {
717
896
  return rendered;
718
897
  }
719
898
 
899
+ async function analyzeCommandDiagnosticForItem(item, logId = null) {
900
+ const text = getItemText(item);
901
+ if (!text) return null;
902
+ const { analyzeCommandDiagnostic, renderCommandDiagnosticFooter } = await getCommandDiagnosticsMod();
903
+ const diagnostic = await analyzeCommandDiagnostic({
904
+ command: extractCommandLine(item),
905
+ args: Array.isArray(item?.arguments?.argv)
906
+ ? item.arguments.argv
907
+ : Array.isArray(item?.args)
908
+ ? item.args
909
+ : [],
910
+ output: item?.aggregated_output ?? item?.output ?? item?.text ?? item?.stdout ?? "",
911
+ stderr: item?.stderr ?? "",
912
+ exitCode: item?.exit_code ?? item?.exitCode ?? 0,
913
+ });
914
+ if (!diagnostic) return null;
915
+ return {
916
+ ...diagnostic,
917
+ retrieveCommand: logId ? `bosun --tool-log ${logId}` : null,
918
+ _helpers: { renderCommandDiagnosticFooter },
919
+ };
920
+ }
921
+
720
922
  function analyzeLiveToolOutput(item, opts) {
721
923
  const originalText = getItemText(item);
722
924
  if (typeof originalText !== "string" || originalText.length < (opts.liveToolCompactionMinChars ?? 4000)) return null;
723
925
  const commandFamily = extractCommandFamily(item);
724
926
  const allowlist = new Set((opts.liveToolCompactionAllowCommands || []).map((value) => String(value).trim().toLowerCase()).filter(Boolean));
725
- if (allowlist.size > 0 && !allowlist.has(commandFamily)) return null;
927
+ const enforceAllowlist =
928
+ allowlist.size > 0 &&
929
+ !allowlist.has(commandFamily) &&
930
+ item?.type !== "command_execution" &&
931
+ item?.type !== "command_output";
932
+ if (enforceAllowlist) return null;
726
933
  if (opts.liveToolCompactionBlockStructured !== false && isLikelyStructuredOutput(originalText, item)) return null;
727
934
  const family = classifyLiveFamily(commandFamily, item);
728
935
  const normalizedText = originalText.replace(/\r\n?/g, "\n");
@@ -782,34 +989,62 @@ function shouldApplyLiveCompaction(item, opts, contextUsagePct, force = false) {
782
989
  return runtimeReady && (sizePressure || contextPressure);
783
990
  }
784
991
 
785
- async function maybeCompactLiveToolOutputs(items, opts = {}, { contextUsagePct = null, force = false, agentType = null } = {}) {
786
- if (!Array.isArray(items) || items.length === 0) return items;
787
- let changed = false;
788
- const nextItems = [];
789
- for (const item of items) {
790
- if (classifyItem(item) !== "tool_output") {
791
- nextItems.push(item);
792
- continue;
793
- }
794
- if (!shouldApplyLiveCompaction(item, opts, contextUsagePct, force)) {
795
- nextItems.push(item);
796
- continue;
797
- }
798
- const analysis = analyzeLiveToolOutput(item, opts);
799
- if (!analysis) {
800
- nextItems.push(item);
801
- continue;
992
+ async function compactStandaloneToolItem(
993
+ item,
994
+ opts = {},
995
+ { agentType = null } = {},
996
+ ) {
997
+ if (!item || typeof item !== "object") return item;
998
+ const existingText = getItemText(item);
999
+ if (typeof existingText !== "string" || existingText.length < (opts.liveToolCompactionMinChars ?? 4000)) {
1000
+ return item;
1001
+ }
1002
+ if (opts.liveToolCompactionBlockStructured !== false && isLikelyStructuredOutput(existingText, item)) {
1003
+ return item;
1004
+ }
1005
+
1006
+ const directGitClass = classifyImmediateGitOutput(item, opts);
1007
+ if (directGitClass) {
1008
+ const logId = await writeToCache(item, extractToolName(item), extractArgsPreview(item));
1009
+ const compactedItem = applyImmediateGitCompression(item, logId, opts);
1010
+ const diagnostic = await analyzeCommandDiagnosticForItem(item, logId);
1011
+ if (diagnostic) {
1012
+ compactedItem._commandDiagnostics = diagnostic;
1013
+ setItemText(compactedItem, appendCommandDiagnosticFooter(getItemText(compactedItem), diagnostic, opts));
802
1014
  }
1015
+ recordShreddingEvent({
1016
+ originalChars: existingText.length,
1017
+ compressedChars: getItemText(compactedItem).length,
1018
+ savedChars: Math.max(0, existingText.length - getItemText(compactedItem).length),
1019
+ savedPct: Math.max(0, Math.round(((existingText.length - getItemText(compactedItem).length) / Math.max(1, existingText.length)) * 100)),
1020
+ agentType: agentType || null,
1021
+ stage: "live_tool_compaction",
1022
+ compactionFamily: "git",
1023
+ commandFamily: extractCommandFamily(item),
1024
+ });
1025
+ return compactedItem;
1026
+ }
1027
+
1028
+ const analysis = analyzeLiveToolOutput(item, opts);
1029
+ if (analysis) {
803
1030
  const logId = await writeToCache(item, extractToolName(item), extractArgsPreview(item));
1031
+ const diagnostic = await analyzeCommandDiagnosticForItem(item, logId);
804
1032
  const compactedItem = {
805
1033
  ...item,
806
1034
  _cachedLogId: logId,
807
1035
  _liveCompacted: true,
808
1036
  _liveCompactionFamily: analysis.family,
1037
+ _liveCompactionCommandFamily: analysis.commandFamily,
809
1038
  };
810
- setItemText(compactedItem, renderLiveCompactionText(analysis, logId, opts));
811
- nextItems.push(compactedItem);
812
- changed = true;
1039
+ if (diagnostic) compactedItem._commandDiagnostics = diagnostic;
1040
+ setItemText(
1041
+ compactedItem,
1042
+ appendCommandDiagnosticFooter(
1043
+ renderLiveCompactionText(analysis, logId, opts),
1044
+ diagnostic,
1045
+ opts,
1046
+ ),
1047
+ );
813
1048
  recordShreddingEvent({
814
1049
  originalChars: analysis.originalText.length,
815
1050
  compressedChars: getItemText(compactedItem).length,
@@ -820,9 +1055,136 @@ async function maybeCompactLiveToolOutputs(items, opts = {}, { contextUsagePct =
820
1055
  compactionFamily: analysis.family,
821
1056
  commandFamily: analysis.commandFamily,
822
1057
  });
1058
+ return compactedItem;
1059
+ }
1060
+
1061
+ const logId = await writeToCache(item, extractToolName(item), extractArgsPreview(item));
1062
+ const diagnostic = await analyzeCommandDiagnosticForItem(item, logId);
1063
+ const compactedItem = {
1064
+ ...item,
1065
+ _cachedLogId: logId,
1066
+ _liveCompacted: true,
1067
+ _liveCompactionFamily: "generic",
1068
+ _liveCompactionCommandFamily: extractCommandFamily(item),
1069
+ };
1070
+ if (diagnostic) compactedItem._commandDiagnostics = diagnostic;
1071
+ setItemText(
1072
+ compactedItem,
1073
+ appendCommandDiagnosticFooter(
1074
+ renderGenericSignalExcerptText(item, logId, opts),
1075
+ diagnostic,
1076
+ opts,
1077
+ ),
1078
+ );
1079
+ const compactedText = getItemText(compactedItem);
1080
+ if (!compactedText || compactedText.length >= existingText.length) {
1081
+ return item;
1082
+ }
1083
+ recordShreddingEvent({
1084
+ originalChars: existingText.length,
1085
+ compressedChars: compactedText.length,
1086
+ savedChars: Math.max(0, existingText.length - compactedText.length),
1087
+ savedPct: Math.max(0, Math.round(((existingText.length - compactedText.length) / Math.max(1, existingText.length)) * 100)),
1088
+ agentType: agentType || null,
1089
+ stage: "live_tool_compaction",
1090
+ compactionFamily: "generic",
1091
+ commandFamily: extractCommandFamily(item),
1092
+ });
1093
+ return compactedItem;
1094
+ }
1095
+
1096
+ async function maybeCompactLiveToolOutputs(items, opts = {}, { contextUsagePct = null, force = false, agentType = null } = {}) {
1097
+ if (!Array.isArray(items) || items.length === 0) return items;
1098
+ let changed = false;
1099
+ const nextItems = [];
1100
+ for (const item of items) {
1101
+ if (classifyItem(item) !== "tool_output") {
1102
+ nextItems.push(item);
1103
+ continue;
1104
+ }
1105
+ if (!shouldApplyLiveCompaction(item, opts, contextUsagePct, force)) {
1106
+ nextItems.push(item);
1107
+ continue;
1108
+ }
1109
+ const compactedItem = await compactStandaloneToolItem(item, opts, { agentType });
1110
+ nextItems.push(compactedItem);
1111
+ changed = changed || compactedItem !== item;
823
1112
  }
824
1113
  return changed ? nextItems : items;
825
1114
  }
1115
+
1116
+ export async function compactCommandOutputPayload(
1117
+ payload = {},
1118
+ {
1119
+ sessionType = "flow",
1120
+ agentType = "workflow",
1121
+ force = true,
1122
+ } = {},
1123
+ ) {
1124
+ const outputText =
1125
+ typeof payload.output === "string"
1126
+ ? payload.output
1127
+ : typeof payload.stdout === "string"
1128
+ ? payload.stdout
1129
+ : "";
1130
+ const stderrText = typeof payload.stderr === "string" ? payload.stderr : "";
1131
+ const trimmedOutput = outputText.trim();
1132
+ const trimmedStderr = stderrText.trim();
1133
+ const combinedText = [trimmedOutput, trimmedStderr && trimmedStderr !== trimmedOutput ? `[stderr]\n${trimmedStderr}` : ""]
1134
+ .filter(Boolean)
1135
+ .join("\n\n")
1136
+ .trim();
1137
+
1138
+ if (!combinedText) {
1139
+ return {
1140
+ text: "",
1141
+ compacted: false,
1142
+ originalChars: 0,
1143
+ compactedChars: 0,
1144
+ item: null,
1145
+ toolLogId: null,
1146
+ retrieveCommand: null,
1147
+ compactionFamily: null,
1148
+ commandFamily: null,
1149
+ };
1150
+ }
1151
+
1152
+ const syntheticItem = {
1153
+ type: "command_execution",
1154
+ tool_name: payload.toolName || "command_execution",
1155
+ command: String(payload.command || "").trim(),
1156
+ arguments: Array.isArray(payload.args)
1157
+ ? { argv: payload.args.map((value) => String(value)) }
1158
+ : payload.args,
1159
+ aggregated_output: combinedText,
1160
+ exit_code: Number.isFinite(Number(payload.exitCode)) ? Number(payload.exitCode) : undefined,
1161
+ duration_ms: Number.isFinite(Number(payload.durationMs)) ? Number(payload.durationMs) : undefined,
1162
+ };
1163
+
1164
+ const compactedItems = await maybeCompressSessionItems([syntheticItem], {
1165
+ sessionType,
1166
+ agentType,
1167
+ force,
1168
+ skip: false,
1169
+ });
1170
+ const compactedItem = compactedItems[0] || syntheticItem;
1171
+ const compactedText = String(getItemText(compactedItem) || combinedText).trim();
1172
+ const toolLogId = compactedItem?._cachedLogId || null;
1173
+ const commandDiagnostics = compactedItem?._commandDiagnostics || null;
1174
+
1175
+ return {
1176
+ text: compactedText,
1177
+ compacted: compactedText.length < combinedText.length,
1178
+ originalChars: combinedText.length,
1179
+ compactedChars: compactedText.length,
1180
+ item: compactedItem,
1181
+ toolLogId,
1182
+ retrieveCommand: toolLogId ? `bosun --tool-log ${toolLogId}` : null,
1183
+ compactionFamily: compactedItem?._liveCompactionFamily || null,
1184
+ commandFamily: compactedItem?._liveCompactionCommandFamily || extractCommandFamily(syntheticItem),
1185
+ commandDiagnostics,
1186
+ };
1187
+ }
826
1188
  function extractCommandText(item) {
827
1189
  if (!item || typeof item !== "object") return "";
828
1190
 
@@ -953,7 +1315,14 @@ function compressImmediateGitText(text, logId, opts) {
953
1315
  }
954
1316
 
955
1317
  function applyImmediateGitCompression(item, logId, opts) {
956
- const compressed = { ...item, _cachedLogId: logId, _compressed: "git_tier2" };
1318
+ const compressed = {
1319
+ ...item,
1320
+ _cachedLogId: logId,
1321
+ _compressed: "git_tier2",
1322
+ _liveCompacted: true,
1323
+ _liveCompactionFamily: "git",
1324
+ _liveCompactionCommandFamily: extractCommandFamily(item),
1325
+ };
957
1326
  setItemText(compressed, compressImmediateGitText(getItemText(item), logId, opts));
958
1327
  return compressed;
959
1328
  }
@@ -2461,4 +2830,3 @@ export async function maybeCompressSessionItems(
2461
2830
 
2462
2831
  return compressedItems;
2463
2832
  }
2464
-
@@ -599,7 +599,7 @@ class WorktreeManager {
599
599
  const detection = detectProjectStack(worktreePath);
600
600
  if (!detection?.primary) return;
601
601
 
602
- const plan = buildBootstrapPlan(worktreePath, policy, detection);
602
+ const plan = buildBootstrapPlan(worktreePath, policy, detection, this.repoRoot);
603
603
  ensureWorktreeSharedPaths(this.repoRoot, worktreePath, plan.sharedPaths);
604
604
 
605
605
  const signature = buildBootstrapSignature(plan);