bosun 0.32.0 → 0.33.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 (56) hide show
  1. package/.env.example +5 -5
  2. package/README.md +3 -0
  3. package/agent-prompts.mjs +98 -2
  4. package/anomaly-detector.mjs +54 -0
  5. package/codex-config.mjs +104 -0
  6. package/github-app-auth.mjs +7 -7
  7. package/github-auth-manager.mjs +2 -2
  8. package/github-oauth-portal.mjs +7 -7
  9. package/monitor.mjs +3 -3
  10. package/package.json +13 -4
  11. package/postinstall.mjs +17 -0
  12. package/setup-web-server.mjs +388 -5
  13. package/task-executor.mjs +4 -4
  14. package/ui/app.js +418 -38
  15. package/ui/app.monolith.js +4 -4
  16. package/ui/components/agent-selector.js +119 -11
  17. package/ui/components/chat-view.js +10 -9
  18. package/ui/components/diff-viewer.js +13 -12
  19. package/ui/components/forms.js +1 -1
  20. package/ui/components/kanban-board.js +172 -15
  21. package/ui/components/session-list.js +6 -5
  22. package/ui/components/workspace-switcher.js +5 -4
  23. package/ui/demo.html +1819 -310
  24. package/ui/index.html +39 -29
  25. package/ui/modules/icon-utils.js +183 -0
  26. package/ui/modules/icons.js +554 -0
  27. package/ui/modules/router.js +1 -0
  28. package/ui/modules/settings-schema.js +13 -13
  29. package/ui/modules/voice.js +294 -0
  30. package/ui/setup.html +122 -24
  31. package/ui/styles/base.css +4 -1
  32. package/ui/styles/components.css +1064 -65
  33. package/ui/styles/kanban.css +245 -0
  34. package/ui/styles/layout.css +330 -8
  35. package/ui/styles/sessions.css +243 -32
  36. package/ui/styles/variables.css +37 -0
  37. package/ui/styles/workspace-switcher.css +6 -0
  38. package/ui/styles.css +6 -0
  39. package/ui/tabs/agents.js +23 -22
  40. package/ui/tabs/chat.js +36 -13
  41. package/ui/tabs/control.js +5 -6
  42. package/ui/tabs/dashboard.js +19 -12
  43. package/ui/tabs/infra.js +11 -10
  44. package/ui/tabs/logs.js +11 -10
  45. package/ui/tabs/settings.js +17 -16
  46. package/ui/tabs/tasks.js +618 -48
  47. package/ui/tabs/workflows.js +1588 -0
  48. package/ui/vendor/es-module-shims.js +1063 -0
  49. package/ui/vendor/htm.js +1 -0
  50. package/ui/vendor/preact-compat.js +2 -0
  51. package/ui/vendor/preact-hooks.js +2 -0
  52. package/ui/vendor/preact-signals-core.js +1 -0
  53. package/ui/vendor/preact-signals.js +1 -0
  54. package/ui/vendor/preact.js +2 -0
  55. package/ui-server.mjs +355 -12
  56. package/vendor-sync.mjs +241 -0
package/.env.example CHANGED
@@ -332,9 +332,9 @@ TELEGRAM_MINIAPP_ENABLED=false
332
332
  # by the next agent working on that branch. Default: true
333
333
  # TASK_UPSTREAM_SYNC_MAIN=true
334
334
 
335
- # ─── GitHub App (Bosun[botswain] Identity + Auth) ────────────────────────────
336
- # App: https://github.com/apps/bosun-botswain (slug: bosun-botswain)
337
- # Bot identity: bosun-botswain[bot] (appears as contributor on every agent commit)
335
+ # ─── GitHub App (Bosun[VE] Identity + Auth) ────────────────────────────
336
+ # App: https://github.com/apps/bosun-ve (slug: bosun-ve)
337
+ # Bot identity: bosun-ve[bot] (appears as contributor on every agent commit)
338
338
  #
339
339
  # Numeric App ID (shown on the App settings page under "About"):
340
340
  # BOSUN_GITHUB_APP_ID=2911413
@@ -350,9 +350,9 @@ TELEGRAM_MINIAPP_ENABLED=false
350
350
  # BOSUN_GITHUB_WEBHOOK_SECRET=
351
351
  #
352
352
  # Path to the PEM private key downloaded from App settings → Generate a private key:
353
- # BOSUN_GITHUB_PRIVATE_KEY_PATH=/path/to/bosun-botswain.pem
353
+ # BOSUN_GITHUB_PRIVATE_KEY_PATH=/path/to/bosun-ve.pem
354
354
  #
355
- # ─── GitHub App Settings (enable all three in https://github.com/settings/apps/bosun-botswain) ────
355
+ # ─── GitHub App Settings (enable all three in https://github.com/settings/apps/bosun-ve) ────
356
356
  # ✅ Callback URL → http://127.0.0.1:54317/github/callback (set this FIRST, then Save)
357
357
  # ✅ "Request user authorization (OAuth) during installation" → ON
358
358
  # GitHub does OAuth at install time, redirecting to the Callback URL with
package/README.md CHANGED
@@ -36,6 +36,7 @@ bosun --setup
36
36
  ```
37
37
 
38
38
  Requires:
39
+
39
40
  - Node.js 18+
40
41
  - Git
41
42
  - Bash (for `.sh` wrappers) or PowerShell 7+ (for `.ps1` wrappers)
@@ -60,6 +61,7 @@ Requires:
60
61
  **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.
61
62
 
62
63
  Key references:
64
+
63
65
  - [GitHub adapter enhancements](_docs/KANBAN_GITHUB_ENHANCEMENT.md)
64
66
  - [GitHub Projects v2 index](_docs/GITHUB_PROJECTS_V2_INDEX.md)
65
67
  - [GitHub Projects v2 quickstart](_docs/GITHUB_PROJECTS_V2_QUICKSTART.md)
@@ -79,6 +81,7 @@ Bosun enforces a strict quality pipeline in both local hooks and CI:
79
81
 
80
82
  - **Pre-commit hooks** auto-format and lint staged files.
81
83
  - **Pre-push hooks** run targeted checks based on changed files (Go, portal, docs).
84
+ - **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.
82
85
  - **Prepublish checks** validate package contents and release readiness.
83
86
 
84
87
  Local commands you can run any time:
package/agent-prompts.mjs CHANGED
@@ -119,6 +119,12 @@ const PROMPT_DEFS = [
119
119
  description:
120
120
  "Task management agent prompt with full CRUD access via CLI and REST API.",
121
121
  },
122
+ {
123
+ key: "frontendAgent",
124
+ filename: "frontend-agent.md",
125
+ description:
126
+ "Front-end specialist agent with screenshot-based validation and visual verification.",
127
+ },
122
128
  ];
123
129
 
124
130
  export const AGENT_PROMPT_DEFINITIONS = Object.freeze(
@@ -604,6 +610,75 @@ Types: feat, fix, docs, refactor, test, chore
604
610
  Statuses: draft → todo → inprogress → inreview → done
605
611
 
606
612
  See .bosun/agents/task-manager.md for full documentation.
613
+ `,
614
+ frontendAgent: `# Frontend Specialist Agent
615
+
616
+ You are a **front-end development specialist** agent managed by Bosun.
617
+
618
+ ## Core Responsibilities
619
+
620
+ 1. Implement HTML, CSS, and JavaScript/TypeScript UI changes
621
+ 2. Build responsive, accessible UI components
622
+ 3. Ensure visual accuracy matching specifications
623
+ 4. Validate changes through automated testing AND visual verification
624
+
625
+ ## Special Skills
626
+
627
+ - CSS Grid/Flexbox layout
628
+ - Component architecture (React, Preact, Vue, Svelte, vanilla)
629
+ - Responsive design (mobile-first)
630
+ - Accessibility (WCAG 2.1 AA)
631
+ - CSS animations and transitions
632
+ - Design system adherence
633
+
634
+ ## CRITICAL: Evidence-Based Validation
635
+
636
+ After completing implementation, you MUST collect visual evidence:
637
+
638
+ ### Screenshot Protocol
639
+ 1. Start the dev server if not already running
640
+ 2. Navigate to every page/component you modified
641
+ 3. Take screenshots at THREE viewport sizes:
642
+ - Desktop (1920×1080)
643
+ - Tablet (768×1024)
644
+ - Mobile (375×812)
645
+ 4. Save ALL screenshots to \`.bosun/evidence/\` directory
646
+ 5. Use descriptive filenames: \`<page>-<viewport>-<timestamp>.png\`
647
+ 6. Also screenshot any interactive states (modals, dropdowns, hover states)
648
+
649
+ ### Evidence Naming Convention
650
+ \`\`\`
651
+ .bosun/evidence/
652
+ homepage-desktop-1234567890.png
653
+ homepage-tablet-1234567890.png
654
+ homepage-mobile-1234567890.png
655
+ modal-open-desktop-1234567890.png
656
+ dark-mode-desktop-1234567890.png
657
+ \`\`\`
658
+
659
+ ## Workflow
660
+ 1. Read task requirements and any linked designs/specs
661
+ 2. Load relevant skills from \`.bosun/skills/\`
662
+ 3. Implement frontend changes
663
+ 4. Run build: \`npm run build\` (zero errors AND zero warnings)
664
+ 5. Run lint: \`npm run lint\`
665
+ 6. Run tests: \`npm test\`
666
+ 7. Start dev server and collect screenshots (see protocol above)
667
+ 8. Commit with conventional format: \`feat(ui): ...\` or \`fix(ui): ...\`
668
+ 9. Push branch
669
+
670
+ ## IMPORTANT: Do NOT mark the task complete
671
+ The Bosun workflow engine handles completion verification.
672
+ An independent model will review your screenshots against the task
673
+ requirements before the task is marked as done.
674
+
675
+ ## Task Context
676
+ - Task: {{TASK_TITLE}}
677
+ - Description: {{TASK_DESCRIPTION}}
678
+ - Branch: {{BRANCH}}
679
+ - Working Directory: {{WORKTREE_PATH}}
680
+
681
+ {{COAUTHOR_INSTRUCTION}}
607
682
  `,
608
683
  };
609
684
 
@@ -670,19 +745,40 @@ export function getDefaultPromptTemplate(key) {
670
745
  return DEFAULT_PROMPTS[key] || "";
671
746
  }
672
747
 
673
- export function renderPromptTemplate(template, values = {}) {
748
+ export function renderPromptTemplate(template, values = {}, rootDir) {
674
749
  if (typeof template !== "string") return "";
675
750
  const normalized = {};
676
751
  for (const [k, v] of Object.entries(values || {})) {
677
752
  normalized[String(k).trim().toUpperCase()] = normalizeTemplateValue(v);
678
753
  }
679
754
 
680
- return template.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (full, key) => {
755
+ // Resolve namespaced library refs: {{prompt:name}}, {{agent:name}}, {{skill:name}}
756
+ let result = template;
757
+ if (rootDir && _libraryResolver) {
758
+ try {
759
+ result = _libraryResolver(result, rootDir, {});
760
+ } catch {
761
+ // library resolver failed — skip namespaced refs
762
+ }
763
+ }
764
+
765
+ return result.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (full, key) => {
681
766
  const hit = normalized[String(key).toUpperCase()];
682
767
  return hit == null ? "" : hit;
683
768
  });
684
769
  }
685
770
 
771
+ /**
772
+ * Register the library reference resolver. Called by library-manager or at
773
+ * startup to enable {{prompt:name}}, {{agent:name}}, {{skill:name}} syntax.
774
+ *
775
+ * @param {(template: string, rootDir: string, vars: Object) => string} resolver
776
+ */
777
+ let _libraryResolver = null;
778
+ export function setLibraryResolver(resolver) {
779
+ _libraryResolver = typeof resolver === "function" ? resolver : null;
780
+ }
781
+
686
782
  export function resolvePromptTemplate(template, values, fallback) {
687
783
  const base = typeof fallback === "string" ? fallback : "";
688
784
  if (typeof template !== "string" || !template.trim()) return base;
@@ -1211,3 +1211,57 @@ export function createAnomalyDetector(options = {}) {
1211
1211
  */
1212
1212
 
1213
1213
  export default AnomalyDetector;
1214
+
1215
+ // ── Workflow Engine Integration ─────────────────────────────────────────────
1216
+
1217
+ /**
1218
+ * Registered workflow engine reference for anomaly → workflow triggers.
1219
+ * @type {import("./workflow-engine.mjs").WorkflowEngine | null}
1220
+ */
1221
+ let _workflowEngine = null;
1222
+
1223
+ /**
1224
+ * Register a workflow engine to receive anomaly trigger events.
1225
+ * When set, every anomaly emitted will also call engine.evaluateTriggers("anomaly", anomalyData).
1226
+ *
1227
+ * @param {import("./workflow-engine.mjs").WorkflowEngine} engine
1228
+ */
1229
+ export function setWorkflowEngine(engine) {
1230
+ _workflowEngine = engine;
1231
+ }
1232
+
1233
+ /** Get the currently registered workflow engine (or null) */
1234
+ export function getWorkflowEngine() {
1235
+ return _workflowEngine;
1236
+ }
1237
+
1238
+ /**
1239
+ * Create an onAnomaly callback that also fires workflow triggers.
1240
+ * Wraps the user's original callback and additionally invokes
1241
+ * evaluateTriggers on the registered workflow engine.
1242
+ *
1243
+ * @param {(anomaly: Anomaly) => void} [originalCallback]
1244
+ * @returns {(anomaly: Anomaly) => void}
1245
+ */
1246
+ export function wrapAnomalyCallback(originalCallback) {
1247
+ return (anomaly) => {
1248
+ // Always call the original callback
1249
+ if (originalCallback) originalCallback(anomaly);
1250
+
1251
+ // Fire workflow triggers if engine is registered
1252
+ if (_workflowEngine?.evaluateTriggers) {
1253
+ try {
1254
+ _workflowEngine.evaluateTriggers("anomaly", {
1255
+ anomalyType: anomaly.type,
1256
+ severity: anomaly.severity,
1257
+ agentId: anomaly.processId || anomaly.shortId,
1258
+ message: anomaly.message,
1259
+ action: anomaly.action,
1260
+ taskTitle: anomaly.taskTitle,
1261
+ data: anomaly.data,
1262
+ timestamp: Date.now(),
1263
+ });
1264
+ } catch { /* workflow trigger errors should not break anomaly detection */ }
1265
+ }
1266
+ };
1267
+ }
package/codex-config.mjs CHANGED
@@ -1618,6 +1618,110 @@ export function printConfigSummary(result, log = console.log) {
1618
1618
  log(` Config: ${result.path}`);
1619
1619
  }
1620
1620
 
1621
+ // ── Trusted Projects ─────────────────────────────────────────────────────────
1622
+
1623
+ /**
1624
+ * Escape a string for use inside a double-quoted TOML basic string.
1625
+ * Handles backslashes (Windows paths) and double-quote characters.
1626
+ */
1627
+ function tomlEscapeStr(s) {
1628
+ return String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1629
+ }
1630
+
1631
+ /**
1632
+ * Format an array of strings as a TOML array literal, correctly escaping
1633
+ * backslashes so Windows paths are stored faithfully.
1634
+ *
1635
+ * Example output: ["C:\\Users\\jon\\bosun", "/home/jon/bosun"]
1636
+ */
1637
+ function formatTomlArrayEscaped(values) {
1638
+ return `[${values.map((v) => `"${tomlEscapeStr(v)}"`).join(", ")}]`;
1639
+ }
1640
+
1641
+ /**
1642
+ * Parse a TOML basic-string array literal, unescaping backslash sequences.
1643
+ */
1644
+ function parseTomlArrayLiteralEscaped(raw) {
1645
+ if (!raw) return [];
1646
+ const inner = raw.trim().replace(/^\[/, "").replace(/\]$/, "");
1647
+ if (!inner.trim()) return [];
1648
+ // Split on commas that are NOT inside quotes
1649
+ const items = [];
1650
+ let buf = "";
1651
+ let inStr = false;
1652
+ for (let i = 0; i < inner.length; i++) {
1653
+ const ch = inner[i];
1654
+ if (ch === "\\" && inStr) { buf += ch + (inner[++i] || ""); continue; }
1655
+ if (ch === '"') { inStr = !inStr; buf += ch; continue; }
1656
+ if (ch === "," && !inStr) { items.push(buf.trim()); buf = ""; continue; }
1657
+ buf += ch;
1658
+ }
1659
+ if (buf.trim()) items.push(buf.trim());
1660
+ return items
1661
+ .map((item) => item.replace(/^"(.*)"$/s, "$1")) // strip outer quotes
1662
+ .map((item) => item.replace(/\\(["\\])/g, "$1")) // unescape \" and \\
1663
+ .filter(Boolean);
1664
+ }
1665
+
1666
+ /**
1667
+ * Ensure the given directory paths are listed in the `trusted_projects`
1668
+ * top-level key in ~/.codex/config.toml.
1669
+ *
1670
+ * Codex refuses to load a per-project .codex/config.toml unless the project
1671
+ * directory appears in this list — producing warnings like:
1672
+ * "⚠ Project config.toml files are disabled … add <dir> as a trusted project"
1673
+ *
1674
+ * Paths are stored as-is (forward or back slashes preserved) with proper TOML
1675
+ * escaping so Windows paths survive round-trips through the file.
1676
+ *
1677
+ * @param {string[]} paths Absolute directories to trust (e.g. [bosunHome])
1678
+ * @param {{ dryRun?: boolean }} [opts]
1679
+ * @returns {{ added: string[], already: string[], path: string }}
1680
+ */
1681
+ export function ensureTrustedProjects(paths, { dryRun = false } = {}) {
1682
+ const result = { added: [], already: [], path: CONFIG_PATH };
1683
+ const desired = (paths || []).map((p) => resolve(p)).filter(Boolean);
1684
+ if (desired.length === 0) return result;
1685
+
1686
+ let toml = readCodexConfig() || "";
1687
+
1688
+ // Parse existing trusted_projects (multi-line arrays may span lines)
1689
+ const existingMatch = toml.match(/^trusted_projects\s*=\s*(\[[^\]]*\])/m);
1690
+ const existing = existingMatch ? parseTomlArrayLiteralEscaped(existingMatch[1]) : [];
1691
+
1692
+ let changed = false;
1693
+ for (const p of desired) {
1694
+ if (existing.includes(p)) {
1695
+ result.already.push(p);
1696
+ } else {
1697
+ existing.push(p);
1698
+ result.added.push(p);
1699
+ changed = true;
1700
+ }
1701
+ }
1702
+
1703
+ if (!changed) return result;
1704
+ if (dryRun) return result;
1705
+
1706
+ const newLine = `trusted_projects = ${formatTomlArrayEscaped(existing)}`;
1707
+
1708
+ if (existingMatch) {
1709
+ toml = toml.replace(/^trusted_projects\s*=\s*\[[^\]]*\]/m, newLine);
1710
+ } else {
1711
+ // Insert before the first section header (or at top if no sections)
1712
+ const firstSection = toml.search(/^\[/m);
1713
+ if (firstSection === -1) {
1714
+ toml = `${newLine}\n${toml}`;
1715
+ } else {
1716
+ toml = `${toml.slice(0, firstSection)}${newLine}\n\n${toml.slice(firstSection)}`;
1717
+ }
1718
+ }
1719
+
1720
+ mkdirSync(CODEX_DIR, { recursive: true });
1721
+ writeFileSync(CONFIG_PATH, toml, "utf8");
1722
+ return result;
1723
+ }
1724
+
1621
1725
  // ── Internal Helpers ─────────────────────────────────────────────────────────
1622
1726
 
1623
1727
  function escapeRegex(str) {
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * github-app-auth.mjs — GitHub App JWT + Installation Token helpers
4
4
  *
5
- * Provides credential helpers for the Bosun[botswain] GitHub App:
5
+ * Provides credential helpers for the Bosun[VE] GitHub App:
6
6
  * - signAppJWT() — RS256 JWT proving Bosun IS the app
7
7
  * - getInstallationToken(installationId) — short-lived install access token
8
8
  * - getInstallationTokenForRepo(owner,repo) — auto-resolves install from repo
@@ -90,7 +90,7 @@ export async function getInstallationToken(installationId) {
90
90
  Authorization: `Bearer ${jwt}`,
91
91
  Accept: "application/vnd.github+json",
92
92
  "X-GitHub-Api-Version": "2022-11-28",
93
- "User-Agent": "bosun-botswain",
93
+ "User-Agent": "bosun-ve",
94
94
  },
95
95
  },
96
96
  );
@@ -123,7 +123,7 @@ export async function getInstallationTokenForRepo(owner, repo) {
123
123
  Authorization: `Bearer ${jwt}`,
124
124
  Accept: "application/vnd.github+json",
125
125
  "X-GitHub-Api-Version": "2022-11-28",
126
- "User-Agent": "bosun-botswain",
126
+ "User-Agent": "bosun-ve",
127
127
  },
128
128
  },
129
129
  );
@@ -163,7 +163,7 @@ export async function startDeviceFlow(scope = "repo") {
163
163
  headers: {
164
164
  Accept: "application/json",
165
165
  "Content-Type": "application/x-www-form-urlencoded",
166
- "User-Agent": "bosun-botswain",
166
+ "User-Agent": "bosun-ve",
167
167
  },
168
168
  body: body.toString(),
169
169
  });
@@ -217,7 +217,7 @@ export async function pollDeviceToken(deviceCode) {
217
217
  headers: {
218
218
  Accept: "application/json",
219
219
  "Content-Type": "application/x-www-form-urlencoded",
220
- "User-Agent": "bosun-botswain",
220
+ "User-Agent": "bosun-ve",
221
221
  },
222
222
  body: body.toString(),
223
223
  });
@@ -281,7 +281,7 @@ export async function exchangeOAuthCode(code) {
281
281
  headers: {
282
282
  Accept: "application/json",
283
283
  "Content-Type": "application/x-www-form-urlencoded",
284
- "User-Agent": "bosun-botswain",
284
+ "User-Agent": "bosun-ve",
285
285
  },
286
286
  body: body.toString(),
287
287
  });
@@ -318,7 +318,7 @@ export async function getOAuthUser(accessToken) {
318
318
  Authorization: `Bearer ${accessToken}`,
319
319
  Accept: "application/vnd.github+json",
320
320
  "X-GitHub-Api-Version": "2022-11-28",
321
- "User-Agent": "bosun-botswain",
321
+ "User-Agent": "bosun-ve",
322
322
  },
323
323
  });
324
324
  if (!res.ok) {
@@ -82,7 +82,7 @@ async function verifyToken(token) {
82
82
  Authorization: `Bearer ${token}`,
83
83
  Accept: "application/vnd.github+json",
84
84
  "X-GitHub-Api-Version": "2022-11-28",
85
- "User-Agent": "bosun-botswain",
85
+ "User-Agent": "bosun-ve",
86
86
  },
87
87
  });
88
88
  if (!res.ok) return null;
@@ -162,7 +162,7 @@ export async function getAuthHeaders(options = {}) {
162
162
  Authorization: `Bearer ${token}`,
163
163
  Accept: "application/vnd.github+json",
164
164
  "X-GitHub-Api-Version": "2022-11-28",
165
- "User-Agent": "bosun-botswain",
165
+ "User-Agent": "bosun-ve",
166
166
  };
167
167
  }
168
168
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * github-oauth-portal.mjs — Self-contained OAuth setup portal for Bosun[botswain]
2
+ * github-oauth-portal.mjs — Self-contained OAuth setup portal for Bosun[VE]
3
3
  *
4
4
  * Serves a local HTTP portal on port 54317 (bound to 127.0.0.1) that guides
5
5
  * users through GitHub App installation and OAuth authorisation.
@@ -13,7 +13,7 @@
13
13
  * GET /api/status — JSON status endpoint
14
14
  * GET /api/installations — list App installations (requires App JWT)
15
15
  *
16
- * GitHub App settings (https://github.com/settings/apps/bosun-botswain):
16
+ * GitHub App settings (https://github.com/settings/apps/bosun-ve):
17
17
  * Callback URL: http://127.0.0.1:54317/github/callback ← ONLY URL needed
18
18
  *
19
19
  * Notes:
@@ -54,7 +54,7 @@ import {
54
54
 
55
55
  const DEFAULT_PORT = 54317;
56
56
  const DEFAULT_HOST = "127.0.0.1";
57
- const GITHUB_APP_NAME = "bosun-botswain";
57
+ const GITHUB_APP_NAME = "bosun-ve";
58
58
  const GITHUB_APP_URL = `https://github.com/apps/${GITHUB_APP_NAME}`;
59
59
  const GITHUB_APP_INSTALL_URL = `${GITHUB_APP_URL}/installations/new`;
60
60
  const STATE_FILE = join(homedir(), ".bosun", "github-auth-state.json");
@@ -271,7 +271,7 @@ function htmlPage(title, bodyHtml) {
271
271
  <body>
272
272
  <div class="logo">⚓</div>
273
273
  <h1>Bosun</h1>
274
- <div class="subtitle">GitHub App OAuth Setup Portal · <code>bosun-botswain</code></div>
274
+ <div class="subtitle">GitHub App OAuth Setup Portal · <code>bosun-ve</code></div>
275
275
  ${bodyHtml}
276
276
  <script>
277
277
  function copyToClipboard(text, btn) {
@@ -569,7 +569,7 @@ async function handleSetup(req, res) {
569
569
  Authorization: `Bearer ${jwt}`,
570
570
  Accept: "application/vnd.github+json",
571
571
  "X-GitHub-Api-Version": "2022-11-28",
572
- "User-Agent": "bosun-botswain",
572
+ "User-Agent": "bosun-ve",
573
573
  },
574
574
  },
575
575
  );
@@ -703,7 +703,7 @@ function handleWebhookEvent(eventType, payload) {
703
703
  }
704
704
 
705
705
  const CMD_RE = /\/bosun[ \t]+(\w+)(?:[ \t]+(\S+))?/g;
706
- const MENTION_RE = /@bosun-botswain/i;
706
+ const MENTION_RE = /@bosun-ve/i;
707
707
 
708
708
  function processBosunCommand(body, payload) {
709
709
  if (MENTION_RE.test(body)) {
@@ -766,7 +766,7 @@ async function handleApiInstallations(req, res) {
766
766
  Authorization: `Bearer ${jwt}`,
767
767
  Accept: "application/vnd.github+json",
768
768
  "X-GitHub-Api-Version": "2022-11-28",
769
- "User-Agent": "bosun-botswain",
769
+ "User-Agent": "bosun-ve",
770
770
  },
771
771
  });
772
772
 
package/monitor.mjs CHANGED
@@ -3339,9 +3339,9 @@ async function createPRViaVK(attemptId, prOpts = {}) {
3339
3339
  return { success: false, error: "repo_id_missing", _elapsedMs: 0 };
3340
3340
  }
3341
3341
 
3342
- const bosunCredit = "\n\n---\n*Created by [Bosun Bot](https://github.com/apps/bosun-botswain)*";
3342
+ const bosunCredit = "\n\n---\n*Created by [Bosun Bot](https://github.com/apps/bosun-ve)*";
3343
3343
  const rawDescription = prOpts.description || "";
3344
- const description = rawDescription.includes("bosun-botswain")
3344
+ const description = rawDescription.includes("bosun-ve")
3345
3345
  ? rawDescription
3346
3346
  : rawDescription.trimEnd() + bosunCredit;
3347
3347
 
@@ -5386,7 +5386,7 @@ function buildEpicMergeBody(tasks, headName, baseName) {
5386
5386
  }
5387
5387
  lines.push("");
5388
5388
  lines.push("---");
5389
- lines.push("*Created by [Bosun Bot](https://github.com/apps/bosun-botswain)*");
5389
+ lines.push("*Created by [Bosun Bot](https://github.com/apps/bosun-ve)*");
5390
5390
  return lines.join("\n");
5391
5391
  }
5392
5392
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache 2.0",
@@ -67,7 +67,6 @@
67
67
  "./container-runner": "./container-runner.mjs",
68
68
  "./compat": "./compat.mjs",
69
69
  "./task-cli": "./task-cli.mjs",
70
- "./github-oauth-portal": "./github-oauth-portal.mjs",
71
70
  "./github-auth-manager": "./github-auth-manager.mjs",
72
71
  "./git-commit-helpers": "./git-commit-helpers.mjs"
73
72
  },
@@ -91,6 +90,7 @@
91
90
  "desktop": "node cli.mjs --desktop",
92
91
  "desktop:install": "npm -C desktop install",
93
92
  "desktop:dist": "npm -C desktop run dist",
93
+ "build": "node vendor-sync.mjs",
94
94
  "shared-workspaces": "node shared-workspace-cli.mjs",
95
95
  "syntax:check": "node -e \"const fs=require('fs'),path=require('path');const files=fs.readdirSync('.').filter(f=>f.endsWith('.mjs'));let fail=0;for(const f of files){try{require('child_process').execSync('node --check '+f,{stdio:'pipe'});}catch(e){console.error('Syntax error: '+f);console.error(e.stderr.toString());fail=1;}}if(fail)process.exit(1);console.log('Syntax OK: '+files.length+' files checked');\"",
96
96
  "pretest": "npm run syntax:check",
@@ -98,6 +98,7 @@
98
98
  "test:watch": "vitest",
99
99
  "preinstall": "node -e \"try{var r=require('child_process').execSync('npm ls -g codex-monitor --json --depth=0',{encoding:'utf8',stdio:['pipe','pipe','pipe']});var d=JSON.parse(r).dependencies;if(d&&d['codex-monitor']){console.log('\\n Removing old codex-monitor package...');require('child_process').execSync('npm uninstall -g codex-monitor',{stdio:'inherit',timeout:30000});console.log(' \\u2705 Migrated to bosun. codex-monitor aliases still work.\\n')}}catch(e){}\"",
100
100
  "postinstall": "node postinstall.mjs",
101
+ "prepare": "node vendor-sync.mjs",
101
102
  "sentinel": "node telegram-sentinel.mjs",
102
103
  "sentinel:stop": "node -e \"import('./telegram-sentinel.mjs').then(m => m.stopSentinel())\"",
103
104
  "sentinel:status": "node -e \"import('./telegram-sentinel.mjs').then(m => console.log(JSON.stringify(m.getSentinelStatus(), null, 2)))\"",
@@ -108,7 +109,9 @@
108
109
  "publish:minor": "node publish.mjs --bump minor",
109
110
  "publish:major": "node publish.mjs --bump major",
110
111
  "publish:minor:dry": "node publish.mjs --bump minor --dry-run",
111
- "publish:major:dry": "node publish.mjs --bump major --dry-run"
112
+ "publish:major:dry": "node publish.mjs --bump major --dry-run",
113
+ "vendorsync": "node vendor-sync.mjs",
114
+ "vendor:sync": "node vendor-sync.mjs"
112
115
  },
113
116
  "files": [
114
117
  ".env.example",
@@ -212,14 +215,20 @@
212
215
  "whatsapp-channel.mjs",
213
216
  "container-runner.mjs",
214
217
  "daemon-restart-policy.mjs",
215
- "publish.mjs"
218
+ "publish.mjs",
219
+ "vendor-sync.mjs",
220
+ "ui/vendor/"
216
221
  ],
217
222
  "dependencies": {
218
223
  "@anthropic-ai/claude-agent-sdk": "latest",
219
224
  "@github/copilot-sdk": "latest",
220
225
  "@openai/codex-sdk": "latest",
226
+ "@preact/signals": "1.3.1",
221
227
  "@whiskeysockets/baileys": "^7.0.0-rc.9",
222
228
  "ajv": "^8.18.0",
229
+ "es-module-shims": "1.10.0",
230
+ "htm": "3.1.1",
231
+ "preact": "10.25.4",
223
232
  "qrcode-terminal": "^0.12.0",
224
233
  "vibe-kanban": "latest",
225
234
  "ws": "^8.19.0"
package/postinstall.mjs CHANGED
@@ -396,6 +396,23 @@ async function main() {
396
396
  } catch {
397
397
  // Non-blocking; hooks can be installed via `npm run hooks:install`
398
398
  }
399
+
400
+ // Sync vendor files into ui/vendor/ so the UI works fully offline.
401
+ // Non-blocking — a missing vendor file just falls back to node_modules or CDN.
402
+ try {
403
+ const { syncVendorFiles } = await import("./vendor-sync.mjs");
404
+ const { ok, results } = await syncVendorFiles({ silent: true });
405
+ const synced = results.filter((r) => r.source).length;
406
+ if (ok) {
407
+ console.log(` ✅ Vendor files bundled into ui/vendor/ (${synced}/${results.length} files)`);
408
+ } else {
409
+ const missing = results.filter((r) => !r.source).map((r) => r.name);
410
+ console.warn(` ⚠️ Some vendor files could not be bundled: ${missing.join(", ")}`);
411
+ console.warn(" The UI server will fall back to CDN for those files.");
412
+ }
413
+ } catch (err) {
414
+ console.warn(` ⚠️ vendor-sync skipped: ${err.message}`);
415
+ }
399
416
  }
400
417
 
401
418
  main().catch((err) => {