bosun 0.36.2 → 0.36.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 (57) hide show
  1. package/agent-prompts.mjs +95 -0
  2. package/analyze-agent-work-helpers.mjs +308 -0
  3. package/analyze-agent-work.mjs +926 -0
  4. package/autofix.mjs +2 -0
  5. package/bosun.schema.json +101 -3
  6. package/codex-shell.mjs +85 -10
  7. package/desktop/main.mjs +871 -48
  8. package/desktop/preload.mjs +54 -1
  9. package/desktop-shortcut.mjs +90 -11
  10. package/git-editor-fix.mjs +273 -0
  11. package/mcp-registry.mjs +579 -0
  12. package/meeting-workflow-service.mjs +631 -0
  13. package/monitor.mjs +18 -103
  14. package/package.json +21 -2
  15. package/primary-agent.mjs +32 -12
  16. package/session-tracker.mjs +68 -0
  17. package/setup-web-server.mjs +20 -10
  18. package/setup.mjs +376 -83
  19. package/startup-service.mjs +51 -6
  20. package/stream-resilience.mjs +17 -7
  21. package/ui/app.js +164 -4
  22. package/ui/components/agent-selector.js +145 -1
  23. package/ui/components/chat-view.js +161 -15
  24. package/ui/components/session-list.js +2 -2
  25. package/ui/components/shared.js +188 -15
  26. package/ui/modules/icons.js +13 -0
  27. package/ui/modules/utils.js +44 -0
  28. package/ui/modules/voice-client-sdk.js +733 -0
  29. package/ui/modules/voice-overlay.js +128 -15
  30. package/ui/modules/voice.js +15 -6
  31. package/ui/setup.html +281 -81
  32. package/ui/styles/components.css +99 -3
  33. package/ui/styles/sessions.css +122 -14
  34. package/ui/styles.css +14 -0
  35. package/ui/tabs/agents.js +1 -1
  36. package/ui/tabs/chat.js +123 -14
  37. package/ui/tabs/control.js +16 -22
  38. package/ui/tabs/dashboard.js +85 -8
  39. package/ui/tabs/library.js +113 -17
  40. package/ui/tabs/settings.js +116 -2
  41. package/ui/tabs/tasks.js +388 -39
  42. package/ui/tabs/telemetry.js +0 -1
  43. package/ui/tabs/workflows.js +4 -0
  44. package/ui-server.mjs +400 -22
  45. package/update-check.mjs +41 -13
  46. package/voice-action-dispatcher.mjs +844 -0
  47. package/voice-agents-sdk.mjs +664 -0
  48. package/voice-auth-manager.mjs +164 -0
  49. package/voice-relay.mjs +1194 -0
  50. package/voice-tools.mjs +914 -0
  51. package/workflow-templates/agents.mjs +6 -2
  52. package/workflow-templates/github.mjs +154 -12
  53. package/workflow-templates.mjs +3 -0
  54. package/github-reconciler.mjs +0 -506
  55. package/merge-strategy.mjs +0 -1210
  56. package/pr-cleanup-daemon.mjs +0 -992
  57. package/workspace-reaper.mjs +0 -405
package/agent-prompts.mjs CHANGED
@@ -125,6 +125,18 @@ const PROMPT_DEFS = [
125
125
  description:
126
126
  "Front-end specialist agent with screenshot-based validation and visual verification.",
127
127
  },
128
+ {
129
+ key: "voiceAgent",
130
+ filename: "voice-agent.md",
131
+ description:
132
+ "Voice agent system prompt for real-time voice sessions with action dispatch.",
133
+ },
134
+ {
135
+ key: "voiceAgentCompact",
136
+ filename: "voice-agent-compact.md",
137
+ description:
138
+ "Compact voice agent prompt for bandwidth-constrained or low-latency sessions.",
139
+ },
128
140
  ];
129
141
 
130
142
  export const AGENT_PROMPT_DEFINITIONS = Object.freeze(
@@ -922,6 +934,89 @@ requirements before the task is marked as done.
922
934
  - Working Directory: {{WORKTREE_PATH}}
923
935
 
924
936
  {{COAUTHOR_INSTRUCTION}}
937
+ `,
938
+ voiceAgent: `# Bosun Voice Agent
939
+
940
+ You are **Bosun**, a voice-first assistant for the VirtEngine development platform.
941
+ You interact with developers through real-time voice conversations and have **full access**
942
+ to the Bosun workspace, task board, coding agents, and system operations.
943
+
944
+ ## Core Capabilities
945
+
946
+ You can do everything Bosun can — through voice. This includes:
947
+ - **Task management**: List, create, update, delete, search, and comment on tasks
948
+ - **Agent delegation**: Send work to coding agents (Codex, Copilot, Claude, Gemini, OpenCode)
949
+ - **Agent steering**: Use /ask (read-only), /agent (code changes), or /plan (architecture)
950
+ - **System monitoring**: Check fleet status, agent health, system configuration
951
+ - **Workspace navigation**: Read files, list directories, search code
952
+ - **Workflow management**: List and inspect workflow templates
953
+ - **Skills & prompts**: Browse the knowledge base and prompt library
954
+
955
+ ## How Actions Work
956
+
957
+ When the user asks you to do something, you perform it by returning a JSON action intent.
958
+ Bosun processes the action directly via JavaScript (no MCP bridge needed) and returns the result.
959
+ You then speak the result to the user naturally.
960
+
961
+ ### Action Format
962
+ \`\`\`json
963
+ { "action": "task.list", "params": { "status": "todo" } }
964
+ \`\`\`
965
+
966
+ ### Multiple Actions
967
+ \`\`\`json
968
+ { "action": "batch", "params": { "actions": [
969
+ { "action": "task.stats", "params": {} },
970
+ { "action": "agent.status", "params": {} }
971
+ ] } }
972
+ \`\`\`
973
+
974
+ {{VOICE_ACTION_MANIFEST}}
975
+
976
+ ## Agent Delegation
977
+
978
+ When users need code written, files modified, bugs debugged, or PRs created:
979
+ 1. Use \`agent.delegate\` with a detailed message
980
+ 2. Choose the right mode: "ask" for questions, "agent" for code changes, "plan" for architecture
981
+ 3. You can specify which executor to use, or let the default handle it
982
+
983
+ Examples:
984
+ - "Fix the login bug" → \`{ "action": "agent.code", "params": { "message": "Fix the login bug in auth.mjs" } }\`
985
+ - "How does the config system work?" → \`{ "action": "agent.ask", "params": { "message": "Explain the config system" } }\`
986
+ - "Plan a refactor of the voice module" → \`{ "action": "agent.plan", "params": { "message": "Plan refactoring voice-relay.mjs" } }\`
987
+
988
+ ## Conversation Style
989
+
990
+ - Be **concise and conversational** — this is voice, not text.
991
+ - Lead with the answer, then add details if needed.
992
+ - For numbers, say them naturally: "You have 12 tasks in the backlog."
993
+ - When tasks or agents are busy, keep the user informed.
994
+ - For long outputs (code, logs), summarize the key points vocally.
995
+ - When delegating to an agent, let the user know: "I'm sending that to Codex now."
996
+
997
+ ## Error Handling
998
+
999
+ If an action fails, explain what happened and suggest alternatives.
1000
+ Never show raw error objects — speak the issue naturally.
1001
+
1002
+ ## Security
1003
+
1004
+ - Never expose API keys, tokens, or secrets in conversation.
1005
+ - Only execute safe operations via voice (reads, creates, delegates).
1006
+ - Dangerous operations (delete all tasks, force push) require explicit confirmation.
1007
+ `,
1008
+ voiceAgentCompact: `# Bosun Voice (Compact)
1009
+
1010
+ Voice assistant for VirtEngine. Access tasks, agents, workspace.
1011
+
1012
+ Return JSON actions: { "action": "<name>", "params": { ... } }
1013
+
1014
+ {{VOICE_ACTION_MANIFEST}}
1015
+
1016
+ Key actions: task.list, task.create, task.stats, agent.delegate, agent.ask, agent.plan,
1017
+ system.status, workspace.readFile, workspace.search.
1018
+
1019
+ Be concise. Lead with answers. Summarize long outputs.
925
1020
  `,
926
1021
  };
927
1022
 
@@ -0,0 +1,308 @@
1
+ import { classifyComplexity } from "./task-complexity.mjs";
2
+
3
+ export function normalizeTimestamp(value) {
4
+ const ts = Date.parse(value);
5
+ return Number.isFinite(ts) ? ts : null;
6
+ }
7
+
8
+ export function normalizeErrorFingerprint(message) {
9
+ const text = String(message || "unknown").trim().toLowerCase();
10
+ if (!text) return "unknown";
11
+ return text
12
+ .replace(/0x[0-9a-f]+/gi, "0x#")
13
+ .replace(/\d+(?:\.\d+)?/g, "#")
14
+ .replace(/\s+/g, " ")
15
+ .slice(0, 120);
16
+ }
17
+
18
+ function groupBy(array, keyFn) {
19
+ const groups = {};
20
+ for (const item of array) {
21
+ const key = typeof keyFn === "function" ? keyFn(item) : item[keyFn];
22
+ if (!groups[key]) groups[key] = [];
23
+ groups[key].push(item);
24
+ }
25
+ return groups;
26
+ }
27
+
28
+ function average(numbers) {
29
+ if (numbers.length === 0) return 0;
30
+ return numbers.reduce((a, b) => a + b, 0) / numbers.length;
31
+ }
32
+
33
+ function incrementCounter(target, key) {
34
+ const resolved = key || "unknown";
35
+ target[resolved] = (target[resolved] || 0) + 1;
36
+ }
37
+
38
+ function resolveKnownValue(...values) {
39
+ for (const value of values) {
40
+ if (value === null || value === undefined) continue;
41
+ const text = String(value).trim();
42
+ if (!text) continue;
43
+ if (text.toLowerCase() === "unknown") continue;
44
+ return text;
45
+ }
46
+ return "unknown";
47
+ }
48
+
49
+ function buildDistribution(counts, total) {
50
+ return Object.entries(counts)
51
+ .map(([label, count]) => ({
52
+ label,
53
+ count,
54
+ percent: total > 0 ? (count * 100.0) / total : 0,
55
+ }))
56
+ .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
57
+ }
58
+
59
+ const SIZE_LABEL_PATTERN = /\[(xs|s|m|l|xl|xxl|2xl)\]/i;
60
+
61
+ function extractSizeLabelFromTitle(title) {
62
+ const text = String(title || "");
63
+ const match = text.match(SIZE_LABEL_PATTERN);
64
+ if (!match) return "unknown";
65
+ const label = match[1].toLowerCase();
66
+ return label === "2xl" ? "xxl" : label;
67
+ }
68
+
69
+ function classifyComplexityBucket({ sizeLabel, title, description }) {
70
+ if (!description) return "unknown";
71
+ const normalizedSize = sizeLabel && sizeLabel !== "unknown" ? sizeLabel : null;
72
+ const result = classifyComplexity({
73
+ sizeLabel: normalizedSize,
74
+ title: title || "",
75
+ description: description || "",
76
+ });
77
+ return result?.tier || "unknown";
78
+ }
79
+
80
+ function resolveNowTimestamp(now) {
81
+ if (now instanceof Date) return now.getTime();
82
+ if (typeof now === "number" && Number.isFinite(now)) return now;
83
+ if (typeof now === "string") {
84
+ const parsed = normalizeTimestamp(now);
85
+ if (parsed !== null) return parsed;
86
+ }
87
+ return Date.now();
88
+ }
89
+
90
+ export function filterRecordsByWindow(
91
+ records,
92
+ { days, now, timestampKey = "timestamp" } = {},
93
+ ) {
94
+ if (!Array.isArray(records)) return [];
95
+ const windowDays = Number.isFinite(days) && days > 0 ? days : null;
96
+ if (!windowDays) return [...records];
97
+ const cutoff = resolveNowTimestamp(now) - windowDays * 24 * 60 * 60 * 1000;
98
+ return records.filter((record) => {
99
+ const ts = normalizeTimestamp(record?.[timestampKey]);
100
+ if (ts === null) return true;
101
+ return ts >= cutoff;
102
+ });
103
+ }
104
+
105
+ export function buildErrorClusters(errors) {
106
+ if (!Array.isArray(errors) || errors.length === 0) return [];
107
+
108
+ const byFingerprint = groupBy(
109
+ errors,
110
+ (e) =>
111
+ e.data?.error_fingerprint ||
112
+ normalizeErrorFingerprint(e.data?.error_message),
113
+ );
114
+
115
+ return Object.entries(byFingerprint)
116
+ .map(([fingerprint, events]) => ({
117
+ fingerprint,
118
+ count: events.length,
119
+ affected_tasks: new Set(events.map((e) => e.task_id)).size,
120
+ affected_attempts: new Set(events.map((e) => e.attempt_id)).size,
121
+ first_seen: events[0].timestamp,
122
+ last_seen: events[events.length - 1].timestamp,
123
+ sample_message: events[0].data?.error_message || "",
124
+ categories: [
125
+ ...new Set(events.map((e) => e.data?.error_category).filter(Boolean)),
126
+ ],
127
+ }))
128
+ .sort((a, b) => b.count - a.count);
129
+ }
130
+
131
+ function buildTaskProfiles(metrics, errors) {
132
+ const taskProfiles = new Map();
133
+ const ensureTaskProfile = (taskId) => {
134
+ const key = taskId || "unknown";
135
+ if (!taskProfiles.has(key)) {
136
+ taskProfiles.set(key, {
137
+ task_id: key,
138
+ task_title: "",
139
+ task_description: "",
140
+ executor: "unknown",
141
+ model: "unknown",
142
+ durations: [],
143
+ });
144
+ }
145
+ return taskProfiles.get(key);
146
+ };
147
+
148
+ for (const metric of metrics) {
149
+ const profile = ensureTaskProfile(metric.task_id || "unknown");
150
+ if (metric.task_title && !profile.task_title) {
151
+ profile.task_title = metric.task_title;
152
+ }
153
+ if (metric.task_description && !profile.task_description) {
154
+ profile.task_description = metric.task_description;
155
+ }
156
+ if (metric.executor && profile.executor === "unknown") {
157
+ profile.executor = metric.executor;
158
+ }
159
+ if (metric.model && profile.model === "unknown") {
160
+ profile.model = metric.model;
161
+ }
162
+ if (metric.metrics?.duration_ms) {
163
+ profile.durations.push(metric.metrics.duration_ms);
164
+ }
165
+ }
166
+
167
+ for (const error of errors) {
168
+ const profile = ensureTaskProfile(error.task_id || "unknown");
169
+ if (error.task_title && !profile.task_title) {
170
+ profile.task_title = error.task_title;
171
+ }
172
+ if (error.task_description && !profile.task_description) {
173
+ profile.task_description = error.task_description;
174
+ }
175
+ if (error.executor && profile.executor === "unknown") {
176
+ profile.executor = error.executor;
177
+ }
178
+ if (error.model && profile.model === "unknown") {
179
+ profile.model = error.model;
180
+ }
181
+ }
182
+
183
+ for (const profile of taskProfiles.values()) {
184
+ profile.size_label = extractSizeLabelFromTitle(profile.task_title);
185
+ profile.complexity = classifyComplexityBucket({
186
+ sizeLabel: profile.size_label,
187
+ title: profile.task_title,
188
+ description: profile.task_description,
189
+ });
190
+ profile.avg_duration_ms =
191
+ profile.durations.length > 0 ? average(profile.durations) : 0;
192
+ }
193
+
194
+ return taskProfiles;
195
+ }
196
+
197
+ function buildErrorCorrelationEntries(errors, taskProfiles) {
198
+ const correlations = new Map();
199
+ const ensureCorrelation = (fingerprint) => {
200
+ if (!correlations.has(fingerprint)) {
201
+ correlations.set(fingerprint, {
202
+ fingerprint,
203
+ count: 0,
204
+ task_ids: new Set(),
205
+ by_executor: {},
206
+ by_size: {},
207
+ by_complexity: {},
208
+ sample_message: "",
209
+ first_seen_ts: null,
210
+ last_seen_ts: null,
211
+ });
212
+ }
213
+ return correlations.get(fingerprint);
214
+ };
215
+
216
+ for (const error of errors) {
217
+ const fingerprint =
218
+ error.data?.error_fingerprint ||
219
+ normalizeErrorFingerprint(error.data?.error_message);
220
+ const entry = ensureCorrelation(fingerprint);
221
+ entry.count += 1;
222
+ entry.task_ids.add(error.task_id || "unknown");
223
+ if (!entry.sample_message && error.data?.error_message) {
224
+ entry.sample_message = error.data.error_message;
225
+ }
226
+
227
+ const timestamp = normalizeTimestamp(error.timestamp);
228
+ if (timestamp !== null) {
229
+ if (!entry.first_seen_ts || timestamp < entry.first_seen_ts) {
230
+ entry.first_seen_ts = timestamp;
231
+ }
232
+ if (!entry.last_seen_ts || timestamp > entry.last_seen_ts) {
233
+ entry.last_seen_ts = timestamp;
234
+ }
235
+ }
236
+
237
+ const profile = taskProfiles.get(error.task_id || "unknown");
238
+ const sizeLabel = resolveKnownValue(
239
+ profile?.size_label,
240
+ extractSizeLabelFromTitle(error.task_title),
241
+ );
242
+ const executor = resolveKnownValue(profile?.executor, error.executor);
243
+ const complexity = resolveKnownValue(profile?.complexity);
244
+
245
+ incrementCounter(entry.by_size, sizeLabel);
246
+ incrementCounter(entry.by_executor, executor);
247
+ incrementCounter(entry.by_complexity, complexity);
248
+ }
249
+
250
+ return correlations;
251
+ }
252
+
253
+ export function buildErrorCorrelationSummary({
254
+ errors = [],
255
+ metrics = [],
256
+ windowDays = 7,
257
+ top = 10,
258
+ } = {}) {
259
+ const windowDaysResolved =
260
+ Number.isFinite(windowDays) && windowDays > 0 ? windowDays : 7;
261
+ const topLimit = Number.isFinite(top) && top > 0 ? top : 10;
262
+ const errorList = Array.isArray(errors) ? errors : [];
263
+ const metricList = Array.isArray(metrics) ? metrics : [];
264
+
265
+ const taskProfiles = buildTaskProfiles(metricList, errorList);
266
+ const correlations = buildErrorCorrelationEntries(errorList, taskProfiles);
267
+ const sorted = [...correlations.values()].sort((a, b) => b.count - a.count);
268
+ const topEntries = sorted.slice(0, topLimit);
269
+
270
+ return {
271
+ window_days: windowDaysResolved,
272
+ total_errors: errorList.length,
273
+ total_fingerprints: correlations.size,
274
+ top: topLimit,
275
+ correlations: topEntries,
276
+ };
277
+ }
278
+
279
+ export function buildErrorCorrelationJsonPayload(summary, { now } = {}) {
280
+ const safeSummary = summary && typeof summary === "object" ? summary : null;
281
+ const correlations = Array.isArray(safeSummary?.correlations)
282
+ ? safeSummary.correlations
283
+ : [];
284
+ const generatedAt = new Date(resolveNowTimestamp(now)).toISOString();
285
+
286
+ return {
287
+ generated_at: generatedAt,
288
+ window_days: safeSummary?.window_days ?? 7,
289
+ total_errors: safeSummary?.total_errors ?? 0,
290
+ total_fingerprints: safeSummary?.total_fingerprints ?? 0,
291
+ top: safeSummary?.top ?? correlations.length,
292
+ correlations: correlations.map((entry) => ({
293
+ fingerprint: entry.fingerprint,
294
+ count: entry.count,
295
+ task_count: entry.task_ids?.size || 0,
296
+ sample_message: entry.sample_message,
297
+ first_seen: entry.first_seen_ts
298
+ ? new Date(entry.first_seen_ts).toISOString()
299
+ : null,
300
+ last_seen: entry.last_seen_ts
301
+ ? new Date(entry.last_seen_ts).toISOString()
302
+ : null,
303
+ by_executor: buildDistribution(entry.by_executor, entry.count),
304
+ by_size: buildDistribution(entry.by_size, entry.count),
305
+ by_complexity: buildDistribution(entry.by_complexity, entry.count),
306
+ })),
307
+ };
308
+ }