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
@@ -1,506 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import {
4
- addComment as addKanbanComment,
5
- getKanbanAdapter,
6
- getKanbanBackendName,
7
- updateTaskStatus,
8
- } from "./kanban-adapter.mjs";
9
-
10
- const execFileAsync = promisify(execFile);
11
- const TAG = "[gh-reconciler]";
12
-
13
- const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
14
- const DEFAULT_MERGED_LOOKBACK_HOURS = 72;
15
- const DEFAULT_MAX_BACKOFF_MS = 30 * 60 * 1000; // 30 minutes max backoff
16
-
17
- function parseNumber(value, fallback) {
18
- const parsed = Number(value);
19
- return Number.isFinite(parsed) ? parsed : fallback;
20
- }
21
-
22
- function parseRepoSlug(raw) {
23
- const text = String(raw || "").trim().replace(/^https?:\/\/github\.com\//i, "");
24
- if (!text) return "";
25
- const cleaned = text.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
26
- const [owner, repo] = cleaned.split("/", 2);
27
- if (!owner || !repo) return "";
28
- return `${owner}/${repo}`;
29
- }
30
-
31
- function parseIssueRefs(text) {
32
- const refs = new Set();
33
- const raw = String(text || "");
34
- const re = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)\b/gi;
35
- let match = re.exec(raw);
36
- while (match) {
37
- refs.add(String(match[1]));
38
- match = re.exec(raw);
39
- }
40
- return refs;
41
- }
42
-
43
- function parseIssueFromBranch(branchName) {
44
- const match = String(branchName || "").trim().match(/^ve\/(\d+)-/i);
45
- return match?.[1] || null;
46
- }
47
-
48
- function normalizeIssueLabels(issue) {
49
- const labels = Array.isArray(issue?.labels) ? issue.labels : [];
50
- return labels
51
- .map((label) =>
52
- typeof label === "string" ? label : String(label?.name || "").trim(),
53
- )
54
- .map((label) => label.toLowerCase())
55
- .filter(Boolean);
56
- }
57
-
58
- function isIssueInReview(issue) {
59
- const labels = new Set(normalizeIssueLabels(issue));
60
- return labels.has("inreview") || labels.has("in-review");
61
- }
62
-
63
- function isTrackingIssue(issue, trackingLabels) {
64
- const title = String(issue?.title || "").toLowerCase();
65
- if (title.includes("meta issue") || title.includes("tracker")) {
66
- return true;
67
- }
68
- const labels = new Set(normalizeIssueLabels(issue));
69
- for (const label of trackingLabels) {
70
- if (labels.has(label)) return true;
71
- }
72
- return false;
73
- }
74
-
75
- async function defaultGh(args) {
76
- const { stdout } = await execFileAsync("gh", args, {
77
- encoding: "utf8",
78
- maxBuffer: 20 * 1024 * 1024,
79
- timeout: 120_000,
80
- });
81
- const raw = String(stdout || "").trim();
82
- if (!raw) return [];
83
- return JSON.parse(raw);
84
- }
85
-
86
- function buildIssueMappings(openPrs, mergedPrs) {
87
- const map = new Map();
88
-
89
- function ensure(issueNumber) {
90
- if (!map.has(issueNumber)) {
91
- map.set(issueNumber, {
92
- openPrs: [],
93
- mergedPrs: [],
94
- });
95
- }
96
- return map.get(issueNumber);
97
- }
98
-
99
- function refsForPr(pr) {
100
- const refs = new Set();
101
- for (const issue of parseIssueRefs(pr?.title)) refs.add(issue);
102
- for (const issue of parseIssueRefs(pr?.body)) refs.add(issue);
103
- const fromBranch = parseIssueFromBranch(pr?.headRefName);
104
- if (fromBranch) refs.add(fromBranch);
105
- return refs;
106
- }
107
-
108
- for (const pr of openPrs) {
109
- for (const issueNumber of refsForPr(pr)) {
110
- ensure(issueNumber).openPrs.push(pr);
111
- }
112
- }
113
- for (const pr of mergedPrs) {
114
- for (const issueNumber of refsForPr(pr)) {
115
- ensure(issueNumber).mergedPrs.push(pr);
116
- }
117
- }
118
- return map;
119
- }
120
-
121
- export class GitHubReconciler {
122
- constructor(options = {}) {
123
- this.enabled = options.enabled !== false;
124
- this.intervalMs = Math.max(
125
- 30_000,
126
- parseNumber(options.intervalMs, DEFAULT_INTERVAL_MS),
127
- );
128
- this.mergedLookbackHours = Math.max(
129
- 1,
130
- parseNumber(
131
- options.mergedLookbackHours,
132
- DEFAULT_MERGED_LOOKBACK_HOURS,
133
- ),
134
- );
135
- this.repoSlug =
136
- parseRepoSlug(options.repoSlug) ||
137
- parseRepoSlug(process.env.GITHUB_REPOSITORY) ||
138
- parseRepoSlug(
139
- process.env.GITHUB_REPO_OWNER && process.env.GITHUB_REPO_NAME
140
- ? `${process.env.GITHUB_REPO_OWNER}/${process.env.GITHUB_REPO_NAME}`
141
- : "",
142
- ) ||
143
- "unknown/unknown";
144
- this.trackingLabels = new Set(
145
- (Array.isArray(options.trackingLabels)
146
- ? options.trackingLabels
147
- : String(options.trackingLabels || "tracking").split(",")
148
- )
149
- .map((value) => String(value || "").trim().toLowerCase())
150
- .filter(Boolean),
151
- );
152
- this.addComment = options.addComment || addKanbanComment;
153
- this.updateTaskStatus = options.updateTaskStatus || updateTaskStatus;
154
- this.gh = options.gh || defaultGh;
155
- this.sendTelegram = options.sendTelegram || null;
156
- this.timer = null;
157
- this.running = false;
158
- // Back-off state for rate-limit handling
159
- this._currentIntervalMs = this.intervalMs;
160
- this._maxBackoffMs = parseNumber(
161
- options.maxBackoffMs,
162
- DEFAULT_MAX_BACKOFF_MS,
163
- );
164
- }
165
-
166
- async _listOpenIssues() {
167
- return await this.gh([
168
- "issue",
169
- "list",
170
- "--repo",
171
- this.repoSlug,
172
- "--state",
173
- "open",
174
- "--limit",
175
- "200",
176
- "--json",
177
- "number,title,labels,url",
178
- ]);
179
- }
180
-
181
- async _listOpenPrs() {
182
- return await this.gh([
183
- "pr",
184
- "list",
185
- "--repo",
186
- this.repoSlug,
187
- "--state",
188
- "open",
189
- "--limit",
190
- "200",
191
- "--json",
192
- "number,title,body,headRefName,url",
193
- ]);
194
- }
195
-
196
- async _listMergedPrs() {
197
- const since = new Date(
198
- Date.now() - this.mergedLookbackHours * 60 * 60 * 1000,
199
- )
200
- .toISOString()
201
- .slice(0, 10);
202
- return await this.gh([
203
- "pr",
204
- "list",
205
- "--repo",
206
- this.repoSlug,
207
- "--state",
208
- "merged",
209
- "--search",
210
- `merged:>=${since}`,
211
- "--limit",
212
- "200",
213
- "--json",
214
- "number,title,body,headRefName,mergedAt,url",
215
- ]);
216
- }
217
-
218
- /**
219
- * Fetch issues that were closed within the merged lookback window.
220
- * These may have been auto-closed by GitHub when a PR merged while Bosun
221
- * was offline — they never went through the reconciler's normal path.
222
- */
223
- async _listRecentlyClosedIssues() {
224
- const since = new Date(
225
- Date.now() - this.mergedLookbackHours * 60 * 60 * 1000,
226
- )
227
- .toISOString()
228
- .slice(0, 10);
229
- try {
230
- return await this.gh([
231
- "issue",
232
- "list",
233
- "--repo",
234
- this.repoSlug,
235
- "--state",
236
- "closed",
237
- "--search",
238
- `closed:>=${since}`,
239
- "--limit",
240
- "200",
241
- "--json",
242
- "number,title,labels,url,closedAt",
243
- ]);
244
- } catch (err) {
245
- console.warn(`${TAG} failed to list recently-closed issues: ${err?.message || err}`);
246
- return [];
247
- }
248
- }
249
-
250
- async reconcileOnce() {
251
- const backend = String(getKanbanBackendName() || "").toLowerCase();
252
- if (!this.enabled) {
253
- return { status: "skipped", reason: "disabled" };
254
- }
255
- if (backend !== "github") {
256
- return { status: "skipped", reason: `backend=${backend || "unknown"}` };
257
- }
258
- if (!this.repoSlug || this.repoSlug === "unknown/unknown") {
259
- return { status: "skipped", reason: "missing-repo" };
260
- }
261
-
262
- const summary = {
263
- status: "ok",
264
- checked: 0,
265
- closed: 0,
266
- recentlyClosed: 0,
267
- inreview: 0,
268
- normalized: 0,
269
- skippedTracking: 0,
270
- projectMismatches: 0,
271
- errors: 0,
272
- };
273
-
274
- // Build a map of project board statuses for issues when in kanban mode
275
- /** @type {Map<string, string>} issueNumber → project board status */
276
- const projectStatusMap = new Map();
277
- const projectMode = String(process.env.GITHUB_PROJECT_MODE || "issues").trim().toLowerCase();
278
- if (projectMode === "kanban") {
279
- try {
280
- const adapter = getKanbanAdapter();
281
- if (typeof adapter.listTasksFromProject === "function") {
282
- const projectNumber =
283
- process.env.GITHUB_PROJECT_NUMBER ||
284
- process.env.GITHUB_PROJECT_ID ||
285
- null;
286
- if (projectNumber) {
287
- const projectTasks = await adapter.listTasksFromProject(projectNumber);
288
- for (const task of projectTasks) {
289
- if (task?.id && task?.status) {
290
- projectStatusMap.set(String(task.id), task.status);
291
- }
292
- }
293
- }
294
- }
295
- } catch (err) {
296
- console.warn(`${TAG} failed to read project board for reconciliation: ${err?.message || err}`);
297
- }
298
- }
299
-
300
- const [issuesRaw, openPrsRaw, mergedPrsRaw, recentlyClosedRaw] = await Promise.all([
301
- this._listOpenIssues(),
302
- this._listOpenPrs(),
303
- this._listMergedPrs(),
304
- this._listRecentlyClosedIssues(),
305
- ]);
306
-
307
- const issues = Array.isArray(issuesRaw) ? issuesRaw : [];
308
- const openPrs = Array.isArray(openPrsRaw) ? openPrsRaw : [];
309
- const mergedPrs = Array.isArray(mergedPrsRaw) ? mergedPrsRaw : [];
310
- const recentlyClosed = Array.isArray(recentlyClosedRaw) ? recentlyClosedRaw : [];
311
- const mappings = buildIssueMappings(openPrs, mergedPrs);
312
-
313
- for (const issue of issues) {
314
- const issueNumber = String(issue?.number || "").trim();
315
- if (!issueNumber) continue;
316
- summary.checked += 1;
317
- const mapped = mappings.get(issueNumber) || {
318
- openPrs: [],
319
- mergedPrs: [],
320
- };
321
- const hasOpenPr = mapped.openPrs.length > 0;
322
- const hasMergedPr = mapped.mergedPrs.length > 0;
323
-
324
- try {
325
- if (hasMergedPr) {
326
- if (isTrackingIssue(issue, this.trackingLabels)) {
327
- summary.skippedTracking += 1;
328
- continue;
329
- }
330
- await this.updateTaskStatus(issueNumber, "done");
331
- if (this.addComment) {
332
- const mergedUrls = mapped.mergedPrs
333
- .slice(0, 3)
334
- .map((pr) => pr?.url)
335
- .filter(Boolean);
336
- const suffix =
337
- mergedUrls.length > 0
338
- ? `\n\nMerged PR(s):\n${mergedUrls.map((url) => `- ${url}`).join("\n")}`
339
- : "";
340
- await this.addComment(
341
- issueNumber,
342
- `## :check: Auto-Reconciled\nThis issue was auto-closed by bosun after detecting merged PR linkage.${suffix}`,
343
- );
344
- }
345
- summary.closed += 1;
346
- continue;
347
- }
348
-
349
- if (hasOpenPr) {
350
- if (!isIssueInReview(issue)) {
351
- await this.updateTaskStatus(issueNumber, "inreview");
352
- summary.inreview += 1;
353
- }
354
- continue;
355
- }
356
-
357
- if (isIssueInReview(issue)) {
358
- await this.updateTaskStatus(issueNumber, "todo");
359
- summary.normalized += 1;
360
- continue;
361
- }
362
-
363
- // Project board mismatch detection (kanban mode only)
364
- if (projectStatusMap.size > 0) {
365
- const projectStatus = projectStatusMap.get(issueNumber);
366
- if (projectStatus) {
367
- const issueStatus = isIssueInReview(issue) ? "inreview" : "todo";
368
- if (projectStatus !== issueStatus && projectStatus !== "todo") {
369
- // Project board says a different status than issue labels — reconcile
370
- try {
371
- await this.updateTaskStatus(issueNumber, projectStatus);
372
- summary.projectMismatches += 1;
373
- } catch (syncErr) {
374
- console.warn(
375
- `${TAG} failed to sync project status for #${issueNumber}: ${syncErr?.message || syncErr}`,
376
- );
377
- }
378
- }
379
- }
380
- }
381
- } catch (err) {
382
- summary.errors += 1;
383
- console.warn(
384
- `${TAG} failed reconciling issue #${issueNumber}: ${err?.message || err}`,
385
- );
386
- }
387
- }
388
-
389
- // Second pass: recently-closed issues that may have been auto-closed by GitHub
390
- // (when a PR merged while Bosun was offline). Ensure project board is synced to "done".
391
- for (const issue of recentlyClosed) {
392
- const issueNumber = String(issue?.number || "").trim();
393
- if (!issueNumber) continue;
394
- const mapped = mappings.get(issueNumber);
395
- const hasMergedPr = mapped && mapped.mergedPrs.length > 0;
396
- if (!hasMergedPr) continue;
397
- if (isTrackingIssue(issue, this.trackingLabels)) continue;
398
- try {
399
- // updateTaskStatus("done") on an already-closed issue is a no-op for the issue
400
- // itself but triggers project board sync when in kanban mode.
401
- await this.updateTaskStatus(issueNumber, "done");
402
- summary.recentlyClosed += 1;
403
- } catch (err) {
404
- summary.errors += 1;
405
- console.warn(
406
- `${TAG} failed syncing recently-closed issue #${issueNumber}: ${err?.message || err}`,
407
- );
408
- }
409
- }
410
-
411
- console.log(
412
- `${TAG} cycle complete: checked=${summary.checked} closed=${summary.closed} recentlyClosed=${summary.recentlyClosed} inreview=${summary.inreview} normalized=${summary.normalized} skippedTracking=${summary.skippedTracking} projectMismatches=${summary.projectMismatches} errors=${summary.errors}`,
413
- );
414
- return summary;
415
- }
416
-
417
- start() {
418
- if (this.running) return this;
419
- this.running = true;
420
- if (!this.repoSlug || this.repoSlug === "unknown/unknown") {
421
- console.warn(`${TAG} disabled (missing repo slug)`);
422
- return this;
423
- }
424
- console.log(
425
- `${TAG} started (repo=${this.repoSlug}, interval=${this.intervalMs}ms, lookback=${this.mergedLookbackHours}h)`,
426
- );
427
- this._scheduleNext(0); // run immediately on start
428
- return this;
429
- }
430
-
431
- /** Schedule the next reconcile cycle after `delayMs` milliseconds. */
432
- _scheduleNext(delayMs) {
433
- if (!this.running) return;
434
- this.timer = setTimeout(() => {
435
- this.reconcileOnce()
436
- .then(() => {
437
- // Success: reset backoff to base interval
438
- if (this._currentIntervalMs !== this.intervalMs) {
439
- console.log(
440
- `${TAG} rate-limit backoff cleared — restoring interval to ${this.intervalMs}ms`,
441
- );
442
- this._currentIntervalMs = this.intervalMs;
443
- }
444
- this._scheduleNext(this._currentIntervalMs);
445
- })
446
- .catch((err) => {
447
- const msg = String(err?.message || err);
448
- console.warn(`${TAG} cycle failed: ${msg}`);
449
- const isRateLimit =
450
- msg.includes("rate limit") ||
451
- msg.includes("API rate limit") ||
452
- msg.includes("rate-limit");
453
- if (isRateLimit) {
454
- // Exponential back-off: double interval up to the max
455
- this._currentIntervalMs = Math.min(
456
- this._currentIntervalMs * 2,
457
- this._maxBackoffMs,
458
- );
459
- console.warn(
460
- `${TAG} rate-limit detected — backing off to ${this._currentIntervalMs}ms`,
461
- );
462
- }
463
- if (this.sendTelegram) {
464
- void Promise.resolve(
465
- this.sendTelegram(
466
- `:alert: GitHub reconciler cycle failed: ${msg}`,
467
- ),
468
- ).catch(() => {});
469
- }
470
- this._scheduleNext(this._currentIntervalMs);
471
- });
472
- }, delayMs);
473
- if (this.timer?.unref) this.timer.unref();
474
- }
475
-
476
- stop() {
477
- if (this.timer) {
478
- clearTimeout(this.timer);
479
- this.timer = null;
480
- }
481
- this.running = false;
482
- console.log(`${TAG} stopped`);
483
- }
484
- }
485
-
486
- let _singleton = null;
487
-
488
- export function startGitHubReconciler(options = {}) {
489
- if (_singleton) {
490
- _singleton.stop();
491
- }
492
- _singleton = new GitHubReconciler(options);
493
- return _singleton.start();
494
- }
495
-
496
- export function stopGitHubReconciler() {
497
- if (_singleton) {
498
- _singleton.stop();
499
- _singleton = null;
500
- }
501
- }
502
-
503
- export async function runGitHubReconcilerOnce(options = {}) {
504
- const reconciler = new GitHubReconciler(options);
505
- return await reconciler.reconcileOnce();
506
- }