bosun 0.26.3

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 (122) hide show
  1. package/.env.example +918 -0
  2. package/LICENSE +190 -0
  3. package/README.md +98 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/bosun.config.example.json +115 -0
  13. package/bosun.schema.json +465 -0
  14. package/claude-shell.mjs +708 -0
  15. package/cli.mjs +1028 -0
  16. package/codex-config.mjs +1274 -0
  17. package/codex-model-profiles.mjs +135 -0
  18. package/codex-shell.mjs +762 -0
  19. package/compat.mjs +286 -0
  20. package/config-doctor.mjs +613 -0
  21. package/config.mjs +1724 -0
  22. package/conflict-resolver.mjs +248 -0
  23. package/container-runner.mjs +450 -0
  24. package/copilot-shell.mjs +827 -0
  25. package/daemon-restart-policy.mjs +56 -0
  26. package/diff-stats.mjs +282 -0
  27. package/error-detector.mjs +829 -0
  28. package/fetch-runtime.mjs +34 -0
  29. package/fleet-coordinator.mjs +838 -0
  30. package/get-telegram-chat-id.mjs +71 -0
  31. package/git-safety.mjs +170 -0
  32. package/github-reconciler.mjs +403 -0
  33. package/hook-profiles.mjs +651 -0
  34. package/kanban-adapter.mjs +4491 -0
  35. package/lib/logger.mjs +645 -0
  36. package/maintenance.mjs +828 -0
  37. package/merge-strategy.mjs +1171 -0
  38. package/monitor.mjs +12237 -0
  39. package/package.json +209 -0
  40. package/postinstall.mjs +187 -0
  41. package/pr-cleanup-daemon.mjs +978 -0
  42. package/preflight.mjs +408 -0
  43. package/prepublish-check.mjs +90 -0
  44. package/presence.mjs +328 -0
  45. package/primary-agent.mjs +290 -0
  46. package/publish.mjs +241 -0
  47. package/repo-root.mjs +29 -0
  48. package/restart-controller.mjs +100 -0
  49. package/review-agent.mjs +557 -0
  50. package/rotate-agent-logs.sh +133 -0
  51. package/sdk-conflict-resolver.mjs +973 -0
  52. package/session-tracker.mjs +880 -0
  53. package/setup.mjs +3946 -0
  54. package/shared-knowledge.mjs +410 -0
  55. package/shared-state-manager.mjs +841 -0
  56. package/shared-workspace-cli.mjs +199 -0
  57. package/shared-workspace-registry.mjs +537 -0
  58. package/shared-workspaces.json +18 -0
  59. package/startup-service.mjs +1070 -0
  60. package/sync-engine.mjs +1063 -0
  61. package/task-archiver.mjs +801 -0
  62. package/task-assessment.mjs +550 -0
  63. package/task-claims.mjs +924 -0
  64. package/task-complexity.mjs +581 -0
  65. package/task-executor.mjs +5111 -0
  66. package/task-store.mjs +753 -0
  67. package/telegram-bot.mjs +9683 -0
  68. package/telegram-sentinel.mjs +2010 -0
  69. package/ui/app.js +867 -0
  70. package/ui/app.legacy.js +1464 -0
  71. package/ui/app.monolith.js +2488 -0
  72. package/ui/components/charts.js +226 -0
  73. package/ui/components/chat-view.js +567 -0
  74. package/ui/components/command-palette.js +587 -0
  75. package/ui/components/diff-viewer.js +190 -0
  76. package/ui/components/forms.js +357 -0
  77. package/ui/components/kanban-board.js +451 -0
  78. package/ui/components/session-list.js +305 -0
  79. package/ui/components/shared.js +525 -0
  80. package/ui/demo.html +640 -0
  81. package/ui/index.html +70 -0
  82. package/ui/modules/api.js +297 -0
  83. package/ui/modules/icons.js +461 -0
  84. package/ui/modules/router.js +81 -0
  85. package/ui/modules/settings-schema.js +261 -0
  86. package/ui/modules/state.js +679 -0
  87. package/ui/modules/telegram.js +331 -0
  88. package/ui/modules/utils.js +270 -0
  89. package/ui/styles/animations.css +140 -0
  90. package/ui/styles/base.css +98 -0
  91. package/ui/styles/components.css +2032 -0
  92. package/ui/styles/kanban.css +286 -0
  93. package/ui/styles/layout.css +810 -0
  94. package/ui/styles/sessions.css +841 -0
  95. package/ui/styles/variables.css +188 -0
  96. package/ui/styles.css +141 -0
  97. package/ui/styles.monolith.css +1046 -0
  98. package/ui/tabs/agents.js +1417 -0
  99. package/ui/tabs/chat.js +75 -0
  100. package/ui/tabs/control.js +892 -0
  101. package/ui/tabs/dashboard.js +515 -0
  102. package/ui/tabs/infra.js +537 -0
  103. package/ui/tabs/logs.js +783 -0
  104. package/ui/tabs/settings.js +1509 -0
  105. package/ui/tabs/tasks.js +1385 -0
  106. package/ui-server.mjs +4084 -0
  107. package/update-check.mjs +471 -0
  108. package/utils.mjs +172 -0
  109. package/ve-kanban.mjs +654 -0
  110. package/ve-kanban.ps1 +1365 -0
  111. package/ve-kanban.sh +18 -0
  112. package/ve-orchestrator.mjs +340 -0
  113. package/ve-orchestrator.ps1 +6546 -0
  114. package/ve-orchestrator.sh +18 -0
  115. package/vibe-kanban-wrapper.mjs +41 -0
  116. package/vk-error-resolver.mjs +470 -0
  117. package/vk-log-stream.mjs +914 -0
  118. package/whatsapp-channel.mjs +520 -0
  119. package/workspace-monitor.mjs +581 -0
  120. package/workspace-reaper.mjs +405 -0
  121. package/workspace-registry.mjs +238 -0
  122. package/worktree-manager.mjs +1266 -0
package/ui/demo.html ADDED
@@ -0,0 +1,640 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
+ <meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; connect-src *;" />
7
+ <title>Bosun MiniApp — Demo Mode</title>
8
+
9
+ <!-- Telegram WebApp SDK is fully mocked below — no real SDK needed -->
10
+
11
+ <!-- Import map — same as the real app -->
12
+ <script type="importmap">
13
+ {
14
+ "imports": {
15
+ "preact": "https://esm.sh/preact@10.25.4",
16
+ "preact/hooks": "https://esm.sh/preact@10.25.4/hooks",
17
+ "preact/compat": "https://esm.sh/preact@10.25.4/compat",
18
+ "htm": "https://esm.sh/htm@3.1.1",
19
+ "@preact/signals": "https://esm.sh/@preact/signals@1.3.1?deps=preact@10.25.4"
20
+ }
21
+ }
22
+ </script>
23
+
24
+ <!-- Load the real MiniApp styles -->
25
+ <link rel="stylesheet" href="styles.css" />
26
+ <link rel="stylesheet" href="styles/kanban.css" />
27
+ <link rel="stylesheet" href="styles/sessions.css" />
28
+
29
+ <!-- Fonts (match site) -->
30
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
31
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
32
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet" />
33
+
34
+ <style>
35
+ /* Demo mode indicator */
36
+ .demo-banner {
37
+ position: fixed;
38
+ top: 0;
39
+ left: 0;
40
+ right: 0;
41
+ z-index: 99999;
42
+ background: linear-gradient(135deg, #60cc5d, #4caf50);
43
+ color: #000;
44
+ text-align: center;
45
+ font-size: 11px;
46
+ font-weight: 700;
47
+ padding: 3px 8px;
48
+ font-family: 'Inter', -apple-system, sans-serif;
49
+ letter-spacing: 0.05em;
50
+ text-transform: uppercase;
51
+ }
52
+ /* Push content down */
53
+ body { padding-top: 22px !important; }
54
+ /* Ensure no scrollbar leaks */
55
+ html, body { overflow: hidden; height: 100%; margin: 0; }
56
+ /* Force dark theme */
57
+ :root {
58
+ color-scheme: dark;
59
+ }
60
+ </style>
61
+
62
+ <!-- ═══ DEMO MOCK LAYER ═══════════════════════════════════════════════
63
+ This script runs BEFORE the real app.js to mock:
64
+ 1. Telegram.WebApp — provides fake theme, user, initData
65
+ 2. fetch() — intercepts /api/* to return seed data
66
+ 3. WebSocket — provides fake WS connection for live updates
67
+ ═══════════════════════════════════════════════════════════════════ -->
68
+ <script>
69
+ (function() {
70
+ 'use strict';
71
+
72
+ /* ── Mock Telegram.WebApp ──────────────────────────────────────── */
73
+ const mockUser = {
74
+ id: 1337420,
75
+ first_name: 'Demo',
76
+ last_name: 'User',
77
+ username: 'demo_user',
78
+ language_code: 'en',
79
+ is_premium: true,
80
+ };
81
+
82
+ const theme = {
83
+ bg_color: '#0b0f14',
84
+ text_color: '#f1f5f9',
85
+ hint_color: '#64748b',
86
+ link_color: '#4cc9f0',
87
+ button_color: '#4cc9f0',
88
+ button_text_color: '#000000',
89
+ secondary_bg_color: '#131a24',
90
+ header_bg_color: '#0b0f14',
91
+ accent_text_color: '#4cc9f0',
92
+ section_bg_color: '#131a24',
93
+ section_header_text_color: '#94a3b8',
94
+ subtitle_text_color: '#94a3b8',
95
+ destructive_text_color: '#ef4444',
96
+ section_separator_color: '#1e293b',
97
+ bottom_bar_bg_color: '#0b0f14',
98
+ };
99
+
100
+ window.Telegram = {
101
+ WebApp: {
102
+ initData: 'demo_mode=true&user=' + encodeURIComponent(JSON.stringify(mockUser)),
103
+ initDataUnsafe: { user: mockUser, auth_date: Date.now() },
104
+ version: '8.0',
105
+ platform: 'web',
106
+ colorScheme: 'dark',
107
+ themeParams: theme,
108
+ isExpanded: true,
109
+ viewportHeight: window.innerHeight,
110
+ viewportStableHeight: window.innerHeight,
111
+ headerColor: theme.header_bg_color,
112
+ backgroundColor: theme.bg_color,
113
+ bottomBarColor: theme.bottom_bar_bg_color,
114
+ isClosingConfirmationEnabled: false,
115
+ isVerticalSwipesEnabled: false,
116
+ isFullscreen: true,
117
+ safeAreaInset: { top: 0, bottom: 0, left: 0, right: 0 },
118
+ contentSafeAreaInset: { top: 0, bottom: 0, left: 0, right: 0 },
119
+ BackButton: {
120
+ isVisible: false,
121
+ show() { this.isVisible = true; },
122
+ hide() { this.isVisible = false; },
123
+ onClick(cb) { this._cb = cb; },
124
+ offClick() { this._cb = null; },
125
+ },
126
+ MainButton: {
127
+ isVisible: false,
128
+ text: '',
129
+ color: theme.button_color,
130
+ textColor: theme.button_text_color,
131
+ isProgressVisible: false,
132
+ show() { this.isVisible = true; },
133
+ hide() { this.isVisible = false; },
134
+ setText(t) { this.text = t; },
135
+ onClick(cb) { this._cb = cb; },
136
+ offClick() { this._cb = null; },
137
+ showProgress() { this.isProgressVisible = true; },
138
+ hideProgress() { this.isProgressVisible = false; },
139
+ enable() {},
140
+ disable() {},
141
+ },
142
+ HapticFeedback: {
143
+ impactOccurred() {},
144
+ notificationOccurred() {},
145
+ selectionChanged() {},
146
+ },
147
+ ready() {},
148
+ expand() {},
149
+ close() {},
150
+ requestFullscreen() {},
151
+ disableVerticalSwipes() {},
152
+ enableClosingConfirmation() {},
153
+ setHeaderColor(c) { this.headerColor = c; },
154
+ setBackgroundColor(c) { this.backgroundColor = c; },
155
+ setBottomBarColor(c) { this.bottomBarColor = c; },
156
+ onEvent(name, cb) {},
157
+ offEvent(name, cb) {},
158
+ showConfirm(msg, cb) { if (cb) cb(true); },
159
+ showAlert(msg, cb) { if (cb) cb(); },
160
+ showPopup(params, cb) { if (cb) cb('ok'); },
161
+ openLink(url) { window.open(url, '_blank'); },
162
+ openTelegramLink(url) {},
163
+ },
164
+ };
165
+
166
+ /* ── Seed Data ─────────────────────────────────────────────────── */
167
+ const now = Date.now();
168
+ const min = 60000;
169
+ const hr = 3600000;
170
+
171
+ const SEED_TASKS = [
172
+ { id: 've-abc123', title: 'feat(market): add order expiry validation', status: 'done', priority: 'high', assignee: 'copilot-claude', branch: 've/abc123-market-order-expiry', pr: 187, created: now - 4*hr, updated: now - 1*hr },
173
+ { id: 've-def456', title: 'fix(veid): token validation edge case', status: 'done', priority: 'medium', assignee: 'codex-default', branch: 've/def456-veid-token-fix', pr: 188, created: now - 3*hr, updated: now - 45*min },
174
+ { id: 've-ghi789', title: 'refactor(escrow): batch settlement pipeline', status: 'inprogress', priority: 'high', assignee: 'copilot-claude', branch: 've/ghi789-escrow-batch', pr: 189, created: now - 2*hr, updated: now - 15*min },
175
+ { id: 've-jkl012', title: 'feat(hpc): GPU resource metering', status: 'inprogress', priority: 'critical', assignee: 'codex-default', branch: 've/jkl012-hpc-gpu-metering', pr: null, created: now - 1*hr, updated: now - 5*min },
176
+ { id: 've-mno345', title: 'docs: update provider integration guide', status: 'todo', priority: 'low', assignee: null, branch: null, pr: null, created: now - 30*min, updated: now - 30*min },
177
+ { id: 've-pqr678', title: 'feat(provider): health check endpoint', status: 'todo', priority: 'medium', assignee: null, branch: null, pr: null, created: now - 20*min, updated: now - 20*min },
178
+ { id: 've-stu901', title: 'fix(roles): permission cascade bug', status: 'draft', priority: 'high', assignee: null, branch: null, pr: null, created: now - 10*min, updated: now - 10*min },
179
+ { id: 've-vwx234', title: 'test(mfa): add integration test suite', status: 'inreview', priority: 'medium', assignee: 'copilot-claude', branch: 've/vwx234-mfa-tests', pr: 190, created: now - 5*hr, updated: now - 30*min },
180
+ ];
181
+
182
+ const SEED_STATUS = {
183
+ status: 'running',
184
+ uptime: '2h 34m 12s',
185
+ executorMode: 'internal',
186
+ board: 'github',
187
+ maxParallel: 6,
188
+ version: '0.26.2',
189
+ tasks: { completed: 13, failed: 1, retried: 2, active: 2, queued: 3 },
190
+ prs: { created: 15, merged: 11, pending: 2, ciFailures: 1 },
191
+ };
192
+
193
+ const SEED_EXECUTORS = [
194
+ { name: 'copilot-claude', sdk: 'copilot', model: 'claude-opus-4-6', weight: 50, status: 'active', load: 67, tasksCompleted: 8, avgTime: '12m', uptime: '2h 34m', session: 'sk-...7f3a' },
195
+ { name: 'codex-default', sdk: 'codex', model: 'o4-mini', weight: 50, status: 'active', load: 42, tasksCompleted: 5, avgTime: '18m', uptime: '2h 34m', session: 'cx-...a91b' },
196
+ ];
197
+
198
+ const SEED_AGENTS = [
199
+ { id: 'agent-001', name: 'copilot-claude', type: 'copilot', model: 'claude-opus-4-6', status: 'busy', currentTask: 've-ghi789', lastHeartbeat: now - 30000 },
200
+ { id: 'agent-002', name: 'codex-default', type: 'codex', model: 'o4-mini', status: 'busy', currentTask: 've-jkl012', lastHeartbeat: now - 15000 },
201
+ ];
202
+
203
+ const SEED_LOGS = [
204
+ { ts: now - 20*min, level: 'info', source: 'monitor', msg: 'Polling github for new tasks...' },
205
+ { ts: now - 19*min, level: 'info', source: 'kanban', msg: 'Found 2 new tasks in backlog' },
206
+ { ts: now - 18*min, level: 'info', source: 'task-executor', msg: 'Routing #42 feat(market): add order expiry → copilot-claude' },
207
+ { ts: now - 17*min, level: 'info', source: 'task-executor', msg: 'Routing #43 fix(veid): token validation → codex-default' },
208
+ { ts: now - 14*min, level: 'info', source: 'worktree', msg: 'Created ve/abc123-market-order-expiry' },
209
+ { ts: now - 12*min, level: 'info', source: 'copilot', msg: 'Session started for #42' },
210
+ { ts: now - 10*min, level: 'success', source: 'pr', msg: 'PR #187 created — CI running...' },
211
+ { ts: now - 8*min, level: 'success', source: 'ci', msg: 'PR #187 — all checks passed' },
212
+ { ts: now - 7*min, level: 'success', source: 'merge', msg: 'PR #187 merged to main ✓' },
213
+ { ts: now - 5*min, level: 'info', source: 'health', msg: 'Health check: all systems nominal' },
214
+ { ts: now - 2*min, level: 'info', source: 'review-agent', msg: 'Reviewing PR #189...' },
215
+ { ts: now - 1*min, level: 'info', source: 'monitor', msg: 'Next poll in 60s...' },
216
+ ];
217
+
218
+ const SEED_HEALTH = {
219
+ overall: 'healthy',
220
+ components: [
221
+ { name: 'GitHub API', status: 'ok', latency: '142ms' },
222
+ { name: 'Telegram Bot', status: 'ok', latency: '89ms' },
223
+ { name: 'Codex SDK', status: 'ok', latency: '234ms' },
224
+ { name: 'Copilot SDK', status: 'ok', latency: '178ms' },
225
+ { name: 'Shared State', status: 'ok', latency: '12ms' },
226
+ { name: 'Worktree Manager', status: 'ok', latency: '45ms' },
227
+ ],
228
+ };
229
+
230
+ const SEED_INFRA = {
231
+ worktrees: [
232
+ { branch: 've/abc123-market-order-expiry', path: '/worktrees/abc123', active: false },
233
+ { branch: 've/ghi789-escrow-batch', path: '/worktrees/ghi789', active: true },
234
+ { branch: 've/jkl012-hpc-gpu-metering', path: '/worktrees/jkl012', active: true },
235
+ { branch: 've/vwx234-mfa-tests', path: '/worktrees/vwx234', active: true },
236
+ ],
237
+ containers: [],
238
+ tunnelStatus: 'active',
239
+ tunnelUrl: 'https://bosun-demo.trycloudflare.com',
240
+ };
241
+
242
+ const SEED_PROJECT = {
243
+ name: 'virtengine',
244
+ repo: 'virtengine/virtengine',
245
+ defaultBranch: 'main',
246
+ totalTasks: SEED_TASKS.length,
247
+ completedTasks: 2,
248
+ activePRs: 3,
249
+ };
250
+
251
+ /* ── Internal State (mutable — actions modify this in-place) ──── */
252
+ const STATE = {
253
+ tasks: SEED_TASKS,
254
+ status: SEED_STATUS,
255
+ executors: SEED_EXECUTORS,
256
+ agents: SEED_AGENTS,
257
+ logs: SEED_LOGS,
258
+ health: SEED_HEALTH,
259
+ infra: SEED_INFRA,
260
+ project: SEED_PROJECT,
261
+ paused: false,
262
+ maxParallel: 6,
263
+ settings: {
264
+ maxParallel: 6,
265
+ executorMode: 'internal',
266
+ autoMerge: true,
267
+ autoRebase: true,
268
+ reviewAgent: true,
269
+ sentinelEnabled: false,
270
+ sdk: 'auto',
271
+ region: 'auto',
272
+ },
273
+ sessions: [
274
+ { id: 'ses-001', name: 'copilot-claude #42', agentType: 'copilot', status: 'active', taskId: 've-ghi789', created: now - 2*hr, messages: [
275
+ { role: 'system', text: 'Session started for ve-ghi789', ts: now - 2*hr },
276
+ { role: 'assistant', text: 'Analyzing escrow batch settlement pipeline...', ts: now - 110*min },
277
+ { role: 'assistant', text: 'Implementing batch processor in x/escrow/keeper/batch.go', ts: now - 90*min },
278
+ ]},
279
+ { id: 'ses-002', name: 'codex-default #43', agentType: 'codex', status: 'active', taskId: 've-jkl012', created: now - 1*hr, messages: [
280
+ { role: 'system', text: 'Session started for ve-jkl012', ts: now - 1*hr },
281
+ { role: 'assistant', text: 'Reading GPU metering requirements...', ts: now - 50*min },
282
+ ]},
283
+ ],
284
+ worktrees: [
285
+ { branch: 've/abc123-market-order-expiry', path: '/worktrees/abc123', active: false, taskId: 've-abc123' },
286
+ { branch: 've/ghi789-escrow-batch', path: '/worktrees/ghi789', active: true, taskId: 've-ghi789' },
287
+ { branch: 've/jkl012-hpc-gpu-metering', path: '/worktrees/jkl012', active: true, taskId: 've-jkl012' },
288
+ { branch: 've/vwx234-mfa-tests', path: '/worktrees/vwx234', active: true, taskId: 've-vwx234' },
289
+ ],
290
+ sharedWorkspaces: [
291
+ { id: 'sw-1', name: 'shared-escrow-dev', claimedBy: 'copilot-claude', claimedAt: now - 1*hr, expiresAt: now + 1*hr },
292
+ ],
293
+ gitBranches: ['main', 've/abc123-market-order-expiry', 've/ghi789-escrow-batch', 've/jkl012-hpc-gpu-metering', 've/vwx234-mfa-tests'],
294
+ gitDiff: '--- a/x/escrow/keeper/batch.go\n+++ b/x/escrow/keeper/batch.go\n@@ -0,0 +1,42 @@\n+package keeper\n+\n+// BatchSettle processes pending settlements in batch.\n+func (k Keeper) BatchSettle(ctx sdk.Context) error {\n+ // ... implementation\n+ return nil\n+}',
295
+ agentLogFiles: ['copilot-claude-42.log', 'codex-default-43.log', 'monitor.log'],
296
+ presence: {
297
+ instances: [{ id: 'inst-1', host: 'dev-machine', pid: 12345, started: now - 3*hr, role: 'primary' }],
298
+ coordinator: { id: 'inst-1', host: 'dev-machine', elected: now - 3*hr },
299
+ },
300
+ };
301
+
302
+ /* ── Helper: find task by ID ───────────────────────────────────── */
303
+ function findTask(id) { return STATE.tasks.find(t => t.id === id); }
304
+ function addLog(level, source, msg) {
305
+ STATE.logs.unshift({ ts: Date.now(), level, source, msg });
306
+ if (STATE.logs.length > 100) STATE.logs.length = 100;
307
+ }
308
+
309
+ /* ── API Route Handler (comprehensive — catches ALL /api/* calls) */
310
+ function handleApi(path, method, body) {
311
+ // Normalize: strip query string for route matching
312
+ const [route, qs] = path.split('?');
313
+ const params = new URLSearchParams(qs || '');
314
+
315
+ // ── Status & Executor ──
316
+ if (route === '/api/status')
317
+ return { data: STATE.status };
318
+ if (route === '/api/executor')
319
+ return { data: { ...STATE.status, maxParallel: STATE.maxParallel, paused: STATE.paused, executors: STATE.executors } };
320
+ if (route === '/api/executor/pause') {
321
+ STATE.paused = true; addLog('info', 'executor', 'Executor paused');
322
+ return { ok: true, paused: true };
323
+ }
324
+ if (route === '/api/executor/resume') {
325
+ STATE.paused = false; addLog('info', 'executor', 'Executor resumed');
326
+ return { ok: true, paused: false };
327
+ }
328
+ if (route === '/api/executor/maxparallel') {
329
+ const mp = body?.maxParallel ?? STATE.maxParallel;
330
+ STATE.maxParallel = mp; STATE.status.maxParallel = mp;
331
+ addLog('info', 'executor', `Max parallel set to ${mp}`);
332
+ return { ok: true, maxParallel: mp };
333
+ }
334
+ if (route === '/api/executor/stop-slot') {
335
+ addLog('info', 'executor', `Slot stopped: ${body?.slotId || 'unknown'}`);
336
+ return { ok: true };
337
+ }
338
+ if (route === '/api/executor/dispatch') {
339
+ addLog('info', 'executor', `Manual dispatch: ${body?.taskId || 'unknown'}`);
340
+ return { ok: true, dispatched: body?.taskId };
341
+ }
342
+
343
+ // ── Tasks ──
344
+ if (route === '/api/tasks') {
345
+ let tasks = [...STATE.tasks];
346
+ const status = params.get('status');
347
+ const priority = params.get('priority');
348
+ const search = params.get('search');
349
+ const sort = params.get('sort');
350
+ if (status && status !== 'all') tasks = tasks.filter(t => t.status === status);
351
+ if (priority && priority !== 'all') tasks = tasks.filter(t => t.priority === priority);
352
+ if (search) tasks = tasks.filter(t => t.title.toLowerCase().includes(search.toLowerCase()));
353
+ if (sort === 'priority') tasks.sort((a, b) => { const o = {critical:0,high:1,medium:2,low:3}; return (o[a.priority]??9)-(o[b.priority]??9); });
354
+ if (sort === '-updated') tasks.sort((a, b) => b.updated - a.updated);
355
+ const page = parseInt(params.get('page')) || 1;
356
+ const pageSize = parseInt(params.get('pageSize') || params.get('limit')) || 50;
357
+ const total = tasks.length;
358
+ const paged = tasks.slice((page-1)*pageSize, page*pageSize);
359
+ return { data: paged, tasks: paged, total, totalPages: Math.max(1, Math.ceil(total/pageSize)), page, pageSize };
360
+ }
361
+ if (route === '/api/tasks/detail') {
362
+ const t = findTask(params.get('taskId'));
363
+ return { data: t || null };
364
+ }
365
+ if (route === '/api/tasks/create') {
366
+ const id = 've-' + Math.random().toString(36).slice(2, 8);
367
+ const t = { id, title: body?.title || 'New task', status: 'todo', priority: body?.priority || 'medium', assignee: null, branch: null, pr: null, created: Date.now(), updated: Date.now() };
368
+ STATE.tasks.unshift(t); addLog('success', 'kanban', `Task created: ${t.title}`);
369
+ return { ok: true, data: t };
370
+ }
371
+ if (route === '/api/tasks/update') {
372
+ const t = findTask(body?.taskId || body?.id);
373
+ if (t) { Object.assign(t, body, { updated: Date.now() }); addLog('info', 'kanban', `Task updated: ${t.title}`); }
374
+ return { ok: true, data: t || null };
375
+ }
376
+ if (route === '/api/tasks/edit') {
377
+ const t = findTask(body?.taskId || body?.id);
378
+ if (t) { if (body?.title) t.title = body.title; if (body?.priority) t.priority = body.priority; t.updated = Date.now(); }
379
+ return { ok: true, data: t || null };
380
+ }
381
+ if (route === '/api/tasks/start') {
382
+ const t = findTask(body?.taskId);
383
+ if (t) { t.status = 'inprogress'; t.updated = Date.now(); addLog('info', 'task-executor', `Task started: ${t.title}`); }
384
+ return { ok: true, data: t || null };
385
+ }
386
+ if (route === '/api/tasks/retry') {
387
+ const t = findTask(body?.taskId);
388
+ if (t) { t.status = 'todo'; t.updated = Date.now(); addLog('info', 'task-executor', `Task retried: ${t.title}`); }
389
+ return { ok: true, data: t || null };
390
+ }
391
+
392
+ // ── Agents ──
393
+ if (route === '/api/agents')
394
+ return { data: STATE.agents };
395
+ if (route === '/api/executors')
396
+ return { executors: STATE.executors };
397
+
398
+ // ── Logs ──
399
+ if (route === '/api/logs') {
400
+ const lines = parseInt(params.get('lines')) || 50;
401
+ return { data: STATE.logs.slice(0, lines), logs: STATE.logs.slice(0, lines) };
402
+ }
403
+ if (route === '/api/agent-logs')
404
+ return { data: STATE.agentLogFiles };
405
+ if (route === '/api/agent-logs/tail') {
406
+ const file = params.get('file') || '';
407
+ const n = parseInt(params.get('lines')) || 100;
408
+ const lines = Array.from({ length: Math.min(n, 20) }, (_, i) => {
409
+ const ts = new Date(now - (20 - i) * 30000).toISOString();
410
+ return `[${ts}] [info] Demo log line ${i + 1} from ${file || 'agent'}`;
411
+ });
412
+ return { data: lines.join('\n') };
413
+ }
414
+ if (route === '/api/agent-context') {
415
+ const q = params.get('query') || '';
416
+ return { data: { query: q, branch: q, files: ['keeper.go', 'types.go', 'msg_server.go'], recentCommits: ['feat: initial implementation', 'fix: lint errors'], worktree: '/worktrees/' + q.slice(0, 6) } };
417
+ }
418
+
419
+ // ── Health ──
420
+ if (route === '/api/health')
421
+ return STATE.health;
422
+
423
+ // ── Infrastructure ──
424
+ if (route === '/api/infra')
425
+ return { data: STATE.infra };
426
+ if (route === '/api/worktrees') {
427
+ if (route === '/api/worktrees') return { data: STATE.worktrees, stats: { total: STATE.worktrees.length, active: STATE.worktrees.filter(w => w.active).length } };
428
+ }
429
+ if (route === '/api/worktrees/prune') {
430
+ STATE.worktrees = STATE.worktrees.filter(w => w.active); addLog('info', 'worktree', 'Pruned inactive worktrees');
431
+ return { ok: true, pruned: 1 };
432
+ }
433
+ if (route === '/api/worktrees/release') {
434
+ const branch = body?.branch;
435
+ const wt = STATE.worktrees.find(w => w.branch === branch);
436
+ if (wt) wt.active = false;
437
+ return { ok: true };
438
+ }
439
+
440
+ // ── Shared Workspaces ──
441
+ if (route === '/api/shared-workspaces')
442
+ return { data: STATE.sharedWorkspaces };
443
+ if (route === '/api/shared-workspaces/claim') {
444
+ const ws = { id: 'sw-' + Math.random().toString(36).slice(2, 5), name: body?.name || 'workspace', claimedBy: body?.agent || 'demo', claimedAt: Date.now(), expiresAt: Date.now() + 2*hr };
445
+ STATE.sharedWorkspaces.push(ws);
446
+ return { ok: true, data: ws };
447
+ }
448
+ if (route === '/api/shared-workspaces/renew') {
449
+ const ws = STATE.sharedWorkspaces.find(w => w.id === body?.id);
450
+ if (ws) ws.expiresAt = Date.now() + 2*hr;
451
+ return { ok: true };
452
+ }
453
+ if (route === '/api/shared-workspaces/release') {
454
+ STATE.sharedWorkspaces = STATE.sharedWorkspaces.filter(w => w.id !== body?.id);
455
+ return { ok: true };
456
+ }
457
+
458
+ // ── Git ──
459
+ if (route === '/api/git/branches')
460
+ return { data: STATE.gitBranches };
461
+ if (route === '/api/git/diff')
462
+ return { data: STATE.gitDiff };
463
+
464
+ // ── Sessions ──
465
+ if (route === '/api/sessions' && method === 'GET') {
466
+ return { data: STATE.sessions.map(s => ({ id: s.id, name: s.name, agentType: s.agentType, status: s.status, taskId: s.taskId, created: s.created })) };
467
+ }
468
+ if (route.match(/^\/api\/sessions\/[^/]+\/diff$/)) {
469
+ return { data: STATE.gitDiff };
470
+ }
471
+ if (route.match(/^\/api\/sessions\/[^/]+\/message$/)) {
472
+ const sid = route.split('/')[3];
473
+ const ses = STATE.sessions.find(s => s.id === sid);
474
+ if (ses) ses.messages.push({ role: 'user', text: body?.text || body?.message || '', ts: Date.now() });
475
+ return { ok: true };
476
+ }
477
+ if (route.match(/^\/api\/sessions\/[^/]+\/resume$/)) {
478
+ return { ok: true };
479
+ }
480
+ if (route.match(/^\/api\/sessions\/[^/]+\/archive$/)) {
481
+ const sid = route.split('/')[3];
482
+ const ses = STATE.sessions.find(s => s.id === sid);
483
+ if (ses) ses.status = 'archived';
484
+ return { ok: true };
485
+ }
486
+ if (route === '/api/sessions/create') {
487
+ const s = { id: 'ses-' + Math.random().toString(36).slice(2, 5), name: body?.name || 'New session', agentType: body?.agentType || 'copilot', status: 'active', taskId: body?.taskId || null, created: Date.now(), messages: [] };
488
+ STATE.sessions.push(s);
489
+ return { ok: true, data: s };
490
+ }
491
+ if (route.match(/^\/api\/sessions\/[^/]+$/) && method === 'GET') {
492
+ const sid = route.split('/')[3];
493
+ return { data: STATE.sessions.find(s => s.id === sid) || null };
494
+ }
495
+
496
+ // ── Settings & Config ──
497
+ if (route === '/api/settings')
498
+ return STATE.settings;
499
+ if (route === '/api/config/update') {
500
+ Object.assign(STATE.settings, body || {});
501
+ return { ok: true };
502
+ }
503
+
504
+ // ── Command ──
505
+ if (route === '/api/command') {
506
+ const cmd = body?.command || '';
507
+ addLog('info', 'command', `Executed: ${cmd}`);
508
+ return { ok: true, command: cmd, response: 'Command executed in demo mode: ' + cmd };
509
+ }
510
+
511
+ // ── Presence ──
512
+ if (route === '/api/presence')
513
+ return { data: STATE.presence };
514
+
515
+ // ── Project ──
516
+ if (route === '/api/project' || route === '/api/project-summary')
517
+ return { data: STATE.project };
518
+
519
+ // ── Catch-all: return empty success for any unhandled /api/ route ──
520
+ console.debug('[demo] Unhandled API route:', method, path);
521
+ return { ok: true, data: null };
522
+ }
523
+
524
+ /* ── Intercept fetch() — ALL /api/ calls handled internally ───── */
525
+ const _realFetch = window.fetch;
526
+ window.fetch = function(url, options) {
527
+ const path = typeof url === 'string' ? url : url?.url || '';
528
+
529
+ // Intercept ALL /api/ calls — nothing reaches the network
530
+ if (path.startsWith('/api/') || path.startsWith('/api?')) {
531
+ let body = null;
532
+ if (options?.body) {
533
+ try { body = JSON.parse(options.body); } catch {}
534
+ }
535
+ const method = (options?.method || 'GET').toUpperCase();
536
+ const data = handleApi(path, method, body);
537
+ return Promise.resolve(new Response(JSON.stringify(data), {
538
+ status: 200,
539
+ headers: { 'Content-Type': 'application/json' },
540
+ }));
541
+ }
542
+
543
+ // Pass through non-API requests (fonts, CDN, ES modules, etc.)
544
+ return _realFetch.apply(this, arguments);
545
+ };
546
+
547
+ /* ── Intercept WebSocket ───────────────────────────────────────── */
548
+ const _RealWS = window.WebSocket;
549
+ window.WebSocket = function(url, protocols) {
550
+ const fake = {
551
+ url: url,
552
+ readyState: 1, // OPEN
553
+ bufferedAmount: 0,
554
+ extensions: '',
555
+ protocol: '',
556
+ onopen: null,
557
+ onmessage: null,
558
+ onclose: null,
559
+ onerror: null,
560
+ send(data) { /* swallow */ },
561
+ close() {
562
+ this.readyState = 3;
563
+ if (this.onclose) this.onclose({ code: 1000, reason: 'Demo mode' });
564
+ },
565
+ addEventListener(type, handler) {
566
+ this['on' + type] = handler;
567
+ },
568
+ removeEventListener() {},
569
+ };
570
+
571
+ // Simulate open after microtask
572
+ setTimeout(() => {
573
+ fake.readyState = 1;
574
+ if (fake.onopen) fake.onopen({});
575
+
576
+ // Send pong responses
577
+ setInterval(() => {
578
+ if (fake.onmessage) {
579
+ fake.onmessage({ data: JSON.stringify({ type: 'pong', ts: Date.now() }) });
580
+ }
581
+ }, 5000);
582
+
583
+ // Simulate periodic task updates
584
+ let updateIdx = 0;
585
+ setInterval(() => {
586
+ if (!fake.onmessage) return;
587
+ const updates = [
588
+ { type: 'task-update', task: { id: 've-ghi789', status: 'inprogress' } },
589
+ { type: 'log', entry: { ts: Date.now(), level: 'info', source: 'monitor', msg: 'Health check passed — all systems nominal' } },
590
+ { type: 'executor-update', executor: { name: 'copilot-claude', load: 55 + Math.floor(Math.random() * 25) } },
591
+ ];
592
+ fake.onmessage({ data: JSON.stringify(updates[updateIdx % updates.length]) });
593
+ updateIdx++;
594
+ }, 8000);
595
+ }, 100);
596
+
597
+ return fake;
598
+ };
599
+ window.WebSocket.CONNECTING = 0;
600
+ window.WebSocket.OPEN = 1;
601
+ window.WebSocket.CLOSING = 2;
602
+ window.WebSocket.CLOSED = 3;
603
+
604
+ })();
605
+ </script>
606
+ </head>
607
+ <body>
608
+ <!-- Demo mode banner -->
609
+ <div class="demo-banner">⚡ Bosun Demo Mode — Seed Data</div>
610
+
611
+ <!-- App root — boot loader lives INSIDE #app so Preact's render() replaces it -->
612
+ <div id="app">
613
+ <div id="boot-loader" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:'Inter',sans-serif;color:#9aa3b8;background:#0b0f14;overflow:hidden">
614
+ <style>
615
+ .boot-ring{position:absolute;inset:0;border-radius:50%;border:2px solid rgba(76,201,240,.15);animation:boot-spin 3s linear infinite}
616
+ .boot-ring-inner{position:absolute;inset:8px;border-radius:50%;border:2px solid transparent;border-top-color:#4cc9f0;border-right-color:rgba(76,201,240,.4);animation:boot-spin 1.5s linear infinite reverse}
617
+ .boot-dot{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;background:linear-gradient(135deg,#4cc9f0,#60cc5d);box-shadow:0 0 20px rgba(76,201,240,.5);animation:boot-pulse 2s ease-in-out infinite}
618
+ @keyframes boot-spin{to{transform:rotate(360deg)}}
619
+ @keyframes boot-pulse{0%,100%{transform:scale(1);opacity:.8}50%{transform:scale(1.2);opacity:1}}
620
+ </style>
621
+ <div style="position:relative;width:60px;height:60px;margin-bottom:16px">
622
+ <div class="boot-ring"></div>
623
+ <div class="boot-ring-inner"></div>
624
+ <div class="boot-dot"></div>
625
+ </div>
626
+ <div style="font-size:13px;color:#64748b">Loading demo...</div>
627
+ </div>
628
+ </div>
629
+
630
+ <!-- Load the REAL app.js (it uses ES modules + importmap) -->
631
+ <script type="module" src="app.js"></script>
632
+ <script>
633
+ // Fallback: if ES modules fail to load within 12s, show an error
634
+ setTimeout(function() {
635
+ var el = document.getElementById('boot-loader');
636
+ if (el) el.innerHTML = '<div style="text-align:center;padding:40px;font-family:Inter,sans-serif;color:#94a3b8"><div style="font-size:18px;margin-bottom:8px">⚠️ Failed to load</div><div style="font-size:12px;color:#64748b">Check connection (CDN: esm.sh) or refresh</div></div>';
637
+ }, 12000);
638
+ </script>
639
+ </body>
640
+ </html>