catalyst-os 2.0.3 → 3.0.1
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/.catalyst/bin/install.js +46 -0
- package/.catalyst/main/project-config.yaml +16 -0
- package/.catalyst/spec-structure.yaml +1 -1
- package/.catalyst/voice/README.md +78 -0
- package/.catalyst/voice/artifact.js +61 -0
- package/.catalyst/voice/meet-server.js +293 -0
- package/.catalyst/voice/meeting-diy.html +205 -0
- package/.catalyst/voice/meeting.html +198 -0
- package/.catalyst/voice/package.json +11 -0
- package/.claude/agents/arbiter.md +4 -2
- package/.claude/agents/curator.md +75 -0
- package/.claude/commands/audit-spec.md +1 -1
- package/.claude/commands/meet-spec.md +43 -0
- package/.claude/commands/seal-spec.md +16 -0
- package/.claude/skills/meet-spec/SKILL.md +112 -0
- package/.claude/skills/spec-approval/SKILL.md +8 -1
- package/.claude/skills/spec-validation/SKILL.md +5 -3
- package/.claude/skills/using-skills/SKILL.md +1 -0
- package/AGENTS.md +2 -1
- package/README.md +37 -1
- package/package.json +2 -1
|
@@ -0,0 +1,205 @@
|
|
|
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" />
|
|
6
|
+
<title>Catalyst Meeting Room (DIY)</title>
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
|
|
8
|
+
<style>
|
|
9
|
+
:root { --bg:#0e0e14; --panel:#1a1a24; --accent:#4fd1a5; --text:#e8e8f0; --dim:#8a8a9a; }
|
|
10
|
+
* { box-sizing: border-box; }
|
|
11
|
+
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
12
|
+
background:var(--bg); color:var(--text); }
|
|
13
|
+
.room { max-width:1080px; margin:0 auto; padding:28px 24px; }
|
|
14
|
+
h1 { font-size:20px; font-weight:600; margin:0 0 4px; }
|
|
15
|
+
.topic { color:var(--dim); margin:0 0 22px; font-size:14px; }
|
|
16
|
+
.split { display:flex; gap:20px; align-items:flex-start; }
|
|
17
|
+
.notes-pane { flex:1; min-width:0; background:var(--panel); border:1px solid #2a2a36;
|
|
18
|
+
border-radius:12px; overflow:hidden; }
|
|
19
|
+
.chat-pane { width:360px; flex:none; }
|
|
20
|
+
@media (max-width:820px){ .split{flex-direction:column;} .chat-pane{width:100%;} }
|
|
21
|
+
.pane-h { padding:12px 16px; font-size:13px; font-weight:600; color:var(--dim);
|
|
22
|
+
border-bottom:1px solid #2a2a36; letter-spacing:.04em; text-transform:uppercase; }
|
|
23
|
+
.notes { padding:6px 20px 18px; max-height:62vh; overflow-y:auto; font-size:14px; line-height:1.55; }
|
|
24
|
+
.notes h1 { font-size:18px; } .notes h2 { font-size:15px; color:var(--accent); margin:18px 0 6px; }
|
|
25
|
+
.notes ul { margin:6px 0; padding-left:20px; } .notes li { margin:3px 0; }
|
|
26
|
+
.notes .empty { color:var(--dim); font-style:italic; }
|
|
27
|
+
.controls { text-align:center; }
|
|
28
|
+
.orb { width:84px; height:84px; border-radius:50%; margin:6px auto 12px;
|
|
29
|
+
background:radial-gradient(circle at 35% 35%, #8fe6c8, var(--accent));
|
|
30
|
+
box-shadow:0 0 0 0 rgba(79,209,165,.5); transition:transform .2s; }
|
|
31
|
+
.orb.listening { animation:pulse 2s infinite; }
|
|
32
|
+
.orb.speaking { animation:pulse .7s infinite; transform:scale(1.08); }
|
|
33
|
+
.orb.thinking { background:radial-gradient(circle at 35% 35%,#ffd27a,#ff9b3d); animation:spin 1.1s linear infinite; }
|
|
34
|
+
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(79,209,165,.5);} 70%{box-shadow:0 0 0 18px rgba(79,209,165,0);} 100%{box-shadow:0 0 0 0 rgba(79,209,165,0);} }
|
|
35
|
+
@keyframes spin { to { transform:rotate(360deg);} }
|
|
36
|
+
.status { color:var(--dim); min-height:20px; margin-bottom:14px; font-size:13px; }
|
|
37
|
+
.btns { display:flex; gap:10px; justify-content:center; }
|
|
38
|
+
button { border:none; border-radius:10px; padding:10px 18px; font-size:14px; font-weight:600; cursor:pointer; }
|
|
39
|
+
.start { background:var(--accent); color:#06281e; } .end { background:#2a2a36; color:var(--text); }
|
|
40
|
+
button:disabled { opacity:.4; cursor:not-allowed; }
|
|
41
|
+
.chat { display:flex; gap:8px; margin-top:14px; }
|
|
42
|
+
.chat input { flex:1; min-width:0; background:var(--panel); border:1px solid #2a2a36; color:var(--text);
|
|
43
|
+
border-radius:8px; padding:9px 11px; font-size:14px; }
|
|
44
|
+
.chat input:focus { outline:none; border-color:var(--accent); }
|
|
45
|
+
.chat button { background:#2a2a36; color:var(--text); }
|
|
46
|
+
.log { margin-top:14px; background:var(--panel); border:1px solid #2a2a36; border-radius:10px;
|
|
47
|
+
padding:12px; max-height:34vh; overflow-y:auto; font-size:13px; line-height:1.5; }
|
|
48
|
+
.log .u { color:#7fd1ff; } .log .a { color:#8fe6c8; } .log .t { color:var(--dim); font-style:italic; }
|
|
49
|
+
.hint { color:var(--dim); font-size:12px; margin-top:16px; }
|
|
50
|
+
.done { text-align:center; padding:30px; } .done code { background:var(--panel); padding:4px 8px; border-radius:6px; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<div class="room">
|
|
55
|
+
<h1>Catalyst Meeting Room</h1>
|
|
56
|
+
<p class="topic" id="topic">Loading…</p>
|
|
57
|
+
<div class="split">
|
|
58
|
+
<section class="notes-pane">
|
|
59
|
+
<div class="pane-h">Meeting Notes — live</div>
|
|
60
|
+
<div class="notes" id="notes"><p class="empty">Notes will build up here as you talk…</p></div>
|
|
61
|
+
</section>
|
|
62
|
+
<section class="chat-pane">
|
|
63
|
+
<div class="controls">
|
|
64
|
+
<div class="orb" id="orb"></div>
|
|
65
|
+
<div class="status" id="status">Ready when you are.</div>
|
|
66
|
+
<div class="btns">
|
|
67
|
+
<button class="start" id="startBtn">Start</button>
|
|
68
|
+
<button class="end" id="endBtn" disabled>End meeting</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="chat" id="chatRow" hidden>
|
|
72
|
+
<input id="chatInput" type="text" placeholder="Type instead of speaking…" autocomplete="off" />
|
|
73
|
+
<button id="chatSend">Send</button>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="log" id="log"></div>
|
|
76
|
+
</section>
|
|
77
|
+
</div>
|
|
78
|
+
<p class="hint">Free mode: browser speech + Claude reading the codebase. No ElevenLabs.</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<script>
|
|
82
|
+
const $ = (id) => document.getElementById(id);
|
|
83
|
+
const orb = $('orb'), status = $('status'), log = $('log'), notes = $('notes');
|
|
84
|
+
const startBtn = $('startBtn'), endBtn = $('endBtn');
|
|
85
|
+
const chatRow = $('chatRow'), chatInput = $('chatInput'), chatSend = $('chatSend');
|
|
86
|
+
|
|
87
|
+
const LANG_MAP = { tr: 'tr-TR', en: 'en-US', de: 'de-DE', fr: 'fr-FR', es: 'es-ES' };
|
|
88
|
+
const transcript = [];
|
|
89
|
+
let lang = 'en-US', ended = false, busy = false;
|
|
90
|
+
let recognition = null, wantListen = false;
|
|
91
|
+
let lastNotesMd = '', lastNotesHtml = '';
|
|
92
|
+
|
|
93
|
+
fetch('/config').then(r => r.json()).then(c => {
|
|
94
|
+
$('topic').textContent = c.topic || 'Untitled meeting';
|
|
95
|
+
lang = LANG_MAP[c.language] || c.language || 'en-US';
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
function setStatus(t) { status.textContent = t; }
|
|
99
|
+
function setOrb(cls) { orb.className = 'orb' + (cls ? ' ' + cls : ''); }
|
|
100
|
+
function esc(s){ return String(s).replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])); }
|
|
101
|
+
function addLine(kind, text) {
|
|
102
|
+
const cls = kind === 'user' ? 'u' : kind === 'ai' ? 'a' : 't';
|
|
103
|
+
const who = kind === 'user' ? 'You' : kind === 'ai' ? 'Catalyst' : '·';
|
|
104
|
+
const div = document.createElement('div');
|
|
105
|
+
div.className = cls; div.textContent = `${who}: ${text}`;
|
|
106
|
+
log.appendChild(div); log.scrollTop = log.scrollHeight;
|
|
107
|
+
if (kind === 'user' || kind === 'ai') transcript.push(`${who}: ${text}`);
|
|
108
|
+
}
|
|
109
|
+
function renderNotes(md) {
|
|
110
|
+
if (!md) return;
|
|
111
|
+
lastNotesMd = md;
|
|
112
|
+
lastNotesHtml = (window.marked && marked.parse) ? marked.parse(md) : `<pre>${esc(md)}</pre>`;
|
|
113
|
+
notes.innerHTML = lastNotesHtml;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function speak(text) {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
if (!('speechSynthesis' in window)) return resolve();
|
|
119
|
+
const u = new SpeechSynthesisUtterance(text);
|
|
120
|
+
u.lang = lang; u.onend = resolve; u.onerror = resolve;
|
|
121
|
+
setOrb('speaking'); setStatus('Catalyst is speaking…');
|
|
122
|
+
speechSynthesis.speak(u);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function handleTurn(text) {
|
|
127
|
+
if (!text || busy || ended) return;
|
|
128
|
+
busy = true; stopListening();
|
|
129
|
+
addLine('user', text);
|
|
130
|
+
setOrb('thinking'); setStatus('Thinking… (reading the codebase)');
|
|
131
|
+
let reply = '(no response)';
|
|
132
|
+
try {
|
|
133
|
+
const r = await fetch('/turn', {
|
|
134
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
135
|
+
body: JSON.stringify({ text }),
|
|
136
|
+
});
|
|
137
|
+
const data = await r.json();
|
|
138
|
+
reply = data.reply || reply;
|
|
139
|
+
renderNotes(data.notes);
|
|
140
|
+
} catch (e) { reply = 'Error: ' + (e?.message || e); }
|
|
141
|
+
addLine('ai', reply);
|
|
142
|
+
await speak(reply);
|
|
143
|
+
busy = false;
|
|
144
|
+
if (!ended) startListening();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildRecognition() {
|
|
148
|
+
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
149
|
+
if (!SR) return null;
|
|
150
|
+
const rec = new SR();
|
|
151
|
+
rec.lang = lang; rec.continuous = false; rec.interimResults = true;
|
|
152
|
+
rec.onresult = (e) => {
|
|
153
|
+
let finalText = '';
|
|
154
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
155
|
+
if (e.results[i].isFinal) finalText += e.results[i][0].transcript;
|
|
156
|
+
else setStatus('… ' + e.results[i][0].transcript);
|
|
157
|
+
}
|
|
158
|
+
if (finalText.trim()) handleTurn(finalText.trim());
|
|
159
|
+
};
|
|
160
|
+
rec.onend = () => { if (wantListen && !busy && !ended) { try { rec.start(); } catch {} } };
|
|
161
|
+
rec.onerror = (ev) => { if (ev.error === 'not-allowed') setStatus('Mic blocked — use the text box.'); };
|
|
162
|
+
return rec;
|
|
163
|
+
}
|
|
164
|
+
function startListening() {
|
|
165
|
+
wantListen = true; if (!recognition) return;
|
|
166
|
+
setOrb('listening'); setStatus('Listening…');
|
|
167
|
+
try { recognition.start(); } catch {}
|
|
168
|
+
}
|
|
169
|
+
function stopListening() { wantListen = false; if (recognition) { try { recognition.stop(); } catch {} } }
|
|
170
|
+
|
|
171
|
+
startBtn.onclick = () => {
|
|
172
|
+
startBtn.disabled = true; endBtn.disabled = false; chatRow.hidden = false;
|
|
173
|
+
recognition = buildRecognition();
|
|
174
|
+
if (!recognition) { setStatus('Speech needs Chrome. You can still type below.'); setOrb(''); return; }
|
|
175
|
+
startListening();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function sendChat() {
|
|
179
|
+
const t = chatInput.value.trim(); if (!t) return;
|
|
180
|
+
chatInput.value = ''; handleTurn(t);
|
|
181
|
+
}
|
|
182
|
+
chatSend.onclick = sendChat;
|
|
183
|
+
chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
|
|
184
|
+
|
|
185
|
+
endBtn.onclick = async () => {
|
|
186
|
+
ended = true; endBtn.disabled = true; stopListening();
|
|
187
|
+
if ('speechSynthesis' in window) speechSynthesis.cancel();
|
|
188
|
+
setOrb('thinking'); setStatus('Wrapping up…');
|
|
189
|
+
try {
|
|
190
|
+
const r = await fetch('/end', {
|
|
191
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify({ transcript: transcript.join('\n'), notesMd: lastNotesMd, notesHtml: lastNotesHtml }),
|
|
193
|
+
});
|
|
194
|
+
const j = await r.json();
|
|
195
|
+
document.querySelector('.room').innerHTML =
|
|
196
|
+
`<div class="done"><h1>Meeting captured ✓</h1>` +
|
|
197
|
+
`<p>Notes saved to:</p><p><code>${j.path}</code><br><code>${j.htmlPath || ''}</code></p>` +
|
|
198
|
+
`<p class="topic">Back in your terminal, shape it with<br>` +
|
|
199
|
+
`<code>/catalyze-spec @${j.path}</code></p>` +
|
|
200
|
+
`<p class="topic">You can close this tab.</p></div>`;
|
|
201
|
+
} catch (e) { setStatus('Failed to save: ' + (e?.message || e)); }
|
|
202
|
+
};
|
|
203
|
+
</script>
|
|
204
|
+
</body>
|
|
205
|
+
</html>
|
|
@@ -0,0 +1,198 @@
|
|
|
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" />
|
|
6
|
+
<title>Catalyst Meeting Room</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { --bg:#0e0e14; --panel:#1a1a24; --accent:#9b6dff; --text:#e8e8f0; --dim:#8a8a9a; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
11
|
+
background:var(--bg); color:var(--text); display:flex; min-height:100vh; }
|
|
12
|
+
.room { margin:auto; width:100%; max-width:680px; padding:32px; }
|
|
13
|
+
h1 { font-size:20px; font-weight:600; margin:0 0 4px; }
|
|
14
|
+
.topic { color:var(--dim); margin:0 0 28px; font-size:14px; }
|
|
15
|
+
.orb { width:120px; height:120px; border-radius:50%; margin:24px auto;
|
|
16
|
+
background:radial-gradient(circle at 35% 35%, #b89bff, var(--accent));
|
|
17
|
+
box-shadow:0 0 0 0 rgba(155,109,255,.5); transition:transform .2s; }
|
|
18
|
+
.orb.listening { animation:pulse 2s infinite; }
|
|
19
|
+
.orb.speaking { animation:pulse .7s infinite; transform:scale(1.08); }
|
|
20
|
+
.orb.thinking { background:radial-gradient(circle at 35% 35%,#ffd27a,#ff9b3d);
|
|
21
|
+
animation:spin 1.1s linear infinite; }
|
|
22
|
+
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(155,109,255,.5);} 70%{box-shadow:0 0 0 22px rgba(155,109,255,0);} 100%{box-shadow:0 0 0 0 rgba(155,109,255,0);} }
|
|
23
|
+
@keyframes spin { to { transform:rotate(360deg);} }
|
|
24
|
+
.status { text-align:center; color:var(--dim); min-height:22px; margin:8px 0 24px; font-size:14px; }
|
|
25
|
+
.btns { display:flex; gap:12px; justify-content:center; }
|
|
26
|
+
button { border:none; border-radius:10px; padding:12px 22px; font-size:15px;
|
|
27
|
+
font-weight:600; cursor:pointer; }
|
|
28
|
+
.start { background:var(--accent); color:#fff; }
|
|
29
|
+
.end { background:#2a2a36; color:var(--text); }
|
|
30
|
+
button:disabled { opacity:.4; cursor:not-allowed; }
|
|
31
|
+
.log { margin-top:28px; background:var(--panel); border-radius:12px; padding:16px;
|
|
32
|
+
max-height:34vh; overflow-y:auto; font-size:13px; line-height:1.6; }
|
|
33
|
+
.log .u { color:#7fd1ff; } .log .a { color:#c8b0ff; } .log .t { color:var(--dim); font-style:italic; }
|
|
34
|
+
.done { text-align:center; padding:20px; }
|
|
35
|
+
.done code { background:var(--panel); padding:4px 8px; border-radius:6px; }
|
|
36
|
+
.chat { display:flex; gap:8px; margin-top:16px; }
|
|
37
|
+
.chat input { flex:1; background:var(--panel); border:1px solid #2a2a36; color:var(--text);
|
|
38
|
+
border-radius:8px; padding:10px 12px; font-size:14px; }
|
|
39
|
+
.chat input:focus { outline:none; border-color:var(--accent); }
|
|
40
|
+
.chat button { background:#2a2a36; color:var(--text); }
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<div class="room">
|
|
45
|
+
<h1>Catalyst Meeting Room</h1>
|
|
46
|
+
<p class="topic" id="topic">Loading…</p>
|
|
47
|
+
<div class="orb" id="orb"></div>
|
|
48
|
+
<div class="status" id="status">Ready when you are.</div>
|
|
49
|
+
<div class="btns">
|
|
50
|
+
<button class="start" id="startBtn">Start meeting</button>
|
|
51
|
+
<button class="end" id="endBtn" disabled>End meeting</button>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="chat" id="chatRow" hidden>
|
|
54
|
+
<input id="chatInput" type="text" placeholder="Type a message instead of speaking…" autocomplete="off" />
|
|
55
|
+
<button id="chatSend">Send</button>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="log" id="log" hidden></div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<script type="module">
|
|
61
|
+
import { Conversation } from 'https://esm.sh/@elevenlabs/client';
|
|
62
|
+
|
|
63
|
+
const $ = (id) => document.getElementById(id);
|
|
64
|
+
const orb = $('orb'), status = $('status'), log = $('log');
|
|
65
|
+
const startBtn = $('startBtn'), endBtn = $('endBtn');
|
|
66
|
+
const chatRow = $('chatRow'), chatInput = $('chatInput'), chatSend = $('chatSend');
|
|
67
|
+
let conversation = null;
|
|
68
|
+
const transcript = [];
|
|
69
|
+
|
|
70
|
+
fetch('/config').then(r => r.json()).then(c => {
|
|
71
|
+
$('topic').textContent = c.topic || 'Untitled meeting';
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function setStatus(t) { status.textContent = t; }
|
|
75
|
+
function setOrb(cls) { orb.className = 'orb' + (cls ? ' ' + cls : ''); }
|
|
76
|
+
function addLine(kind, text) {
|
|
77
|
+
log.hidden = false;
|
|
78
|
+
const cls = kind === 'user' ? 'u' : kind === 'ai' ? 'a' : 't';
|
|
79
|
+
const who = kind === 'user' ? 'You' : kind === 'ai' ? 'Catalyst' : '·';
|
|
80
|
+
const div = document.createElement('div');
|
|
81
|
+
div.className = cls;
|
|
82
|
+
div.textContent = `${who}: ${text}`;
|
|
83
|
+
log.appendChild(div);
|
|
84
|
+
log.scrollTop = log.scrollHeight;
|
|
85
|
+
if (kind === 'user' || kind === 'ai') transcript.push(`${who}: ${text}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Client tool: live codebase investigation via the local daemon -> claude -p.
|
|
89
|
+
const clientTools = {
|
|
90
|
+
investigate_codebase: async ({ query }) => {
|
|
91
|
+
const prev = orb.className;
|
|
92
|
+
setOrb('thinking');
|
|
93
|
+
setStatus(`🔍 Investigating: ${query}`);
|
|
94
|
+
addLine('tool', `investigating: ${query}`);
|
|
95
|
+
try {
|
|
96
|
+
const r = await fetch('/tool', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ query }),
|
|
100
|
+
});
|
|
101
|
+
const j = await r.json();
|
|
102
|
+
addLine('tool', `findings ready`);
|
|
103
|
+
return j.result || 'No result.';
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return `Investigation failed: ${e.message}`;
|
|
106
|
+
} finally {
|
|
107
|
+
orb.className = prev;
|
|
108
|
+
setStatus('Listening…');
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
startBtn.onclick = async () => {
|
|
114
|
+
startBtn.disabled = true;
|
|
115
|
+
setStatus('Connecting…');
|
|
116
|
+
try {
|
|
117
|
+
const cfg = await (await fetch('/config')).json();
|
|
118
|
+
const signed = await (await fetch('/signed-url')).json();
|
|
119
|
+
const opts = {
|
|
120
|
+
clientTools,
|
|
121
|
+
onConnect: () => { setStatus('Listening…'); setOrb('listening'); endBtn.disabled = false; chatRow.hidden = false; },
|
|
122
|
+
onDisconnect: () => { setOrb(''); },
|
|
123
|
+
onError: (e) => { setStatus('Error: ' + (e?.message || e)); },
|
|
124
|
+
onModeChange: ({ mode }) => {
|
|
125
|
+
setOrb(mode === 'speaking' ? 'speaking' : 'listening');
|
|
126
|
+
setStatus(mode === 'speaking' ? 'Catalyst is speaking…' : 'Listening…');
|
|
127
|
+
},
|
|
128
|
+
onMessage: (payload) => {
|
|
129
|
+
// SDK versions differ: payload may be {message, source} or a bare string.
|
|
130
|
+
const raw = payload && typeof payload === 'object' ? payload.message ?? payload : payload;
|
|
131
|
+
const text = typeof raw === 'string' ? raw : (raw && raw.message) || JSON.stringify(raw);
|
|
132
|
+
const src = payload && payload.source;
|
|
133
|
+
if (text) addLine(src === 'user' ? 'user' : 'ai', text);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
if (signed.signedUrl) opts.signedUrl = signed.signedUrl;
|
|
137
|
+
else opts.agentId = cfg.agentId;
|
|
138
|
+
// Drive the conversation language from project-config (voice.language).
|
|
139
|
+
// Requires the language override to be enabled on the agent; if it isn't,
|
|
140
|
+
// ElevenLabs rejects the override, so we retry once without it.
|
|
141
|
+
if (cfg.language) opts.overrides = { agent: { language: cfg.language } };
|
|
142
|
+
try {
|
|
143
|
+
conversation = await Conversation.startSession(opts);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (opts.overrides) {
|
|
146
|
+
console.warn('Language override rejected (enable it on the agent). Retrying without it.', err);
|
|
147
|
+
delete opts.overrides;
|
|
148
|
+
conversation = await Conversation.startSession(opts);
|
|
149
|
+
} else {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
setStatus('Could not start: ' + (e?.message || e));
|
|
155
|
+
startBtn.disabled = false;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Typed messages — useful when speech is misheard. Treated as a user turn.
|
|
160
|
+
function sendChat() {
|
|
161
|
+
const t = chatInput.value.trim();
|
|
162
|
+
if (!t || !conversation) return;
|
|
163
|
+
try {
|
|
164
|
+
conversation.sendUserMessage(t);
|
|
165
|
+
addLine('user', t);
|
|
166
|
+
chatInput.value = '';
|
|
167
|
+
} catch (e) {
|
|
168
|
+
addLine('tool', 'could not send message: ' + (e?.message || e));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
chatSend.onclick = sendChat;
|
|
172
|
+
chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
|
|
173
|
+
|
|
174
|
+
endBtn.onclick = async () => {
|
|
175
|
+
endBtn.disabled = true;
|
|
176
|
+
setStatus('Wrapping up…');
|
|
177
|
+
setOrb('thinking');
|
|
178
|
+
try { if (conversation) await conversation.endSession(); } catch {}
|
|
179
|
+
try {
|
|
180
|
+
const r = await fetch('/end', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
183
|
+
body: JSON.stringify({ transcript: transcript.join('\n') }),
|
|
184
|
+
});
|
|
185
|
+
const j = await r.json();
|
|
186
|
+
document.querySelector('.room').innerHTML =
|
|
187
|
+
`<div class="done"><h1>Meeting captured ✓</h1>` +
|
|
188
|
+
`<p>Artifact written to:</p><p><code>${j.path}</code></p>` +
|
|
189
|
+
`<p class="topic">Back in your terminal, shape it with<br>` +
|
|
190
|
+
`<code>/catalyze-spec @${j.path}</code></p>` +
|
|
191
|
+
`<p class="topic">You can close this tab.</p></div>`;
|
|
192
|
+
} catch (e) {
|
|
193
|
+
setStatus('Failed to save: ' + (e?.message || e));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
</script>
|
|
197
|
+
</body>
|
|
198
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "catalyst-os-voice",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Local voice-meeting runtime for /meet-spec. Zero npm dependencies — Node stdlib only; the browser loads the ElevenLabs SDK from a CDN.",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "meet-server.js",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18.0.0"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -6,8 +6,9 @@ description: >
|
|
|
6
6
|
- Implementation is complete and needs quality checks
|
|
7
7
|
- Production readiness needs to be verified
|
|
8
8
|
|
|
9
|
-
This agent orchestrates Guardians (Enforcer, Sentinel, Inquisitor, Watcher
|
|
10
|
-
to run tests, check code quality, scan for
|
|
9
|
+
This agent orchestrates Guardians (Enforcer, Sentinel, Inquisitor, Watcher,
|
|
10
|
+
and Curator for database specs) to run tests, check code quality, scan for
|
|
11
|
+
security issues, verify schema integrity, and verify compliance.
|
|
11
12
|
|
|
12
13
|
DO NOT validate implementations yourself - delegate to Arbiter.
|
|
13
14
|
model: opus
|
|
@@ -56,6 +57,7 @@ Before any action, load `.claude/skills/using-skills/SKILL.md` and check which s
|
|
|
56
57
|
4. **Coverage**: Meets coverage thresholds
|
|
57
58
|
5. **Performance**: No regressions detected
|
|
58
59
|
6. **Documentation**: Required docs are present
|
|
60
|
+
7. **Schema Integrity** (database specs only): Live DB matches the spec — delegate to Curator (read-only), never to the Alchemist that built it
|
|
59
61
|
|
|
60
62
|
## Output
|
|
61
63
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: curator
|
|
3
|
+
description: >
|
|
4
|
+
PROACTIVELY DELEGATE schema-integrity audits to this agent. MUST BE USED when:
|
|
5
|
+
- Verifying a built implementation matches the actual database schema
|
|
6
|
+
- Confirming columns, constraints, and foreign keys exist in the real DB
|
|
7
|
+
- Tracing that API endpoints persist every field the spec expects
|
|
8
|
+
- Validating SQL functions, sequences, or triggers the feature depends on
|
|
9
|
+
|
|
10
|
+
This is a READ-ONLY Guardian. It audits the database — it never creates or
|
|
11
|
+
modifies schemas, migrations, or data. Building is the Alchemist's job.
|
|
12
|
+
|
|
13
|
+
DO NOT audit database integrity yourself - delegate to Curator.
|
|
14
|
+
model: sonnet
|
|
15
|
+
color: cyan
|
|
16
|
+
skills: secret-scanning
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
You are the Curator, a schema-integrity auditor who verifies that the
|
|
20
|
+
implementation matches the real database.
|
|
21
|
+
|
|
22
|
+
## Opening
|
|
23
|
+
|
|
24
|
+
*"Reconciling the schema against reality..."*
|
|
25
|
+
|
|
26
|
+
## Role
|
|
27
|
+
|
|
28
|
+
You are a **Guardian** in the `/audit-spec` validation phase, orchestrated by the
|
|
29
|
+
Arbiter. You audit — you do not build. The Alchemist designs and migrates the
|
|
30
|
+
schema during `/forge-spec`; you independently verify that what was built is real
|
|
31
|
+
and complete. This separation is the point: the agent that creates a table must
|
|
32
|
+
never be the one that signs off on it.
|
|
33
|
+
|
|
34
|
+
## Read-Only Discipline — BLOCKING
|
|
35
|
+
|
|
36
|
+
- **NEVER** run migrations, `ALTER`/`CREATE`/`DROP`, or any write/DDL statement.
|
|
37
|
+
- **NEVER** edit migration files, schema definitions, or application code.
|
|
38
|
+
- Use read-only queries only (`SELECT`, `information_schema`, `pg_catalog`, describe).
|
|
39
|
+
- If you discover a defect, you **report** it — you do not fix it. Remediation
|
|
40
|
+
goes back to the Alchemist via `/forge-spec`.
|
|
41
|
+
|
|
42
|
+
## Audit Checklist (only for specs touching the database)
|
|
43
|
+
|
|
44
|
+
1. **Tables exist**: Every table the spec/code references exists in the live DB.
|
|
45
|
+
2. **Columns match**: Real column names and types match what the spec and code
|
|
46
|
+
assume — check against `information_schema.columns`, not code aliases.
|
|
47
|
+
3. **Constraints & keys**: All foreign keys, unique constraints, NOT NULL, and
|
|
48
|
+
check constraints the spec requires actually exist.
|
|
49
|
+
4. **End-to-end persistence**: Trace each field the spec expects through the API
|
|
50
|
+
path (API call → DB write → DB read → API response). Flag any dropped field.
|
|
51
|
+
5. **Functions & procedures**: Any SQL functions/stored procedures the feature
|
|
52
|
+
calls actually exist with the expected signature.
|
|
53
|
+
6. **DB-level machinery**: Sequences, triggers, and DB-level defaults the feature
|
|
54
|
+
assumes are present.
|
|
55
|
+
|
|
56
|
+
## Verification Before Reporting
|
|
57
|
+
|
|
58
|
+
Follow `.claude/skills/verification-before-completion/SKILL.md`. Every claim must
|
|
59
|
+
be backed by the actual query output — paste it. "The column exists" is not
|
|
60
|
+
evidence; the `information_schema` row is.
|
|
61
|
+
|
|
62
|
+
## Output
|
|
63
|
+
|
|
64
|
+
Provide a schema-integrity report to the Arbiter:
|
|
65
|
+
|
|
66
|
+
- Overall status: PASS / FAIL
|
|
67
|
+
- Tables / columns verified (with query evidence)
|
|
68
|
+
- Constraints and foreign keys verified
|
|
69
|
+
- API end-to-end trace result per expected field
|
|
70
|
+
- DB functions / sequences / triggers verified
|
|
71
|
+
- Issues found, by severity, each with a remediation note pointing back to
|
|
72
|
+
`/forge-spec` (Alchemist)
|
|
73
|
+
|
|
74
|
+
Block on any mismatch between what the spec expects and what the database
|
|
75
|
+
actually contains.
|
|
@@ -28,6 +28,6 @@ echo "=== FILES CHANGED ===" && git diff --name-only main...HEAD 2>/dev/null ||
|
|
|
28
28
|
|
|
29
29
|
**Invoke skill:** `spec-validation`
|
|
30
30
|
|
|
31
|
-
**Orchestrator:** Arbiter (delegates to Enforcer, Sentinel, Inquisitor, Watcher)
|
|
31
|
+
**Orchestrator:** Arbiter (delegates to Enforcer, Sentinel, Inquisitor, Watcher, and Curator for database specs)
|
|
32
32
|
|
|
33
33
|
**Process skills used:** `verification-before-completion`, `agent-delegation`, `test-driven-development`
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# /meet-spec
|
|
2
|
+
|
|
3
|
+
Hold a **voice meeting** with Catalyst, then turn the notes into a spec.
|
|
4
|
+
|
|
5
|
+
Start a local browser meeting room. Talk through a feature; the meeting agent can
|
|
6
|
+
investigate the codebase, docs, and web live (via `claude -p` → Seer/Scout). When you
|
|
7
|
+
click **End meeting**, a structured, `/catalyze-spec`-ready notes artifact is written.
|
|
8
|
+
|
|
9
|
+
Default mode is **diy** — free, browser speech + local Claude, no API key. Switch
|
|
10
|
+
`voice.mode` to `bundled` for ElevenLabs' nicer (paid) voice.
|
|
11
|
+
|
|
12
|
+
> **Lifecycle position:** **`/meet-spec`** → `/catalyze-spec` → *(optional)* `/challenge-spec` → `/forge-spec` → `/audit-spec` → `/seal-spec`
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/meet-spec "realtime notifications"
|
|
18
|
+
/meet-spec # will ask for the meeting topic
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The output is a meeting artifact, NOT a spec. After the meeting, shape it yourself:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
/catalyze-spec @.catalyst/meetings/YYYY-MM-DD-{slug}/meeting.md
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Pre-computed Context
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
echo "=== NODE VERSION ===" && node --version
|
|
31
|
+
echo "=== CLAUDE CLI? ===" && command -v claude >/dev/null && echo "claude found" || echo "MISSING claude CLI (needed for the brain)"
|
|
32
|
+
echo "=== VOICE CONFIG ===" && sed -n '/^voice:/,/^[^ ]/p' .catalyst/main/project-config.yaml 2>/dev/null || echo "No voice config"
|
|
33
|
+
echo "=== VOICE RUNTIME? ===" && test -f .catalyst/voice/meet-server.js && echo "meet-server.js found" || echo "MISSING .catalyst/voice/ (re-install catalyst-os)"
|
|
34
|
+
echo "=== SECRETS (bundled mode only) ===" && test -f .catalyst/secrets.local.yaml && echo "secrets.local.yaml found" || echo "no secrets.local.yaml (only needed for bundled mode)"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
**Invoke skill:** `meet-spec`
|
|
40
|
+
|
|
41
|
+
**Orchestrator:** Catalyst (the meeting persona lives in the ElevenLabs agent prompt; codebase investigation delegates to Seer/Scout via headless `claude -p`)
|
|
42
|
+
|
|
43
|
+
**Process skills used:** `brainstorming`, `agent-delegation`
|
|
@@ -18,3 +18,19 @@ Commit, archive, and propagate learnings from a **fully built and audited** impl
|
|
|
18
18
|
**Invoke skill:** `spec-approval`
|
|
19
19
|
|
|
20
20
|
**Process skills used:** `verification-before-completion`, `agent-delegation`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Execution Mode
|
|
25
|
+
|
|
26
|
+
`/seal-spec` is the user's explicit authorization to run the **full ceremony end-to-end**:
|
|
27
|
+
|
|
28
|
+
> commit → archive → push → **create PR** → self-document
|
|
29
|
+
|
|
30
|
+
Do **not** stop and ask for confirmation between phases. In particular, after `git push` succeeds, immediately run `gh pr create` in the same turn. The only valid reasons to halt are:
|
|
31
|
+
|
|
32
|
+
- A hard gate from Phase 1 fails (TDD missing, validation not passed, handoff missing)
|
|
33
|
+
- A `git` or `gh` command returns an error
|
|
34
|
+
- The working tree contains unexpected uncommitted state
|
|
35
|
+
|
|
36
|
+
If everything is clean, the entire flow runs to completion and reports the PR URL at the end.
|