@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.
- package/.claude/commands/clear-attention.md +63 -0
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -0
- package/.claude/commands/forge.md +171 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.js +198 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +46 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/agents/aegis/personality.md +294 -0
- package/agents/anvil/personality.md +276 -0
- package/agents/architect/personality.md +258 -0
- package/agents/crucible/personality.md +360 -0
- package/agents/ember/personality.md +291 -0
- package/agents/forge-master/capabilities.md +144 -0
- package/agents/forge-master/context-template.md +128 -0
- package/agents/forge-master/personality.md +138 -0
- package/agents/furnace/personality.md +340 -0
- package/agents/herald/personality.md +247 -0
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +283 -0
- package/agents/pixel/personality.md +113 -0
- package/agents/planning-hub/personality.md +320 -0
- package/agents/scribe/personality.md +251 -0
- package/agents/temper/personality.md +218 -0
- package/bin/cli.js +375 -0
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +483 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/frontend/index.html +13 -0
- package/bin/dashboard/frontend/package.json +16 -0
- package/bin/dashboard/frontend/src/App.svelte +222 -0
- package/bin/dashboard/frontend/src/app.css +1777 -0
- package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
- package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
- package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
- package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
- package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
- package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
- package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
- package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
- package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
- package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
- package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
- package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
- package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
- package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
- package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
- package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
- package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
- package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
- package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
- package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
- package/bin/dashboard/frontend/src/main.js +9 -0
- package/bin/dashboard/frontend/svelte.config.js +5 -0
- package/bin/dashboard/frontend/vite.config.js +20 -0
- package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
- package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +566 -0
- package/bin/forge-daemon.sh +463 -0
- package/bin/forge-setup.sh +645 -0
- package/bin/forge-spawn.sh +164 -0
- package/bin/forge.cmd +83 -0
- package/bin/forge.sh +533 -0
- package/bin/lib/agents.sh +177 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +347 -0
- package/bin/lib/constants.sh +241 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +263 -0
- package/bin/lib/daemon/routing.sh +77 -0
- package/bin/lib/daemon/state.sh +115 -0
- package/bin/lib/daemon/sync.sh +95 -0
- package/bin/lib/database.sh +310 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -0
- package/bin/lib/terminal.js +451 -0
- package/bin/lib/util.sh +126 -0
- package/bin/lib/vcs.js +349 -0
- package/config/agent-manifest.yaml +203 -0
- package/config/agents.json +168 -0
- package/config/task-template.md +159 -0
- package/config/task-types.yaml +106 -0
- package/context/agent-status/aegis.json +7 -0
- package/context/agent-status/anvil.json +7 -0
- package/context/agent-status/architect.json +7 -0
- package/context/agent-status/crucible.json +7 -0
- package/context/agent-status/ember.json +7 -0
- package/context/agent-status/furnace.json +7 -0
- package/context/agent-status/loki.json +7 -0
- package/context/agent-status/oracle.json +7 -0
- package/context/agent-status/pixel.json +7 -0
- package/context/agent-status/planning-hub.json +7 -0
- package/context/agent-status/scribe.json +7 -0
- package/context/agent-status/temper.json +7 -0
- package/context/feature-brainstorm.md +426 -0
- package/context/forge-state.yaml +19 -0
- package/context/modern-conventions.md +129 -0
- package/context/project-context-template.md +122 -0
- package/context/project-context.md +122 -0
- package/docs/TODO.md +150 -0
- package/docs/agents.md +409 -0
- package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
- package/docs/architecture/vibe-lab-integration.md +684 -0
- package/docs/architecture.md +194 -0
- package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
- package/docs/cleanup-workflow.md +329 -0
- package/docs/commands.md +451 -0
- package/docs/dashboard-mockup.html +989 -0
- package/docs/getting-started.md +261 -0
- package/docs/integration/forge-ownership-policy.md +112 -0
- package/docs/npm-publishing.md +132 -0
- package/docs/roadmap-2026.md +519 -0
- package/docs/security.md +144 -0
- package/docs/wireframes/dashboard-mvp.md +1164 -0
- package/docs/workflows/README.md +32 -0
- package/docs/workflows/azure-devops.md +108 -0
- package/docs/workflows/bitbucket.md +104 -0
- package/docs/workflows/git-only.md +130 -0
- package/docs/workflows/gitea.md +168 -0
- package/docs/workflows/github.md +103 -0
- package/docs/workflows/gitlab.md +105 -0
- package/docs/workflows.md +454 -0
- package/package.json +73 -0
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
- package/tasks/completed/ARCH-009-test-organization.md +78 -0
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
- package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
- package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
- package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
- package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
- package/tasks/completed/CLEAN-001.md +38 -0
- package/tasks/completed/CLEAN-002.md +43 -0
- package/tasks/completed/CLEAN-003.md +47 -0
- package/tasks/completed/CLEAN-004.md +56 -0
- package/tasks/completed/CLEAN-005.md +75 -0
- package/tasks/completed/CLEAN-006.md +47 -0
- package/tasks/completed/CLEAN-007.md +34 -0
- package/tasks/completed/CLEAN-008.md +49 -0
- package/tasks/completed/CLEAN-012.md +58 -0
- package/tasks/completed/CLEAN-013.md +45 -0
- package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
- package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
- package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
- package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
- package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
- package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
- package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
- package/tasks/completed/PLAT-1-heimdall.md +420 -0
- package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
- package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
- package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
- package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
- package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
- package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
- package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
- package/tasks/completed/review-bmad-aegis.md +89 -0
- package/tasks/completed/review-bmad-anvil.md +80 -0
- package/tasks/completed/review-bmad-crucible.md +81 -0
- package/tasks/completed/review-bmad-ember.md +90 -0
- package/tasks/completed/review-bmad-furnace.md +79 -0
- package/tasks/completed/review-bmad-pixel.md +82 -0
- package/tasks/completed/review-bmad-scribe.md +92 -0
- package/tasks/completed/review-bmad-sentinel.md +83 -0
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
- package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
- package/tasks/pending/ARCH-006-task-template-location.md +64 -0
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
- package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
- package/tasks/pending/CLEAN-009.md +31 -0
- package/tasks/pending/CLEAN-010.md +30 -0
- package/tasks/pending/CLEAN-011.md +30 -0
- package/tasks/pending/CLEAN-014.md +32 -0
- package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
- package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
- package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
- package/tasks/review/bmad-review-aegis.md +349 -0
- package/tasks/review/bmad-review-anvil.md +259 -0
- package/tasks/review/bmad-review-crucible.md +277 -0
- package/tasks/review/bmad-review-ember.md +307 -0
- package/tasks/review/bmad-review-furnace.md +285 -0
- package/tasks/review/bmad-review-pixel.md +329 -0
- package/tasks/review/bmad-review-scribe.md +361 -0
- package/tasks/review/bmad-review-sentinel.md +242 -0
- 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
|
+
}
|