bosun 0.39.2 → 0.40.0

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 (45) hide show
  1. package/.env.example +4 -0
  2. package/README.md +31 -32
  3. package/agent/agent-custom-tools.mjs +19 -3
  4. package/agent/agent-pool.mjs +14 -5
  5. package/agent/agent-prompts.mjs +2 -2
  6. package/agent/bosun-skills.mjs +83 -14
  7. package/agent/primary-agent.mjs +77 -0
  8. package/bosun.schema.json +22 -0
  9. package/cli.mjs +287 -31
  10. package/desktop/launch.mjs +44 -6
  11. package/desktop/main.mjs +474 -33
  12. package/desktop/package.json +2 -2
  13. package/infra/library-manager.mjs +694 -33
  14. package/infra/monitor.mjs +354 -5
  15. package/infra/session-tracker.mjs +30 -0
  16. package/monitor-tail-sanitizer.mjs +77 -0
  17. package/package.json +6 -3
  18. package/server/ui-server.mjs +1194 -55
  19. package/shell/copilot-shell.mjs +38 -11
  20. package/task/task-cli.mjs +105 -9
  21. package/task/task-executor.mjs +96 -12
  22. package/task/task-store.mjs +1362 -46
  23. package/telegram/telegram-bot.mjs +64 -10
  24. package/tools/generate-demo-defaults.mjs +263 -0
  25. package/tools/packed-cli-smoke.mjs +152 -0
  26. package/tools/prepublish-check.mjs +113 -67
  27. package/tools/test-shared-state-integration.mjs +5 -5
  28. package/ui/components/chat-view.js +12 -2
  29. package/ui/components/kanban-board.js +144 -39
  30. package/ui/components/shared.js +17 -8
  31. package/ui/demo-defaults.js +32549 -0
  32. package/ui/demo.html +275 -8
  33. package/ui/modules/mui.js +58 -25
  34. package/ui/modules/settings-schema.js +3 -0
  35. package/ui/modules/state.js +129 -3
  36. package/ui/styles/kanban.css +57 -132
  37. package/ui/styles/sessions.css +6 -0
  38. package/ui/tabs/library.js +140 -29
  39. package/ui/tabs/tasks.js +1019 -134
  40. package/workflow/mcp-discovery-proxy.mjs +629 -0
  41. package/workflow/mcp-registry.mjs +67 -1
  42. package/workflow/workflow-engine.mjs +354 -2
  43. package/workflow/workflow-nodes.mjs +524 -19
  44. package/workflow/workflow-templates.mjs +6 -1
  45. package/workflow-templates/planning.mjs +114 -1
package/.env.example CHANGED
@@ -348,6 +348,10 @@ VOICE_DELEGATE_EXECUTOR=codex-sdk
348
348
  # INTERNAL_EXECUTOR_BASE_BRANCH_PARALLEL=0
349
349
  # How often to poll kanban for new tasks in ms (default: 30000)
350
350
  # INTERNAL_EXECUTOR_POLL_MS=30000
351
+ # Fail-fast policy for workflow start guards when a task appears missing in task-store.
352
+ # false (default): allow dispatch and emit start_guard_bypass audit event
353
+ # true: block dispatch and emit start_guard_blocked audit event
354
+ # BOSUN_STRICT_START_GUARD_MISSING_TASK=false
351
355
  # SDK to use: "auto" | "codex" | "copilot" | "claude" | "gemini" | "opencode" (default: auto)
352
356
  # INTERNAL_EXECUTOR_SDK=auto
353
357
  # Timeout per task execution in ms (default: 5400000 = 90 min)
package/README.md CHANGED
@@ -5,12 +5,18 @@
5
5
 
6
6
  Bosun is a production-grade control plane for an autonomous software engineer. It plans and routes work across executors, automates PR lifecycles, and keeps operators in control through Telegram, the Mini App dashboard, and optional WhatsApp notifications.
7
7
 
8
+ ## Why "Bosun"?
9
+
10
+ _The name "Bosun" comes from "boatswain", the ship's officer responsible for coordinating deck work, keeping operations moving, and translating command into disciplined execution._
11
+
12
+ _That maps directly to the Bosun project: it does not replace the captain or crew, it orchestrates the work. Our Bosun plans tasks, routes them to the right executors, enforces operational checks, and keeps humans in control while the system keeps delivery moving. Autonomous engineering with you in control of the operation._
13
+
8
14
  <p align="center">
9
15
  <a href="https://bosun.engineer">Website</a> · <a href="https://bosun.engineer/docs/">Docs</a> · <a href="https://github.com/virtengine/bosun?tab=readme-ov-file#bosun">GitHub</a> · <a href="https://www.npmjs.com/package/bosun">npm</a> · <a href="https://github.com/virtengine/bosun/issues">Issues</a>
10
16
  </p>
11
17
 
12
18
  <p align="center">
13
- <img src="site/social-card.png" alt="bosun AI agent supervisor" width="100%" />
19
+ <img src="site/workflows.png" alt="Bosun Workflows for Autonomous Engineering" width="100%" />
14
20
  </p>
15
21
 
16
22
  <p align="center">
@@ -105,18 +111,10 @@ Setup profiles for default workflow behavior:
105
111
  | Codex (OpenAI) | `codex-sdk` | `OPENAI_API_KEY` |
106
112
  | Copilot (VS Code) | `copilot-sdk` | VS Code session |
107
113
  | Claude | `claude-sdk` | `ANTHROPIC_API_KEY` |
108
- | OpenCode | `opencode-sdk` | `OPENCODE_MODEL` (e.g. `anthropic/claude-opus-4-5`), `OPENCODE_PORT` (default `4096`) |
114
+ | OpenCode | `opencode-sdk` | `OPENCODE_MODEL` (e.g. `anthropic/claude-opus-4-6`), `OPENCODE_PORT` (default `4096`) |
109
115
 
110
116
  Set `primaryAgent` in `.bosun/bosun.config.json` or choose an executor preset during `bosun --setup`.
111
117
 
112
- ---
113
-
114
- ## Telegram weekly report
115
-
116
- - Run `/weekly` to generate the operator weekly agent-work report on demand.
117
- - Use `/report weekly` as an alias.
118
- - Optional scheduler knobs in `.env`: `TELEGRAM_WEEKLY_REPORT_ENABLED`, `TELEGRAM_WEEKLY_REPORT_DAY`, `TELEGRAM_WEEKLY_REPORT_HOUR` (UTC), and `TELEGRAM_WEEKLY_REPORT_DAYS`.
119
-
120
118
  ## Daemon and sentinel startup
121
119
 
122
120
  - `bosun --daemon` starts the long-running daemon/monitor.
@@ -130,19 +128,15 @@ Set `primaryAgent` in `.bosun/bosun.config.json` or choose an executor preset du
130
128
 
131
129
  **Source docs (markdown):** `_docs/` is the source of truth for long-form documentation. The website should be generated from the same markdown content so docs stay in sync.
132
130
 
133
- Key references:
131
+ **Product docs and implementation notes:** `docs/` contains focused guides, design notes, and operator-facing references that are kept alongside the codebase.
134
132
 
135
- - [GitHub adapter enhancements](_docs/KANBAN_GITHUB_ENHANCEMENT.md)
136
- - [GitHub Projects v2 index](_docs/GITHUB_PROJECTS_V2_INDEX.md)
137
- - [GitHub Projects v2 quickstart](_docs/GITHUB_PROJECTS_V2_QUICKSTART.md)
138
- - [GitHub Projects v2 API](_docs/GITHUB_PROJECTS_V2_API.md)
139
- - [GitHub Projects v2 monitoring](_docs/GITHUB_PROJECTS_V2_MONITORING.md)
140
- - [GitHub Projects v2 checklist](_docs/GITHUB_PROJECTS_V2_IMPLEMENTATION_CHECKLIST.md)
141
- - [Jira integration](_docs/JIRA_INTEGRATION.md)
142
- - [Workflows](_docs/WORKFLOWS.md)
143
- - [Agent logging quickstart](docs/agent-logging-quickstart.md)
144
- - [Agent logging design](docs/agent-work-logging-design.md)
145
- - [Agent logging summary](docs/AGENT_LOGGING_SUMMARY.md)
133
+ Key places to start:
134
+
135
+ - `README.md` - install, setup, and operational overview
136
+ - `_docs/WORKFLOWS.md` - workflow system and built-in templates
137
+ - `docs/workflows-and-libraries.md` - workflow composition and library behavior
138
+ - `docs/agent-logging-quickstart.md` - agent work logging quickstart
139
+ - `docs/agent-work-logging-design.md` - logging design and event model
146
140
 
147
141
  ---
148
142
 
@@ -152,7 +146,7 @@ Bosun enforces a strict quality pipeline in both local hooks and CI:
152
146
 
153
147
  - **Pre-commit hooks** auto-format and lint staged files.
154
148
  - **Pre-push hooks** run targeted checks based on changed files (Go, portal, docs).
155
- - **Demo load smoke test** runs in `npm test` and blocks push if `site/indexv2.html` or `site/ui/demo.html` fails to load required assets.
149
+ - **Demo load smoke test** runs in `npm test` and blocks push if `site/index.html` or `site/ui/demo.html` fails to load required assets.
156
150
  - **Prepublish checks** validate package contents and release readiness.
157
151
 
158
152
  Local commands you can run any time:
@@ -172,18 +166,23 @@ npm run hooks:install
172
166
 
173
167
  ## Repository layout
174
168
 
175
- - `cli.mjs` — entrypoint for the supervisor
176
- - `monitor.mjs` — main orchestration loop
177
- - `config.mjs`unified config loader
178
- - `ui-server.mjs`Telegram Mini App backend
179
- - `site/` — marketing + docs website
180
- - `docs/` and `_docs/` documentation sources (markdown)
169
+ - `cli.mjs` — CLI entrypoint for setup, daemon, desktop, and operator commands
170
+ - `setup.mjs` — interactive setup flow and config bootstrap
171
+ - `infra/`monitor loop, recovery, lifecycle services, and runtime plumbing
172
+ - `workflow/` and `workflow-templates/`workflow engine, nodes, adapters, and built-in templates
173
+ - `task/` — task execution, claims, archiving, and lifecycle ownership
174
+ - `server/` setup server, Mini App backend, and API endpoints
175
+ - `ui/` — Mini App frontend assets and operator dashboard modules
176
+ - `telegram/` — Telegram bot, sentinel, and channel integrations
177
+ - `github/` and `kanban/` — GitHub auth/webhooks and Vibe-Kanban adapters
178
+ - `workspace/` — shared workspace registry, context indexing, and worktree lifecycle
179
+ - `shell/` and `agent/` — executor integrations, prompts, hooks, and fleet coordination
180
+ - `site/` — marketing site and generated docs website assets
181
+ - `docs/` and `_docs/` — product docs, deep technical references, and long-form source material
182
+ - `tools/` and `tests/` — build utilities, release checks, and regression coverage
181
183
 
182
184
  ---
183
185
 
184
186
  ## License
185
187
 
186
188
  Apache-2.0
187
-
188
- <!-- GitHub Analytics Pixel -->
189
- <img src="https://cloud.umami.is/p/iR78WZdwe" alt="" width="1" height="1" style="display:none;" />
@@ -677,7 +677,7 @@ export async function promoteToGlobal(rootDir, toolId) {
677
677
  * and reflect on whether to create new tools.
678
678
  *
679
679
  * @param {string} rootDir
680
- * @param {{ limit?: number, category?: string, tags?: string[], emitReflectHint?: boolean, activeSkills?: string[], agentType?: string, template?: string, includeBuiltins?: boolean }} [opts]
680
+ * @param {{ limit?: number, category?: string, tags?: string[], emitReflectHint?: boolean, activeSkills?: string[], agentType?: string, template?: string, includeBuiltins?: boolean, eagerOnly?: boolean, discoveryMode?: boolean }} [opts]
681
681
  * @returns {string}
682
682
  */
683
683
  export function getToolsPromptBlock(rootDir, opts = {}) {
@@ -691,6 +691,8 @@ export function getToolsPromptBlock(rootDir, opts = {}) {
691
691
  agentType,
692
692
  template,
693
693
  includeBuiltins = true,
694
+ eagerOnly = false,
695
+ discoveryMode = false,
694
696
  } = opts;
695
697
 
696
698
  let tools;
@@ -712,11 +714,25 @@ export function getToolsPromptBlock(rootDir, opts = {}) {
712
714
  tools = listCustomTools(rootDir, { category, tags, includeBuiltins }).slice(0, limit);
713
715
  }
714
716
 
717
+ if (eagerOnly) {
718
+ tools = tools.filter((tool) => {
719
+ if (tool.autoInject) return true;
720
+ if (activeSkills?.length > 0 && tool.skills?.length > 0) {
721
+ return activeSkills.some((skill) => tool.skills.includes(skill));
722
+ }
723
+ return false;
724
+ });
725
+ }
726
+
715
727
  const lines = [
716
728
  "## Custom Tools Library",
717
729
  "",
718
- "The following reusable helper scripts are available. Run them via",
719
- "`node <tool>.mjs`, `bash <tool>.sh`, or `python3 <tool>.py`.",
730
+ discoveryMode
731
+ ? "Only eagerly-loaded tools are listed below. Use the MCP discovery tools to find the rest at runtime."
732
+ : "The following reusable helper scripts are available. Run them via",
733
+ discoveryMode
734
+ ? "Use `search`, then `get_schema`, then `execute` for tools not listed here. Use `call_discovered_tool` only for simple direct calls."
735
+ : "`node <tool>.mjs`, `bash <tool>.sh`, or `python3 <tool>.py`.",
720
736
  "Built-in tools live in `bosun/tools/`; workspace tools in `.bosun/tools/`.",
721
737
  "",
722
738
  ];
@@ -2228,16 +2228,25 @@ export async function launchEphemeralThread(
2228
2228
  if (mcpCfg.enabled !== false) {
2229
2229
  const requestedIds = launchExtra.mcpServers || [];
2230
2230
  const defaultIds = mcpCfg.defaultServers || [];
2231
+ const registry = await getMcpRegistry();
2232
+ let resolved = [];
2231
2233
  if (requestedIds.length || defaultIds.length) {
2232
- const registry = await getMcpRegistry();
2233
- const resolved = await registry.resolveMcpServersForAgent(
2234
+ resolved = await registry.resolveMcpServersForAgent(
2234
2235
  cwd,
2235
2236
  requestedIds,
2236
2237
  { defaultServers: defaultIds, catalogOverrides: mcpCfg.catalogOverrides || {} },
2237
2238
  );
2238
- if (resolved.length) {
2239
- launchExtra._resolvedMcpServers = resolved;
2240
- }
2239
+ }
2240
+ if (typeof registry.wrapServersWithDiscoveryProxy === "function") {
2241
+ resolved = registry.wrapServersWithDiscoveryProxy(cwd, resolved, {
2242
+ enabled: mcpCfg.useDiscoveryProxy !== false,
2243
+ includeCustomTools: mcpCfg.includeCustomToolsInDiscoveryProxy !== false,
2244
+ cacheTtlMs: mcpCfg.discoveryProxyCacheTtlMs,
2245
+ executeTimeoutMs: mcpCfg.discoveryProxyExecuteTimeoutMs,
2246
+ });
2247
+ }
2248
+ if (resolved.length) {
2249
+ launchExtra._resolvedMcpServers = resolved;
2241
2250
  }
2242
2251
  }
2243
2252
  launchExtra._mcpResolved = true;
@@ -770,7 +770,7 @@ CLI:
770
770
  bosun task delete <id>
771
771
  bosun task stats [--json]
772
772
  bosun task import <file.json>
773
- bosun task plan --count N
773
+ Planner workflow: POST /api/workflows/launch-template {"templateId":"template-task-planner"} or /plan [count] [focus]
774
774
 
775
775
  REST API (port 18432):
776
776
  GET /api/tasks[?status=todo]
@@ -868,7 +868,7 @@ to the Bosun workspace, task board, coding agents, and system operations.
868
868
  You can do everything Bosun can — through voice. This includes:
869
869
  - **Task management**: List, create, update, delete, search, and comment on tasks
870
870
  - **Agent delegation**: Send work to coding agents (Codex, Copilot, Claude, Gemini, OpenCode)
871
- - **Agent steering**: Use /ask (read-only), /agent (code changes), or /plan (architecture)
871
+ - **Agent steering**: Use /ask (read-only), /agent (code changes), or /plan (run task planner workflow)
872
872
  - **System monitoring**: Check fleet status, agent health, system configuration
873
873
  - **Workspace navigation**: Read files, list directories, search code
874
874
  - **Workflow management**: List and inspect workflow templates
@@ -62,6 +62,7 @@ function emitSkillInvokeEvent(skillName, skillTitle, opts = {}) {
62
62
  * filename – the .md file written into skills/
63
63
  * title – short human-readable name
64
64
  * tags – array of lowercase tags agents use to match skills to tasks
65
+ * important – eagerly inline this skill into agent context when matched
65
66
  * scope – "global" | "bosun" (bosun-specific internals)
66
67
  * content – the full Markdown skill text
67
68
  */
@@ -70,6 +71,7 @@ export const BUILTIN_SKILLS = [
70
71
  filename: "background-task-execution.md",
71
72
  title: "Background Task Execution",
72
73
  tags: ["background", "task", "reliability", "heartbeat", "stall", "completion"],
74
+ important: true,
73
75
  scope: "global",
74
76
  content: `# Skill: Background Task Execution
75
77
 
@@ -147,6 +149,7 @@ Your worktree path is provided via \`BOSUN_WORKTREE_PATH\`. Stay inside it.
147
149
  filename: "pr-workflow.md",
148
150
  title: "Pull Request Workflow",
149
151
  tags: ["pr", "pull-request", "github", "review", "ci", "merge"],
152
+ important: true,
150
153
  scope: "global",
151
154
  content: `# Skill: Pull Request Workflow
152
155
 
@@ -209,6 +212,7 @@ gh run list --limit 5 # recent workflow runs
209
212
  filename: "error-recovery.md",
210
213
  title: "Error Recovery Patterns",
211
214
  tags: ["error", "recovery", "retry", "debug", "failure"],
215
+ important: true,
212
216
  scope: "global",
213
217
  content: `# Skill: Error Recovery Patterns
214
218
 
@@ -1079,6 +1083,35 @@ export function buildSkillsIndex(skillsDir) {
1079
1083
  /* directory may not exist yet */
1080
1084
  }
1081
1085
 
1086
+ function extractSkillFileMetadata(filePath) {
1087
+ let content = "";
1088
+ try {
1089
+ content = readFileSync(filePath, "utf8");
1090
+ } catch {
1091
+ return { title: "", tags: [], important: false };
1092
+ }
1093
+
1094
+ let title = "";
1095
+ let tags = [];
1096
+ let important = false;
1097
+
1098
+ const tagMatch = /<!--\s*tags:\s*(.+?)\s*-->/i.exec(content);
1099
+ if (tagMatch) {
1100
+ tags = tagMatch[1].split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
1101
+ }
1102
+
1103
+ const importantMatch = /<!--\s*(?:important|eager)\s*:\s*(true|false|yes|no|on|off|1|0)\s*-->/i.exec(content);
1104
+ if (importantMatch) {
1105
+ const raw = String(importantMatch[1] || "").trim().toLowerCase();
1106
+ important = raw === "true" || raw === "yes" || raw === "on" || raw === "1";
1107
+ }
1108
+
1109
+ const h1 = /^#\s+(?:Skill: )?(.+)/m.exec(content);
1110
+ if (h1) title = h1[1].trim();
1111
+
1112
+ return { title, tags, important };
1113
+ }
1114
+
1082
1115
  for (const filename of files.toSorted((a, b) => a.localeCompare(b))) {
1083
1116
  const filePath = resolve(skillsDir, filename);
1084
1117
  let stat;
@@ -1087,31 +1120,26 @@ export function buildSkillsIndex(skillsDir) {
1087
1120
  const builtin = builtinByFilename[filename];
1088
1121
  let title = basename(filename, ".md").replaceAll("-", " ").replaceAll(/\b\w/g, (c) => c.toUpperCase());
1089
1122
  let tags = [];
1123
+ let important = false;
1090
1124
  let scope = "global";
1091
1125
 
1092
1126
  if (builtin) {
1093
1127
  title = builtin.title;
1094
1128
  tags = builtin.tags;
1129
+ important = builtin.important === true;
1095
1130
  scope = builtin.scope;
1096
1131
  } else {
1097
- // Try to extract tags from the first `<!--tags: ... -->` comment or a
1098
- // "## Tags" / "## Tags:" section in the skill file.
1099
- try {
1100
- const content = readFileSync(filePath, "utf8");
1101
- const tagMatch = /<!--\s*tags:\s*(.+?)\s*-->/i.exec(content);
1102
- if (tagMatch) {
1103
- tags = tagMatch[1].split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
1104
- } else {
1105
- const h1 = /^#\s+(?:Skill: )?(.+)/m.exec(content);
1106
- if (h1) title = h1[1].trim();
1107
- }
1108
- } catch { /* ignore read errors */ }
1132
+ const metadata = extractSkillFileMetadata(filePath);
1133
+ if (metadata.title) title = metadata.title;
1134
+ tags = metadata.tags;
1135
+ important = metadata.important;
1109
1136
  }
1110
1137
 
1111
1138
  entries.push({
1112
1139
  filename,
1113
1140
  title,
1114
1141
  tags,
1142
+ important,
1115
1143
  scope,
1116
1144
  updatedAt: stat.mtime.toISOString(),
1117
1145
  });
@@ -1212,12 +1240,12 @@ export function findRelevantSkills(bosunHome, taskTitle, taskDescription = "", o
1212
1240
  .filter(({ tags }) =>
1213
1241
  tags.some((tag) => searchText.includes(tag)),
1214
1242
  )
1215
- .map(({ filename, title, tags }) => {
1243
+ .map(({ filename, title, tags, important }) => {
1216
1244
  let content = "";
1217
1245
  try {
1218
1246
  content = readFileSync(resolve(skillsDir, filename), "utf8");
1219
1247
  } catch { /* skip unreadable files */ }
1220
- return { filename, title, tags, content };
1248
+ return { filename, title, tags, important: important === true, content };
1221
1249
  })
1222
1250
  .filter(({ content }) => !!content);
1223
1251
 
@@ -1229,3 +1257,44 @@ export function findRelevantSkills(bosunHome, taskTitle, taskDescription = "", o
1229
1257
 
1230
1258
  return matched;
1231
1259
  }
1260
+
1261
+ export function buildRelevantSkillsPromptBlock(bosunHome, taskTitle, taskDescription = "", opts = {}) {
1262
+ const {
1263
+ maxListed = 8,
1264
+ includeMatchedSummary = true,
1265
+ } = opts;
1266
+ const matched = findRelevantSkills(bosunHome, taskTitle, taskDescription, opts);
1267
+ if (!matched.length) return "";
1268
+
1269
+ const importantMatches = matched.filter((skill) => skill.important);
1270
+ const summaryMatches = matched.slice(0, Math.max(1, maxListed));
1271
+ const lines = ["## Skills Context", ""];
1272
+
1273
+ if (importantMatches.length > 0) {
1274
+ lines.push(
1275
+ "These matched skills are marked important, so their contents are loaded directly below.",
1276
+ "",
1277
+ );
1278
+ for (const skill of importantMatches) {
1279
+ lines.push(`### Skill: ${skill.title} (\`${skill.filename}\`)`);
1280
+ lines.push(skill.content.trim());
1281
+ lines.push("");
1282
+ }
1283
+ }
1284
+
1285
+ if (includeMatchedSummary) {
1286
+ lines.push("Matched skill files:");
1287
+ for (const skill of summaryMatches) {
1288
+ const tags = Array.isArray(skill.tags) && skill.tags.length > 0
1289
+ ? ` — tags: ${skill.tags.join(", ")}`
1290
+ : "";
1291
+ const importantLabel = skill.important ? " [important]" : "";
1292
+ lines.push(`- \`${skill.filename}\` — ${skill.title}${importantLabel}${tags}`);
1293
+ }
1294
+ lines.push("");
1295
+ lines.push("Load non-important matched skills on demand if you need their details.");
1296
+ lines.push("");
1297
+ }
1298
+
1299
+ return lines.join("\n").trim();
1300
+ }
@@ -158,6 +158,59 @@ function appendAttachmentsToPrompt(message, attachments) {
158
158
  return { message: `${message}${lines.join("\n")}`, appended: true };
159
159
  }
160
160
 
161
+ function summarizeContextCompressionItems(items) {
162
+ if (!Array.isArray(items) || items.length === 0) return null;
163
+
164
+ const counts = {
165
+ agent: 0,
166
+ user: 0,
167
+ tool: 0,
168
+ other: 0,
169
+ };
170
+
171
+ for (const item of items) {
172
+ if (!item || typeof item !== "object") continue;
173
+ const compressedTag = String(item._compressed || "").trim().toLowerCase();
174
+ const text = String(item.text || item.output || item.aggregated_output || "").toLowerCase();
175
+ const hasToolPlaceholder =
176
+ Boolean(item._cachedLogId)
177
+ || text.includes("full output: bosun --tool-log")
178
+ || text.includes(" chars compressed");
179
+
180
+ if (compressedTag.startsWith("agent_")) {
181
+ counts.agent += 1;
182
+ continue;
183
+ }
184
+ if (compressedTag === "user_breadcrumb") {
185
+ counts.user += 1;
186
+ continue;
187
+ }
188
+ if (hasToolPlaceholder) {
189
+ counts.tool += 1;
190
+ continue;
191
+ }
192
+ if (compressedTag) counts.other += 1;
193
+ }
194
+
195
+ const total = counts.agent + counts.user + counts.tool + counts.other;
196
+ if (total === 0) return null;
197
+
198
+ const detailParts = [];
199
+ if (counts.agent) detailParts.push(`${counts.agent} agent message${counts.agent === 1 ? "" : "s"}`);
200
+ if (counts.user) detailParts.push(`${counts.user} user prompt${counts.user === 1 ? "" : "s"}`);
201
+ if (counts.tool) detailParts.push(`${counts.tool} tool output${counts.tool === 1 ? "" : "s"}`);
202
+ if (counts.other) detailParts.push(`${counts.other} other item${counts.other === 1 ? "" : "s"}`);
203
+
204
+ return {
205
+ total,
206
+ counts,
207
+ detail: detailParts.join(", "),
208
+ content:
209
+ `Context summarized for continuation: ${total} older item${total === 1 ? "" : "s"} compressed (${detailParts.join(", ")}). ` +
210
+ `Session history in this view is unchanged.`,
211
+ };
212
+ }
213
+
161
214
  function buildPrimaryToolCapabilityContract(options = {}) {
162
215
  let rootDir = "";
163
216
  try {
@@ -795,6 +848,18 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
795
848
  timestamp: new Date().toISOString(),
796
849
  _sessionType: sessionType,
797
850
  });
851
+ const compressionSummary = summarizeContextCompressionItems(pooled?.items);
852
+ if (compressionSummary) {
853
+ tracker.recordEvent(sessionId, {
854
+ role: "system",
855
+ type: "system",
856
+ content: compressionSummary.content,
857
+ timestamp: new Date().toISOString(),
858
+ meta: {
859
+ contextCompression: compressionSummary,
860
+ },
861
+ });
862
+ }
798
863
  return pooled;
799
864
  }
800
865
 
@@ -872,6 +937,18 @@ export async function execPrimaryPrompt(userMessage, options = {}) {
872
937
  timestamp: new Date().toISOString(),
873
938
  _sessionType: sessionType,
874
939
  });
940
+ const compressionSummary = summarizeContextCompressionItems(result?.items);
941
+ if (compressionSummary) {
942
+ tracker.recordEvent(sessionId, {
943
+ role: "system",
944
+ type: "system",
945
+ content: compressionSummary.content,
946
+ timestamp: new Date().toISOString(),
947
+ meta: {
948
+ contextCompression: compressionSummary,
949
+ },
950
+ });
951
+ }
875
952
  }
876
953
  return result;
877
954
  } catch (err) {
package/bosun.schema.json CHANGED
@@ -1049,6 +1049,28 @@
1049
1049
  },
1050
1050
  "description": "Environment variable overrides keyed by MCP server ID"
1051
1051
  },
1052
+ "useDiscoveryProxy": {
1053
+ "type": "boolean",
1054
+ "default": true,
1055
+ "description": "Wrap resolved MCP servers behind Bosun's compact search/schema/call discovery proxy to reduce tool-definition prompt overhead."
1056
+ },
1057
+ "includeCustomToolsInDiscoveryProxy": {
1058
+ "type": "boolean",
1059
+ "default": true,
1060
+ "description": "Expose Bosun custom tools through the discovery proxy alongside external MCP servers."
1061
+ },
1062
+ "discoveryProxyCacheTtlMs": {
1063
+ "type": "integer",
1064
+ "minimum": 1000,
1065
+ "default": 60000,
1066
+ "description": "How long Bosun caches wrapped MCP tool catalogs inside the discovery proxy before refreshing them."
1067
+ },
1068
+ "discoveryProxyExecuteTimeoutMs": {
1069
+ "type": "integer",
1070
+ "minimum": 1000,
1071
+ "default": 10000,
1072
+ "description": "Default timeout for the discovery proxy execute tool, which runs short orchestration code against discovered tools."
1073
+ },
1052
1074
  "autoInstallDefaults": {
1053
1075
  "type": "boolean",
1054
1076
  "default": true,