@sugar-crash-studios/vibe-forge 0.4.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 (201) hide show
  1. package/.claude/commands/clear-attention.md +63 -0
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -0
  4. package/.claude/commands/forge.md +171 -0
  5. package/.claude/commands/need-help.md +77 -0
  6. package/.claude/commands/update-status.md +64 -0
  7. package/.claude/commands/worker-loop.md +106 -0
  8. package/.claude/hooks/worker-loop.js +198 -0
  9. package/.claude/scripts/setup-worker-loop.sh +45 -0
  10. package/.claude/settings.local.json +46 -0
  11. package/LICENSE +21 -0
  12. package/README.md +238 -0
  13. package/agents/aegis/personality.md +294 -0
  14. package/agents/anvil/personality.md +276 -0
  15. package/agents/architect/personality.md +258 -0
  16. package/agents/crucible/personality.md +360 -0
  17. package/agents/ember/personality.md +291 -0
  18. package/agents/forge-master/capabilities.md +144 -0
  19. package/agents/forge-master/context-template.md +128 -0
  20. package/agents/forge-master/personality.md +138 -0
  21. package/agents/furnace/personality.md +340 -0
  22. package/agents/herald/personality.md +247 -0
  23. package/agents/loki/personality.md +108 -0
  24. package/agents/oracle/personality.md +283 -0
  25. package/agents/pixel/personality.md +113 -0
  26. package/agents/planning-hub/personality.md +320 -0
  27. package/agents/scribe/personality.md +251 -0
  28. package/agents/temper/personality.md +218 -0
  29. package/bin/cli.js +375 -0
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +483 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/frontend/index.html +13 -0
  34. package/bin/dashboard/frontend/package.json +16 -0
  35. package/bin/dashboard/frontend/src/App.svelte +222 -0
  36. package/bin/dashboard/frontend/src/app.css +1777 -0
  37. package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
  38. package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
  39. package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
  40. package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
  41. package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
  42. package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
  43. package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
  44. package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
  45. package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
  46. package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
  47. package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
  48. package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
  49. package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
  50. package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
  51. package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
  52. package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
  53. package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
  54. package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
  55. package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
  56. package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
  57. package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
  58. package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
  59. package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
  60. package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
  61. package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
  62. package/bin/dashboard/frontend/src/main.js +9 -0
  63. package/bin/dashboard/frontend/svelte.config.js +5 -0
  64. package/bin/dashboard/frontend/vite.config.js +20 -0
  65. package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
  66. package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
  67. package/bin/dashboard/public/index.html +14 -0
  68. package/bin/dashboard/server.js +566 -0
  69. package/bin/forge-daemon.sh +463 -0
  70. package/bin/forge-setup.sh +645 -0
  71. package/bin/forge-spawn.sh +164 -0
  72. package/bin/forge.cmd +83 -0
  73. package/bin/forge.sh +533 -0
  74. package/bin/lib/agents.sh +177 -0
  75. package/bin/lib/colors.sh +44 -0
  76. package/bin/lib/config.sh +347 -0
  77. package/bin/lib/constants.sh +241 -0
  78. package/bin/lib/daemon/display.sh +128 -0
  79. package/bin/lib/daemon/notifications.sh +263 -0
  80. package/bin/lib/daemon/routing.sh +77 -0
  81. package/bin/lib/daemon/state.sh +115 -0
  82. package/bin/lib/daemon/sync.sh +95 -0
  83. package/bin/lib/database.sh +310 -0
  84. package/bin/lib/heimdall-setup.js +113 -0
  85. package/bin/lib/heimdall.js +265 -0
  86. package/bin/lib/json.sh +264 -0
  87. package/bin/lib/terminal.js +451 -0
  88. package/bin/lib/util.sh +126 -0
  89. package/bin/lib/vcs.js +349 -0
  90. package/config/agent-manifest.yaml +203 -0
  91. package/config/agents.json +168 -0
  92. package/config/task-template.md +159 -0
  93. package/config/task-types.yaml +106 -0
  94. package/context/agent-status/aegis.json +7 -0
  95. package/context/agent-status/anvil.json +7 -0
  96. package/context/agent-status/architect.json +7 -0
  97. package/context/agent-status/crucible.json +7 -0
  98. package/context/agent-status/ember.json +7 -0
  99. package/context/agent-status/furnace.json +7 -0
  100. package/context/agent-status/loki.json +7 -0
  101. package/context/agent-status/oracle.json +7 -0
  102. package/context/agent-status/pixel.json +7 -0
  103. package/context/agent-status/planning-hub.json +7 -0
  104. package/context/agent-status/scribe.json +7 -0
  105. package/context/agent-status/temper.json +7 -0
  106. package/context/feature-brainstorm.md +426 -0
  107. package/context/forge-state.yaml +19 -0
  108. package/context/modern-conventions.md +129 -0
  109. package/context/project-context-template.md +122 -0
  110. package/context/project-context.md +122 -0
  111. package/docs/TODO.md +150 -0
  112. package/docs/agents.md +409 -0
  113. package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
  114. package/docs/architecture/vibe-lab-integration.md +684 -0
  115. package/docs/architecture.md +194 -0
  116. package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
  117. package/docs/cleanup-workflow.md +329 -0
  118. package/docs/commands.md +451 -0
  119. package/docs/dashboard-mockup.html +989 -0
  120. package/docs/getting-started.md +261 -0
  121. package/docs/integration/forge-ownership-policy.md +112 -0
  122. package/docs/npm-publishing.md +132 -0
  123. package/docs/roadmap-2026.md +519 -0
  124. package/docs/security.md +144 -0
  125. package/docs/wireframes/dashboard-mvp.md +1164 -0
  126. package/docs/workflows/README.md +32 -0
  127. package/docs/workflows/azure-devops.md +108 -0
  128. package/docs/workflows/bitbucket.md +104 -0
  129. package/docs/workflows/git-only.md +130 -0
  130. package/docs/workflows/gitea.md +168 -0
  131. package/docs/workflows/github.md +103 -0
  132. package/docs/workflows/gitlab.md +105 -0
  133. package/docs/workflows.md +454 -0
  134. package/package.json +73 -0
  135. package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
  136. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
  137. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
  138. package/tasks/completed/ARCH-009-test-organization.md +78 -0
  139. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
  140. package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
  141. package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
  142. package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
  143. package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
  144. package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
  145. package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
  146. package/tasks/completed/CLEAN-001.md +38 -0
  147. package/tasks/completed/CLEAN-002.md +43 -0
  148. package/tasks/completed/CLEAN-003.md +47 -0
  149. package/tasks/completed/CLEAN-004.md +56 -0
  150. package/tasks/completed/CLEAN-005.md +75 -0
  151. package/tasks/completed/CLEAN-006.md +47 -0
  152. package/tasks/completed/CLEAN-007.md +34 -0
  153. package/tasks/completed/CLEAN-008.md +49 -0
  154. package/tasks/completed/CLEAN-012.md +58 -0
  155. package/tasks/completed/CLEAN-013.md +45 -0
  156. package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
  157. package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
  158. package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
  159. package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
  160. package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
  161. package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
  162. package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
  163. package/tasks/completed/PLAT-1-heimdall.md +420 -0
  164. package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
  165. package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
  166. package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
  167. package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
  168. package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
  169. package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
  170. package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
  171. package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
  172. package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
  173. package/tasks/completed/review-bmad-aegis.md +89 -0
  174. package/tasks/completed/review-bmad-anvil.md +80 -0
  175. package/tasks/completed/review-bmad-crucible.md +81 -0
  176. package/tasks/completed/review-bmad-ember.md +90 -0
  177. package/tasks/completed/review-bmad-furnace.md +79 -0
  178. package/tasks/completed/review-bmad-pixel.md +82 -0
  179. package/tasks/completed/review-bmad-scribe.md +92 -0
  180. package/tasks/completed/review-bmad-sentinel.md +83 -0
  181. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
  182. package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
  183. package/tasks/pending/ARCH-006-task-template-location.md +64 -0
  184. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
  185. package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
  186. package/tasks/pending/CLEAN-009.md +31 -0
  187. package/tasks/pending/CLEAN-010.md +30 -0
  188. package/tasks/pending/CLEAN-011.md +30 -0
  189. package/tasks/pending/CLEAN-014.md +32 -0
  190. package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
  191. package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
  192. package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
  193. package/tasks/review/bmad-review-aegis.md +349 -0
  194. package/tasks/review/bmad-review-anvil.md +259 -0
  195. package/tasks/review/bmad-review-crucible.md +277 -0
  196. package/tasks/review/bmad-review-ember.md +307 -0
  197. package/tasks/review/bmad-review-furnace.md +285 -0
  198. package/tasks/review/bmad-review-pixel.md +329 -0
  199. package/tasks/review/bmad-review-scribe.md +361 -0
  200. package/tasks/review/bmad-review-sentinel.md +242 -0
  201. package/tasks/review/task-001.md +78 -0
@@ -0,0 +1,275 @@
1
+ import { writable, get } from 'svelte/store';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Persistence
5
+ // localStorage is the session override; server config is the default.
6
+ // Boot order:
7
+ // 1. Fetch /api/config for server-side default (dashboard_voice)
8
+ // 2. If localStorage has an explicit override, honour it
9
+ // 3. Otherwise use server default
10
+ // ---------------------------------------------------------------------------
11
+ const STORAGE_KEY = 'vibe-forge-voice-enabled';
12
+ const STORAGE_OVERRIDE_KEY = 'vibe-forge-voice-overridden';
13
+
14
+ function _readStoredEnabled() {
15
+ if (typeof localStorage === 'undefined') return false;
16
+ const overridden = localStorage.getItem(STORAGE_OVERRIDE_KEY) === 'true';
17
+ if (overridden) return localStorage.getItem(STORAGE_KEY) === 'true';
18
+ return null; // means "use server default"
19
+ }
20
+
21
+ export const voiceEnabled = writable(_readStoredEnabled() ?? false);
22
+ export const voiceSpeaking = writable(false);
23
+
24
+ // Persist explicit user toggles
25
+ voiceEnabled.subscribe(val => {
26
+ if (typeof localStorage !== 'undefined') {
27
+ localStorage.setItem(STORAGE_KEY, String(val));
28
+ }
29
+ });
30
+
31
+ // Fetch server config and apply default if user hasn't overridden
32
+ if (typeof window !== 'undefined' && _readStoredEnabled() === null) {
33
+ fetch('/api/config')
34
+ .then(r => r.ok ? r.json() : null)
35
+ .catch(() => null)
36
+ .then(cfg => {
37
+ if (cfg?.dashboard_voice === true) {
38
+ voiceEnabled.set(true);
39
+ }
40
+ });
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Agent voice profiles
45
+ // gender: 'male' | 'female'
46
+ // pitch: 0.0–2.0 (1.0 = default)
47
+ // rate: 0.1–10 (1.0 = default)
48
+ // ---------------------------------------------------------------------------
49
+ const PROFILES = {
50
+ 'planning-hub': { gender: 'male', pitch: 0.80, rate: 0.88 }, // deep, measured authority
51
+ 'forge-master': { gender: 'male', pitch: 0.80, rate: 0.88 },
52
+ 'oracle': { gender: 'female', pitch: 1.00, rate: 0.92 }, // thoughtful, unhurried
53
+ 'architect': { gender: 'male', pitch: 0.85, rate: 0.85 }, // calm, deliberate
54
+ 'aegis': { gender: 'female', pitch: 1.10, rate: 1.15 }, // sharp, urgent
55
+ 'pixel': { gender: 'female', pitch: 1.18, rate: 1.05 }, // warm, expressive
56
+ 'ember': { gender: 'male', pitch: 0.92, rate: 1.02 }, // practical, even
57
+ 'anvil': { gender: 'male', pitch: 1.00, rate: 1.05 }, // direct, energetic
58
+ 'furnace': { gender: 'male', pitch: 0.88, rate: 1.00 }, // solid, reliable
59
+ 'crucible': { gender: 'female', pitch: 1.02, rate: 0.98 }, // precise, skeptical cadence
60
+ 'temper': { gender: 'male', pitch: 0.95, rate: 0.90 }, // composed, measured
61
+ 'scribe': { gender: 'female', pitch: 1.05, rate: 0.88 }, // clear, articulate
62
+ 'herald': { gender: 'male', pitch: 1.00, rate: 1.12 }, // bright, announcement energy
63
+ 'loki': { gender: 'male', pitch: 1.12, rate: 1.08 }, // bright, quicksilver energy
64
+ 'system': { gender: 'female', pitch: 1.00, rate: 1.00 },
65
+ };
66
+
67
+ const DEFAULT_PROFILE = { gender: 'male', pitch: 1.0, rate: 1.0 };
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Voice pool — loaded once, then cached
71
+ // ---------------------------------------------------------------------------
72
+ let _maleVoices = [];
73
+ let _femaleVoices = [];
74
+ let _voicesReady = false;
75
+
76
+ const FEMALE_HINTS = /female|zira|samantha|victoria|fiona|kate|moira|veena|tessa|karen|susan|linda|heather|allison|ava|nicky|sandy|shelley/i;
77
+ const MALE_HINTS = /\bmale\b|david|mark|alex|daniel|james|richard|george|thomas|reed|fred|ralph|bruce|lee/i;
78
+
79
+ function _loadVoices() {
80
+ if (!('speechSynthesis' in window)) return;
81
+ const all = window.speechSynthesis.getVoices();
82
+ if (all.length === 0) return;
83
+
84
+ // Prefer English voices
85
+ const en = all.filter(v => v.lang.startsWith('en'));
86
+ const pool = en.length > 0 ? en : all;
87
+
88
+ _femaleVoices = pool.filter(v => FEMALE_HINTS.test(v.name));
89
+ _maleVoices = pool.filter(v => MALE_HINTS.test(v.name));
90
+
91
+ // If heuristics came up empty, split the pool evenly as fallback
92
+ if (_femaleVoices.length === 0 && _maleVoices.length === 0) {
93
+ _maleVoices = pool.filter((_, i) => i % 2 === 0);
94
+ _femaleVoices = pool.filter((_, i) => i % 2 === 1);
95
+ } else {
96
+ // Ensure neither pool is empty
97
+ if (_femaleVoices.length === 0) _femaleVoices = _maleVoices;
98
+ if (_maleVoices.length === 0) _maleVoices = _femaleVoices;
99
+ }
100
+
101
+ _voicesReady = true;
102
+ }
103
+
104
+ // Deterministic: same agent always gets the same voice from its pool
105
+ function _pickVoice(agentName, gender) {
106
+ const pool = gender === 'female' ? _femaleVoices : _maleVoices;
107
+ if (pool.length === 0) return null;
108
+ let hash = 0;
109
+ for (const ch of agentName) hash = (hash * 31 + ch.charCodeAt(0)) & 0xffffffff;
110
+ return pool[Math.abs(hash) % pool.length];
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Server TTS probe — check once whether /api/tts is available
115
+ // ---------------------------------------------------------------------------
116
+ let _serverTtsAvailable = null; // null = unknown, true/false = known
117
+
118
+ async function _probeServerTts() {
119
+ if (_serverTtsAvailable !== null) return _serverTtsAvailable;
120
+ try {
121
+ const r = await fetch('/api/tts?text=ok&agent=system');
122
+ _serverTtsAvailable = r.ok;
123
+ // Drain the response body so the connection is released cleanly
124
+ if (r.ok) r.blob().catch(() => {});
125
+ } catch {
126
+ _serverTtsAvailable = false;
127
+ }
128
+ return _serverTtsAvailable;
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Audio queue — prevents utterances from trampling each other
133
+ // Supports both Audio (server TTS) and SpeechSynthesisUtterance (Web Speech)
134
+ // ---------------------------------------------------------------------------
135
+ let _queue = [];
136
+ let _busy = false;
137
+ let _currentAudio = null; // for server-TTS Audio objects
138
+
139
+ function _flush() {
140
+ if (_busy || _queue.length === 0) return;
141
+ const item = _queue.shift();
142
+ _busy = true;
143
+ voiceSpeaking.set(true);
144
+
145
+ const done = () => {
146
+ _busy = false;
147
+ _currentAudio = null;
148
+ voiceSpeaking.set(_queue.length > 0);
149
+ _flush();
150
+ };
151
+
152
+ if (item.audio) {
153
+ _currentAudio = item.audio;
154
+ item.audio.onended = done;
155
+ item.audio.onerror = done;
156
+ item.audio.play().catch(done);
157
+ } else {
158
+ item.utt.onend = done;
159
+ item.utt.onerror = done;
160
+ window.speechSynthesis.speak(item.utt);
161
+ }
162
+ }
163
+
164
+ function _cancelAll() {
165
+ _queue = [];
166
+ if (_currentAudio) {
167
+ _currentAudio.pause();
168
+ _currentAudio.src = '';
169
+ _currentAudio = null;
170
+ }
171
+ if ('speechSynthesis' in window) window.speechSynthesis.cancel();
172
+ _busy = false;
173
+ voiceSpeaking.set(false);
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Public API
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /**
181
+ * Speak text in an agent's voice (server TTS preferred, Web Speech fallback).
182
+ * @param {string} text
183
+ * @param {string} [agentName] — key from PROFILES (e.g. 'aegis')
184
+ * @param {boolean} [interrupt] — cancel current speech and go immediately
185
+ */
186
+ export async function speak(text, agentName = 'system', interrupt = false) {
187
+ if (!get(voiceEnabled)) return;
188
+ if (!text || !text.trim()) return;
189
+
190
+ if (interrupt) _cancelAll();
191
+
192
+ // Cap queue at 4 to avoid stale backlogs
193
+ if (_queue.length >= 4) return;
194
+
195
+ const agent = agentName.toLowerCase();
196
+
197
+ // Try server-side Edge TTS first
198
+ const serverAvailable = await _probeServerTts().catch(() => false);
199
+ if (serverAvailable) {
200
+ try {
201
+ const params = new URLSearchParams({ text: text.trim(), agent });
202
+ const res = await fetch(`/api/tts?${params}`);
203
+ if (res.ok) {
204
+ const blob = await res.blob();
205
+ const audio = new Audio(URL.createObjectURL(blob));
206
+ _queue.push({ audio });
207
+ _flush();
208
+ return;
209
+ }
210
+ // non-OK response — fall through to Web Speech
211
+ _serverTtsAvailable = false;
212
+ } catch {
213
+ _serverTtsAvailable = false;
214
+ }
215
+ }
216
+
217
+ // Fallback: Web Speech API
218
+ if (!('speechSynthesis' in window)) return;
219
+ if (!_voicesReady) _loadVoices();
220
+
221
+ const profile = PROFILES[agent] || DEFAULT_PROFILE;
222
+ const voice = _pickVoice(agent, profile.gender);
223
+
224
+ const utt = new SpeechSynthesisUtterance(text);
225
+ utt.pitch = profile.pitch;
226
+ utt.rate = profile.rate;
227
+ utt.volume = 1.0;
228
+ if (voice) utt.voice = voice;
229
+
230
+ _queue.push({ utt });
231
+ _flush();
232
+ }
233
+
234
+ export function stopSpeaking() {
235
+ _cancelAll();
236
+ }
237
+
238
+ export function toggleVoice() {
239
+ voiceEnabled.update(v => {
240
+ if (v) stopSpeaking();
241
+ const next = !v;
242
+ // Mark as explicitly overridden so server default no longer applies
243
+ if (typeof localStorage !== 'undefined') {
244
+ localStorage.setItem(STORAGE_OVERRIDE_KEY, 'true');
245
+ }
246
+ return next;
247
+ });
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Infer agent from a notification object (best-effort)
252
+ // ---------------------------------------------------------------------------
253
+ const AGENT_NAMES = Object.keys(PROFILES);
254
+
255
+ export function inferAgent(notification) {
256
+ if (notification?.agent) return notification.agent.toLowerCase();
257
+
258
+ // Scan the message text for a known agent name
259
+ const haystack = `${notification?.title || ''} ${notification?.message || ''}`.toLowerCase();
260
+ for (const name of AGENT_NAMES) {
261
+ if (haystack.includes(name)) return name;
262
+ }
263
+ return 'system';
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Initialise voice pool as early as possible
268
+ // ---------------------------------------------------------------------------
269
+ if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
270
+ if (window.speechSynthesis.getVoices().length > 0) {
271
+ _loadVoices();
272
+ } else {
273
+ window.speechSynthesis.addEventListener('voiceschanged', _loadVoices, { once: true });
274
+ }
275
+ }
@@ -0,0 +1,295 @@
1
+ import { writable, get } from 'svelte/store';
2
+ import { tasks, tasksLoading, tasksError } from './tasks.js';
3
+ import { agents, agentsLoading, agentsError } from './agents.js';
4
+ import { issues, issuesLoading, issuesError } from './issues.js';
5
+ import { addNotification } from './notifications.js';
6
+ import { fetchTasks, fetchAgents, fetchIssues } from '../utils/api.js';
7
+ import { speak, inferAgent } from './voice.js';
8
+
9
+ // Configuration
10
+ const CONFIG = {
11
+ RECONNECT_DELAY: 3000,
12
+ RECONNECT_MAX_DELAY: 30000,
13
+ MIN_LOADING_TIME: 300,
14
+ };
15
+
16
+ // WebSocket state
17
+ export const wsConnected = writable(false);
18
+ export const lastUpdated = writable(null);
19
+ export const showConnectionBanner = writable(false);
20
+
21
+ let ws = null;
22
+ let reconnectAttempts = 0;
23
+ let reconnectTimeout = null;
24
+
25
+ // Get WebSocket URL
26
+ function getWsUrl() {
27
+ const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
28
+ return `${protocol}://${window.location.host}/ws`;
29
+ }
30
+
31
+ // Connect to WebSocket
32
+ export function connectWebSocket() {
33
+ if (ws && ws.readyState === WebSocket.OPEN) {
34
+ return;
35
+ }
36
+
37
+ ws = new WebSocket(getWsUrl());
38
+
39
+ ws.onopen = () => {
40
+ wsConnected.set(true);
41
+ reconnectAttempts = 0;
42
+ showConnectionBanner.set(false);
43
+
44
+ // Refresh all data on reconnect
45
+ refreshAll();
46
+ };
47
+
48
+ ws.onmessage = (event) => {
49
+ try {
50
+ const data = JSON.parse(event.data);
51
+ handleWebSocketMessage(data);
52
+ } catch (e) {
53
+ console.error('Failed to parse WebSocket message:', e);
54
+ }
55
+ };
56
+
57
+ ws.onclose = () => {
58
+ wsConnected.set(false);
59
+ scheduleReconnect();
60
+ };
61
+
62
+ ws.onerror = (error) => {
63
+ console.error('WebSocket error:', error);
64
+ wsConnected.set(false);
65
+ };
66
+ }
67
+
68
+ function scheduleReconnect() {
69
+ if (reconnectTimeout) {
70
+ clearTimeout(reconnectTimeout);
71
+ }
72
+
73
+ const delay = Math.min(
74
+ CONFIG.RECONNECT_DELAY * Math.pow(2, reconnectAttempts),
75
+ CONFIG.RECONNECT_MAX_DELAY
76
+ );
77
+
78
+ reconnectAttempts++;
79
+
80
+ if (reconnectAttempts > 3) {
81
+ showConnectionBanner.set(true);
82
+ }
83
+
84
+ reconnectTimeout = setTimeout(connectWebSocket, delay);
85
+ }
86
+
87
+ function handleWebSocketMessage(data) {
88
+ switch (data.event || data.type) {
89
+ case 'task-created':
90
+ case 'task-updated':
91
+ case 'task-status-changed':
92
+ refreshTasks();
93
+ if (data.task) {
94
+ addNotification({
95
+ type: 'info',
96
+ title: 'Task Updated',
97
+ message: `${data.task.id}: ${data.task.title || 'Status changed'}`,
98
+ });
99
+ speak(`Task updated. ${data.task.title || data.task.id}`, 'forge-master');
100
+ }
101
+ break;
102
+
103
+ case 'agent-status-changed':
104
+ refreshAgents();
105
+ if (data.agent && data.status) {
106
+ speak(`${data.agent} is now ${data.status}.`, data.agent);
107
+ }
108
+ break;
109
+
110
+ case 'issue-detected':
111
+ refreshIssues();
112
+ addNotification({
113
+ type: 'warning',
114
+ title: 'New Issue Detected',
115
+ message: data.issue?.title || 'Check the Issues panel',
116
+ });
117
+ speak(`Issue detected. ${data.issue?.title || 'Check the issues panel.'}`, 'aegis');
118
+ break;
119
+
120
+ case 'dispatch-success':
121
+ refreshTasks();
122
+ refreshIssues();
123
+ speak('Task dispatched.', 'forge-master');
124
+ break;
125
+
126
+ case 'notification': {
127
+ const notif = data.notification;
128
+ addNotification(notif);
129
+ const agent = inferAgent(notif);
130
+ speak(notif?.message || notif?.title || '', agent);
131
+ break;
132
+ }
133
+
134
+ case 'heartbeat':
135
+ updateLastUpdated();
136
+ break;
137
+
138
+ case 'connected':
139
+ case 'pong':
140
+ case 'subscribed':
141
+ // Server handshake messages — no action needed
142
+ break;
143
+
144
+ default:
145
+ console.log('Unknown WebSocket message:', data);
146
+ }
147
+ }
148
+
149
+ function updateLastUpdated() {
150
+ lastUpdated.set(new Date().toISOString());
151
+ }
152
+
153
+ // Force reconnect
154
+ export function reconnect() {
155
+ reconnectAttempts = 0;
156
+ if (reconnectTimeout) {
157
+ clearTimeout(reconnectTimeout);
158
+ }
159
+ connectWebSocket();
160
+ }
161
+
162
+ // Data refresh functions
163
+ export async function refreshTasks() {
164
+ const startTime = Date.now();
165
+ tasksLoading.set(true);
166
+ tasksError.set(null);
167
+
168
+ try {
169
+ const data = await fetchTasks();
170
+
171
+ // Ensure minimum loading time
172
+ const elapsed = Date.now() - startTime;
173
+ if (elapsed < CONFIG.MIN_LOADING_TIME) {
174
+ await new Promise(r => setTimeout(r, CONFIG.MIN_LOADING_TIME - elapsed));
175
+ }
176
+
177
+ // API returns tasks grouped by directory: { pending: [], 'in-progress': [], review: [], completed: [] }
178
+ // Flatten to a single array with status normalized from the directory key
179
+ if (data && (data.pending || data['in-progress'] || data.review || data.completed)) {
180
+ const flatTasks = [
181
+ ...(data.pending || []).map(t => ({ ...t, status: t.status === 'unknown' ? 'pending' : t.status })),
182
+ ...(data['in-progress'] || []).map(t => ({ ...t, status: t.status === 'unknown' ? 'in-progress' : t.status })),
183
+ ...(data.review || []).map(t => ({ ...t, status: t.status === 'unknown' ? 'review' : t.status })),
184
+ ...(data.completed || []).map(t => ({ ...t, status: t.status === 'unknown' ? 'completed' : t.status })),
185
+ ];
186
+ tasks.set(flatTasks);
187
+ } else {
188
+ tasks.set(data.tasks || data || []);
189
+ }
190
+ tasksLoading.set(false);
191
+ } catch (error) {
192
+ console.error('Failed to fetch tasks:', error);
193
+ tasksError.set(error.message);
194
+ tasksLoading.set(false);
195
+ }
196
+ }
197
+
198
+ export async function refreshAgents() {
199
+ const startTime = Date.now();
200
+ agentsLoading.set(true);
201
+ agentsError.set(null);
202
+
203
+ try {
204
+ const data = await fetchAgents();
205
+
206
+ const elapsed = Date.now() - startTime;
207
+ if (elapsed < CONFIG.MIN_LOADING_TIME) {
208
+ await new Promise(r => setTimeout(r, CONFIG.MIN_LOADING_TIME - elapsed));
209
+ }
210
+
211
+ agents.set(data.agents || data || []);
212
+ agentsLoading.set(false);
213
+ } catch (error) {
214
+ console.error('Failed to fetch agents:', error);
215
+ agentsError.set(error.message);
216
+ agentsLoading.set(false);
217
+ }
218
+ }
219
+
220
+ export async function refreshIssues() {
221
+ const startTime = Date.now();
222
+ issuesLoading.set(true);
223
+ issuesError.set(null);
224
+
225
+ try {
226
+ const data = await fetchIssues();
227
+
228
+ const elapsed = Date.now() - startTime;
229
+ if (elapsed < CONFIG.MIN_LOADING_TIME) {
230
+ await new Promise(r => setTimeout(r, CONFIG.MIN_LOADING_TIME - elapsed));
231
+ }
232
+
233
+ issues.set(data.issues || data || []);
234
+ issuesLoading.set(false);
235
+ } catch (error) {
236
+ console.error('Failed to fetch issues:', error);
237
+ issuesError.set(error.message);
238
+ issuesLoading.set(false);
239
+ }
240
+ }
241
+
242
+ export async function refreshAll() {
243
+ await Promise.all([
244
+ refreshTasks(),
245
+ refreshAgents(),
246
+ refreshIssues(),
247
+ ]);
248
+ updateLastUpdated();
249
+ }
250
+
251
+ // Setup periodic refresh as fallback
252
+ let periodicRefreshInterval = null;
253
+
254
+ export function startPeriodicRefresh() {
255
+ if (periodicRefreshInterval) return;
256
+
257
+ periodicRefreshInterval = setInterval(() => {
258
+ if (!get(wsConnected)) {
259
+ refreshAll();
260
+ }
261
+ }, 30000);
262
+ }
263
+
264
+ export function stopPeriodicRefresh() {
265
+ if (periodicRefreshInterval) {
266
+ clearInterval(periodicRefreshInterval);
267
+ periodicRefreshInterval = null;
268
+ }
269
+ }
270
+
271
+ // Disconnect WebSocket and cleanup all resources
272
+ export function disconnectWebSocket() {
273
+ // Clear reconnect timeout
274
+ if (reconnectTimeout) {
275
+ clearTimeout(reconnectTimeout);
276
+ reconnectTimeout = null;
277
+ }
278
+
279
+ // Stop periodic refresh
280
+ stopPeriodicRefresh();
281
+
282
+ // Close WebSocket connection
283
+ if (ws) {
284
+ ws.onclose = null; // Prevent scheduleReconnect from being called
285
+ ws.onerror = null;
286
+ ws.onmessage = null;
287
+ ws.onopen = null;
288
+ ws.close();
289
+ ws = null;
290
+ }
291
+
292
+ // Reset state
293
+ wsConnected.set(false);
294
+ reconnectAttempts = 0;
295
+ }
@@ -0,0 +1,101 @@
1
+ const API_BASE = window.location.origin;
2
+
3
+ async function fetchApi(endpoint, options = {}) {
4
+ const response = await fetch(`${API_BASE}${endpoint}`, {
5
+ headers: {
6
+ 'Content-Type': 'application/json',
7
+ ...options.headers,
8
+ },
9
+ ...options,
10
+ });
11
+
12
+ if (!response.ok) {
13
+ const error = new Error(`HTTP ${response.status}`);
14
+ error.status = response.status;
15
+ try {
16
+ error.data = await response.json();
17
+ } catch (e) {
18
+ error.data = null;
19
+ }
20
+ throw error;
21
+ }
22
+
23
+ return response.json();
24
+ }
25
+
26
+ export async function fetchTasks() {
27
+ return fetchApi('/api/tasks');
28
+ }
29
+
30
+ export async function fetchAgents() {
31
+ return fetchApi('/api/agents');
32
+ }
33
+
34
+ export async function fetchIssues() {
35
+ // Issues endpoint may not exist yet, mock data for now
36
+ try {
37
+ return await fetchApi('/api/issues');
38
+ } catch (e) {
39
+ // Return mock issues for demonstration
40
+ return {
41
+ issues: [
42
+ {
43
+ id: 'issue-1',
44
+ category: 'doc',
45
+ title: 'Stale Docs: README.md',
46
+ target: 'README.md',
47
+ details: 'Last updated 45 days ago',
48
+ meta: 'Threshold: 30 days | Severity: Medium',
49
+ agent: 'scribe',
50
+ priority: 'medium',
51
+ },
52
+ {
53
+ id: 'issue-2',
54
+ category: 'test',
55
+ title: 'Failing Test: auth.test.js',
56
+ target: 'auth.test.js',
57
+ details: '2 tests failing: testLogin, testLogout',
58
+ meta: 'First failed: yesterday 3:45 PM',
59
+ agent: 'crucible',
60
+ priority: 'high',
61
+ },
62
+ {
63
+ id: 'issue-3',
64
+ category: 'sec',
65
+ title: 'Security: lodash vulnerability',
66
+ target: 'package.json',
67
+ details: 'CVE-2021-23337 (high severity)',
68
+ meta: 'Affects: lodash < 4.17.21',
69
+ agent: 'aegis',
70
+ priority: 'critical',
71
+ },
72
+ {
73
+ id: 'issue-4',
74
+ category: 'cov',
75
+ title: 'Low Coverage: utils.js',
76
+ target: 'src/utils.js',
77
+ details: '34% coverage (threshold: 80%)',
78
+ meta: 'Missing: 12 uncovered functions',
79
+ agent: 'crucible',
80
+ priority: 'medium',
81
+ },
82
+ ],
83
+ };
84
+ }
85
+ }
86
+
87
+ export async function dispatchTask(issue) {
88
+ return fetchApi('/api/dispatch', {
89
+ method: 'POST',
90
+ body: JSON.stringify({
91
+ type: issue.category,
92
+ target: issue.target,
93
+ agent: issue.agent,
94
+ context: {
95
+ title: issue.title,
96
+ details: issue.details,
97
+ priority: issue.priority,
98
+ },
99
+ }),
100
+ });
101
+ }