@inetafrica/open-claudia 3.1.8 → 3.1.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.10 — `say` synthesizes in the bot process, not the stripped subprocess
4
+
5
+ - v3.1.9 carried this exact fix but its build never published: the version pin in `test-provider-language.js` still read `3.1.8`, so the guard failed and the docker stage was skipped. Re-cut as v3.1.10 per "never retag a failed version."
6
+ - **Root cause of the persistent "no voice" reports.** `open-claudia say` synthesized the audio *inside the CLI*, which runs in the credential-stripped provider subprocess — with no `AGENTSPACE_POD_TOKEN`. `pocketToVoice` had no speech bearer and returned null, so hosted pods told users they couldn't send voice (the rahil-coders and kazee-people reports), even though the parent bot process — which *does* hold the token — evaluated `canSpeak() === true` and advertised "Voice notes: enabled." The capability check and the actual send were running on opposite sides of the credential boundary.
7
+ - **Fix.** Synthesis moves into the credentialed bot process behind a new loopback `say` JSON kind. The CLI now POSTs only text; `handleJson` runs `textToVoice` in-process, delivers via `adapter.sendVoice`, and cleans up the temp ogg afterward. A restart alone won't adopt this — it ships in the image and every pod is rolled onto it.
8
+ - **Egress guardrail preserved.** The old `say` inherited the R8 external-contact voice-egress default-deny from `handleSend`; the new in-process handler re-applies the same `relationship.guardActive` check, so `say` cannot become a guard bypass to non-owner contacts. Returns 400 (empty text) / 403 (guarded target) / 503 (no TTS route) / 500 (synthesis or delivery failure).
9
+ - Regression coverage in `test-say-loopback.js`: success (synthesis + delivery + temp cleanup), empty text, no-route, delivery failure, the R8 guard, and loopback auth.
10
+
3
11
  ## v3.1.8 — Hosted pods can actually send voice notes
4
12
 
5
13
  - **FFMPEG is discovered on PATH.** Provisioned pods ship `/usr/bin/ffmpeg` but never set `FFMPEG=` in config, and `config.js` read only the explicit setting — so `FFMPEG` resolved empty and every TTS route (`pocketToVoice`, ElevenLabs, `say`) silently returned null: voice input got text-only replies. `FFMPEG` now falls back to PATH discovery via the existing `executableCandidate` helper; an explicit `FFMPEG=` still wins.
package/bin/cli.js CHANGED
@@ -284,13 +284,17 @@ switch (command) {
284
284
  process.exit(2);
285
285
  }
286
286
  (async () => {
287
- const { textToVoice } = require(path.join(botDir, "core", "media"));
288
- const voicePath = await textToVoice(text);
289
- if (!voicePath) {
290
- console.error("No TTS route available — configure the speech gateway, ElevenLabs, or run on macOS.");
287
+ // Synthesis happens in the credentialed bot process, not here: this CLI
288
+ // runs in the credential-stripped provider subprocess with no speech
289
+ // token. Send only text through loopback; the bot process makes the audio.
290
+ const { postJson } = require("./loopback-client");
291
+ try {
292
+ await postJson("say", { text });
293
+ process.exit(0);
294
+ } catch (e) {
295
+ console.error(e.message);
291
296
  process.exit(1);
292
297
  }
293
- sendViaLoopback("send-voice", [voicePath]);
294
298
  })();
295
299
  break;
296
300
  }
package/core/actions.js CHANGED
@@ -416,6 +416,27 @@ async function handleAction(envelope) {
416
416
  await send(`Tooling mode: ${m}${m === "relaxed" ? " ⚠️ deny-gate is log-only; ad-hoc scripting allowed" : " — tool-first enforced"}`);
417
417
  return;
418
418
  }
419
+ if (d.startsWith("vc:")) {
420
+ if (!isChatOwner(envelope.channelId)) return send("Owner only — the voice is shared across this bot.");
421
+ const ttsVoice = require("./tts-voice");
422
+ const rest = d.slice(3);
423
+ if (rest === "clone") {
424
+ state.pendingVoiceClone = { at: Date.now() };
425
+ return send("🎙️ Send me a voice note now — 5–15s of clear speech and I'll clone it as your voice. Send /voice to cancel.");
426
+ }
427
+ state.pendingVoiceClone = null;
428
+ if (rest === "default") {
429
+ ttsVoice.clearVoice();
430
+ return send(`🎙️ Voice reset to default (${ttsVoice.getVoiceLabel()}).`);
431
+ }
432
+ if (rest.startsWith("p:")) {
433
+ const preset = ttsVoice.presetByKey(rest.slice(2));
434
+ if (!preset) return send("That voice button is stale. Open /voice again.");
435
+ ttsVoice.setVoice(preset.id, preset.label);
436
+ return send(`🎙️ Voice set: ${preset.label}`);
437
+ }
438
+ return;
439
+ }
419
440
  if (d.startsWith("apr:")) {
420
441
  // Approval buttons. Owner-only: the exact payload was shown in the prompt;
421
442
  // decide() is idempotent so double-taps can't flip it. Two shapes share
package/core/handlers.js CHANGED
@@ -1322,6 +1322,49 @@ register({
1322
1322
  },
1323
1323
  });
1324
1324
 
1325
+ register({
1326
+ name: "voice", description: "Pick the Pocket TTS voice, or clone your own", args: "[<preset>|<hf://…>|clone|default]", ownerOnly: true,
1327
+ handler: async (env, { tail }) => {
1328
+ if (!authorized(env)) return;
1329
+ const ttsVoice = require("./tts-voice");
1330
+ const state = currentState();
1331
+ const arg = (tail || "").trim();
1332
+ if (arg) {
1333
+ const low = arg.toLowerCase();
1334
+ if (low === "clone") {
1335
+ state.pendingVoiceClone = { at: Date.now() };
1336
+ return send("🎙️ Send me a voice note now — 5–15s of clear speech and I'll clone it as your voice. Send /voice to cancel.");
1337
+ }
1338
+ state.pendingVoiceClone = null;
1339
+ if (low === "default" || low === "reset") {
1340
+ ttsVoice.clearVoice();
1341
+ return send(`🎙️ Voice reset to default (${ttsVoice.getVoiceLabel()}).`);
1342
+ }
1343
+ const preset = ttsVoice.presetByKey(low);
1344
+ if (preset) {
1345
+ ttsVoice.setVoice(preset.id, preset.label);
1346
+ return send(`🎙️ Voice set: ${preset.label}`);
1347
+ }
1348
+ // Anything else is treated as a raw voice reference pocket-tts understands:
1349
+ // a built-in shorthand, an hf:// path, an http(s) URL, or a stored id.
1350
+ ttsVoice.setVoice(arg, null);
1351
+ return send(`🎙️ Voice set: ${arg}`);
1352
+ }
1353
+ state.pendingVoiceClone = null;
1354
+ const rows = ttsVoice.presets().map((p) => [{ text: p.label, callback_data: `vc:p:${p.key}` }]);
1355
+ rows.push([
1356
+ { text: "🎙️ Clone from a clip", callback_data: "vc:clone" },
1357
+ { text: "↩︎ Default", callback_data: "vc:default" },
1358
+ ]);
1359
+ send(
1360
+ `🎙️ Voice: ${ttsVoice.getVoiceLabel()}\n\n` +
1361
+ "Pick a voice — it takes effect immediately, no restart. Or tap Clone from a clip (or send /voice clone) to use your own recorded voice.\n\n" +
1362
+ "For any other voice: /voice hf://kyutai/tts-voices/<folder>/<clip>.wav",
1363
+ { keyboard: { inline_keyboard: rows } },
1364
+ );
1365
+ },
1366
+ });
1367
+
1325
1368
  register({
1326
1369
  name: "budget", description: "Set max spend for next task", args: "[$N]",
1327
1370
  handler: async (env, { tail }) => {
package/core/loopback.js CHANGED
@@ -82,6 +82,7 @@ const JSON_KINDS = new Set([
82
82
  "relay-send", "recent-fetch", "audit-tail",
83
83
  "auth-list", "auth-revoke",
84
84
  "approval-request",
85
+ "say",
85
86
  ]);
86
87
 
87
88
  function callerIsOwner(payload) {
@@ -161,6 +162,44 @@ async function handleJson(req, res, url, kind) {
161
162
  const adapter = registry.findAdapter(adapterId);
162
163
  if (!adapter) return reply(res, 400, { error: `unknown adapter ${adapterId}` });
163
164
 
165
+ if (kind === "say") {
166
+ const text = String(payload.text || "").trim();
167
+ if (!text) return reply(res, 400, { error: "missing text" });
168
+
169
+ // Synthesized voice is file egress, so apply the same R8 default-deny as
170
+ // handleSend: refuse it to a guarded (external, non-owner) target. Owner /
171
+ // CLI / cron paths are unguarded, so this is a strict no-op for them.
172
+ try {
173
+ const relationship = require("./relationship");
174
+ const speaker = relationship.speakerFor(adapter.type, channelId, userId);
175
+ if (relationship.guardActive(speaker)) {
176
+ return reply(res, 403, { error: "voice egress to an external contact is blocked by the guardrail — ask the owner to send it" });
177
+ }
178
+ } catch (e) { /* classification failure → fall through; owner-scoped in practice */ }
179
+
180
+ // Synthesis MUST happen here (the credentialed bot process). The CLI runs in
181
+ // the credential-stripped provider subprocess with no speech token, so it
182
+ // sends only text and this handler produces the audio.
183
+ let voicePath = null;
184
+ try {
185
+ const { textToVoice } = require("./media");
186
+ voicePath = await textToVoice(text);
187
+ } catch (e) {
188
+ return reply(res, 500, { error: `synthesis failed: ${e.message}` });
189
+ }
190
+ if (!voicePath) return reply(res, 503, { error: "no TTS route available" });
191
+
192
+ let ok = false;
193
+ try {
194
+ ok = await adapter.sendVoice(channelId, voicePath);
195
+ } catch (e) {
196
+ try { fs.unlinkSync(voicePath); } catch (_) {}
197
+ return reply(res, 500, { error: `voice send failed: ${e.message}` });
198
+ }
199
+ try { fs.unlinkSync(voicePath); } catch (e) {}
200
+ return reply(res, ok ? 200 : 500, { ok });
201
+ }
202
+
164
203
  if (kind === "schedule-wakeup") {
165
204
  const fireAt = Number(payload.fireAt);
166
205
  if (!Number.isFinite(fireAt) || fireAt <= 0) return reply(res, 400, { error: "missing/invalid fireAt" });
package/core/media.js CHANGED
@@ -4,7 +4,8 @@
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
6
  const { execSync } = require("child_process");
7
- const { WHISPER_CLI, WHISPER_MODEL, FFMPEG, TEMP_DIR, ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL, SPEECH_SERVICE_URL, SPEECH_API_KEY, AGENTSPACE_POD_TOKEN, POCKET_TTS_VOICE, TTS_PROVIDER } = require("./config");
7
+ const { WHISPER_CLI, WHISPER_MODEL, FFMPEG, TEMP_DIR, ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL, SPEECH_SERVICE_URL, SPEECH_API_KEY, AGENTSPACE_POD_TOKEN, TTS_PROVIDER } = require("./config");
8
+ const ttsVoice = require("./tts-voice");
8
9
 
9
10
  const TTS_CMD = process.platform === "darwin" ? "say" : null;
10
11
 
@@ -108,7 +109,7 @@ async function pocketToVoice(clean) {
108
109
  const res = await fetch(`${SPEECH_SERVICE_URL.replace(/\/$/, "")}/v1/audio/speech`, {
109
110
  method: "POST",
110
111
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
111
- body: JSON.stringify({ input: clean, voice: POCKET_TTS_VOICE, response_format: "wav" }),
112
+ body: JSON.stringify({ input: clean, voice: ttsVoice.getVoice(), response_format: "wav" }),
112
113
  signal: AbortSignal.timeout(60000),
113
114
  });
114
115
  if (!res.ok) {
@@ -129,6 +130,52 @@ async function pocketToVoice(clean) {
129
130
  }
130
131
  }
131
132
 
133
+ // Upload a reference clip to the speech gateway for voice cloning. The gateway
134
+ // stores the clip on the TTS pod's model-cache volume and returns a voice id
135
+ // usable as the `voice` field on later /v1/audio/speech calls; pocket-tts clones
136
+ // from it. Returns the id string, or null on any failure.
137
+ async function cloneVoiceFromClip(clipPath) {
138
+ const token = speechBearerToken();
139
+ if (!SPEECH_SERVICE_URL || !token || !clipPath) return null;
140
+ // Telegram voice notes arrive as ogg/opus, which pocket-tts can't decode as a
141
+ // reference prompt — transcode to 24kHz mono wav (the model's native rate)
142
+ // first. Fall back to the raw clip only if ffmpeg is unavailable.
143
+ let wavPath = null;
144
+ let uploadPath = clipPath;
145
+ if (FFMPEG) {
146
+ try {
147
+ wavPath = path.join(TEMP_DIR, `voice-clone-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.wav`);
148
+ execSync(`"${FFMPEG}" -i "${clipPath}" -ar 24000 -ac 1 -y "${wavPath}" 2>/dev/null`, { timeout: 30000 });
149
+ uploadPath = wavPath;
150
+ } catch (e) {
151
+ console.error("Voice clone transcode error:", e.message);
152
+ wavPath = null;
153
+ uploadPath = clipPath;
154
+ }
155
+ }
156
+ try {
157
+ const body = new FormData();
158
+ body.append("file", new Blob([fs.readFileSync(uploadPath)]), path.basename(uploadPath));
159
+ const res = await fetch(`${SPEECH_SERVICE_URL.replace(/\/$/, "")}/v1/voices`, {
160
+ method: "POST",
161
+ headers: { Authorization: `Bearer ${token}` },
162
+ body,
163
+ signal: AbortSignal.timeout(120000),
164
+ });
165
+ if (!res.ok) {
166
+ console.error(`Voice clone upload failed: HTTP ${res.status}`);
167
+ return null;
168
+ }
169
+ const payload = await res.json().catch(() => ({}));
170
+ return String(payload.voice_id || payload.id || "").trim() || null;
171
+ } catch (e) {
172
+ console.error("Voice clone error:", e.message);
173
+ return null;
174
+ } finally {
175
+ if (wavPath) { try { fs.unlinkSync(wavPath); } catch (e) {} }
176
+ }
177
+ }
178
+
132
179
  // True when at least one textToVoice route can produce audio. Mirrors the
133
180
  // pocket -> ElevenLabs -> say fallback chain; local Whisper is deliberately
134
181
  // irrelevant here (hosted pods speak via the gateway without it).
@@ -203,4 +250,4 @@ async function synthSentenceMp3(text) {
203
250
  }
204
251
  }
205
252
 
206
- module.exports = { transcribeAudio, transcribeAudioLocal, textToVoice, splitSentences, synthSentenceMp3, pocketToVoice, speechBearerToken, canSpeak, TTS_CMD };
253
+ module.exports = { transcribeAudio, transcribeAudioLocal, textToVoice, splitSentences, synthSentenceMp3, pocketToVoice, cloneVoiceFromClip, speechBearerToken, canSpeak, TTS_CMD };
package/core/router.js CHANGED
@@ -13,7 +13,7 @@ const introFlow = require("./intro-flow");
13
13
  const { send, deleteMessage } = require("./io");
14
14
  const { currentState, saveState, autoLinkOwnerChannel } = require("./state");
15
15
  const { admitRunContext, runAgent } = require("./runner");
16
- const { transcribeAudio } = require("./media");
16
+ const { transcribeAudio, cloneVoiceFromClip } = require("./media");
17
17
  const { vault } = require("./vault-store");
18
18
  const { isOnboarded, handleOnboarding } = require("./onboarding");
19
19
  const {
@@ -185,6 +185,13 @@ async function handleVoice(envelope) {
185
185
  if (media.size && media.size > MAX_VOICE_SIZE) {
186
186
  return send(`Voice note too large (${Math.round(media.size / 1024 / 1024)}MB). Max: ${MAX_VOICE_SIZE / 1024 / 1024}MB`);
187
187
  }
188
+ // /voice clone armed this channel: the next clip is a cloning sample, not a
189
+ // prompt. Consume the flag (fresh only) before the normal transcribe path.
190
+ if (state.pendingVoiceClone) {
191
+ const fresh = Date.now() - state.pendingVoiceClone.at < 5 * 60 * 1000;
192
+ state.pendingVoiceClone = null;
193
+ if (fresh) return captureVoiceClone(envelope, media);
194
+ }
188
195
  const runContext = admitRunContext("", state.currentSession.dir, envelope.messageId, {
189
196
  purpose: "foreground",
190
197
  attachments: [{ type: "voice", mimeType: media.mimeType || "audio/ogg", fileName: media.fileName || null }],
@@ -216,6 +223,11 @@ async function handleAudio(envelope) {
216
223
  try {
217
224
  const media = envelope.media?.[0];
218
225
  if (!media) return;
226
+ if (state.pendingVoiceClone) {
227
+ const fresh = Date.now() - state.pendingVoiceClone.at < 5 * 60 * 1000;
228
+ state.pendingVoiceClone = null;
229
+ if (fresh) return captureVoiceClone(envelope, media);
230
+ }
219
231
  const runContext = admitRunContext("", state.currentSession.dir, envelope.messageId, {
220
232
  purpose: "foreground",
221
233
  attachments: [{ type: "audio", mimeType: media.mimeType || null, fileName: media.fileName || null }],
@@ -233,6 +245,26 @@ async function handleAudio(envelope) {
233
245
  } catch (err) { await send(`Audio failed: ${err.message}`); }
234
246
  }
235
247
 
248
+ // A clip the owner sent right after /voice clone. Upload it to the speech
249
+ // gateway (which stores it on the TTS pod and returns a voice id), then set that
250
+ // id as the active voice. Never transcribed or sent to the agent.
251
+ async function captureVoiceClone(envelope, media) {
252
+ try {
253
+ envelope.adapter.typing(envelope.channelId).catch(() => {});
254
+ await send("🎙️ Got it — cloning your voice…");
255
+ const clipPath = await envelope.adapter.downloadMedia(media);
256
+ const voiceId = await cloneVoiceFromClip(clipPath);
257
+ try { fs.unlinkSync(clipPath); } catch (e) {}
258
+ if (!voiceId) {
259
+ return send("Couldn't process that clip for cloning. Send a clear 5–15s recording and try /voice clone again.");
260
+ }
261
+ require("./tts-voice").setVoice(voiceId, "your cloned voice");
262
+ return send("✅ Cloned. I'll speak in that voice from now on — change it anytime with /voice.");
263
+ } catch (err) {
264
+ await send(`Voice clone failed: ${err.message}`);
265
+ }
266
+ }
267
+
236
268
  async function handlePhoto(envelope) {
237
269
  if (isDuplicate(envelope)) return;
238
270
  const state = currentState();
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ // Live-read, persisted Pocket TTS voice selection.
4
+ //
5
+ // The active voice overrides the POCKET_TTS_VOICE env default and is read at
6
+ // synth time (media.js), so /voice takes effect immediately with no restart.
7
+ // The choice survives restarts via a small JSON file in the bot config dir;
8
+ // env stays the deploy-time default/floor and is used only when no override is
9
+ // set. A cloned-clip voice is just another voice string (a server-issued id).
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+ const { CONFIG_DIR, POCKET_TTS_VOICE } = require("./config");
14
+ const { atomicWriteFileSync } = require("./fsutil");
15
+
16
+ const STORE = path.join(CONFIG_DIR, "tts-voice.json");
17
+
18
+ // Curated shortlist for the /voice picker. `id` is exactly what pocket-tts's
19
+ // get_state_for_audio_prompt resolves: the built-in "alba" shorthand or an
20
+ // hf:// reference into the kyutai/tts-voices repo. Every path here was verified
21
+ // against that repo's file tree, so the buttons always resolve.
22
+ const PRESETS = [
23
+ { key: "alba", label: "Alba (default)", id: "alba" },
24
+ { key: "announcer", label: "Alba · announcer", id: "hf://kyutai/tts-voices/alba-mackenna/announcer.wav" },
25
+ { key: "merchant", label: "Alba · merchant", id: "hf://kyutai/tts-voices/alba-mackenna/merchant.wav" },
26
+ { key: "expresso", label: "Expresso", id: "hf://kyutai/tts-voices/expresso/ex01-ex02_default_001_channel1_168s.wav" },
27
+ { key: "enunciated", label: "Expresso · clear", id: "hf://kyutai/tts-voices/expresso/ex01-ex02_enunciated_001_channel1_432s.wav" },
28
+ { key: "vctk-p225", label: "VCTK p225", id: "hf://kyutai/tts-voices/vctk/p225_023.wav" },
29
+ { key: "vctk-p226", label: "VCTK p226", id: "hf://kyutai/tts-voices/vctk/p226_023.wav" },
30
+ ];
31
+
32
+ // undefined = not loaded yet; null = loaded, no override (use env default);
33
+ // { voice, label } = an active override.
34
+ let cache;
35
+
36
+ function defaultVoice() {
37
+ return (POCKET_TTS_VOICE && String(POCKET_TTS_VOICE).trim()) || "alba";
38
+ }
39
+
40
+ function load() {
41
+ if (cache !== undefined) return cache;
42
+ try {
43
+ const raw = JSON.parse(fs.readFileSync(STORE, "utf-8"));
44
+ if (raw && typeof raw.voice === "string" && raw.voice.trim()) {
45
+ cache = { voice: raw.voice.trim(), label: typeof raw.label === "string" && raw.label ? raw.label : null };
46
+ return cache;
47
+ }
48
+ } catch (e) { /* no store yet, or malformed — fall back to default */ }
49
+ cache = null;
50
+ return cache;
51
+ }
52
+
53
+ // The active voice string handed to the speech gateway.
54
+ function getVoice() {
55
+ const s = load();
56
+ return (s && s.voice) || defaultVoice();
57
+ }
58
+
59
+ // Human-friendly label for the active voice (preset label, custom note, or id).
60
+ function getVoiceLabel() {
61
+ const s = load();
62
+ if (s) return s.label || s.voice;
63
+ const dv = defaultVoice();
64
+ const p = PRESETS.find((x) => x.id === dv || x.key === dv);
65
+ return p ? p.label : dv;
66
+ }
67
+
68
+ // True when the active voice is an explicit override (not the env default).
69
+ function isOverridden() {
70
+ return !!load();
71
+ }
72
+
73
+ function setVoice(voice, label = null) {
74
+ const v = String(voice || "").trim();
75
+ if (!v) throw new Error("voice is required");
76
+ cache = { voice: v, label: label ? String(label).trim() : null };
77
+ atomicWriteFileSync(STORE, JSON.stringify(cache, null, 2), { backup: false, mode: 0o600 });
78
+ return cache;
79
+ }
80
+
81
+ // Drop the override and fall back to the env/config default.
82
+ function clearVoice() {
83
+ cache = null;
84
+ try { fs.unlinkSync(STORE); } catch (e) { /* already absent */ }
85
+ }
86
+
87
+ function presets() { return PRESETS.slice(); }
88
+ function presetByKey(key) { return PRESETS.find((p) => p.key === String(key || "").toLowerCase()) || null; }
89
+
90
+ // Test seam: forget the in-memory cache so the next read re-consults disk.
91
+ function _resetCache() { cache = undefined; }
92
+
93
+ module.exports = {
94
+ getVoice, getVoiceLabel, isOverridden,
95
+ setVoice, clearVoice,
96
+ presets, presetByKey, defaultVoice,
97
+ STORE, _resetCache,
98
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.1.8",
3
+ "version": "3.1.12",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "setup": "node setup.js",
11
11
  "start": "node bot.js",
12
12
  "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js",
13
- "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js"
13
+ "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js"
14
14
  },
15
15
  "files": [
16
16
  "bot.js",
@@ -186,7 +186,25 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
186
186
  // Fix: FFMPEG PATH discovery in config, media.canSpeak() drives the prompt
187
187
  // flag, and a new `open-claudia say "<text>"` verb synthesizes + sends a
188
188
  // voice note on request.
189
- assert.strictEqual(pkg.version, "3.1.8", "release version must remain unchanged without explicit approval");
189
+ // 3.1.9 was cut for the say-loopback voice fix but never published: its build
190
+ // failed on this very version pin (still 3.1.8), so the docker stage was
191
+ // skipped and no image was built. Never retag a failed version — re-cut as +1.
192
+ // 3.1.10 approved by Sumeet 2026-07-21: re-cut of 3.1.9. The say-loopback fix —
193
+ // `say` synthesized audio inside the credential-stripped provider subprocess
194
+ // (no speech token), so hosted pods reported "no TTS route" while the parent
195
+ // bot process advertised voice as enabled. Synthesis moves into the credentialed
196
+ // bot process behind a loopback "say" kind; the CLI now sends only text; the R8
197
+ // external-contact egress guard is preserved. Plus this version-pin bump.
198
+ // 3.1.11 was cut for the /voice picker + voice cloning feature but its build
199
+ // failed on this very version pin (still 3.1.10): the package.json bump landed
200
+ // without the matching pin update, so the docker stage was skipped and no image
201
+ // was published. Never retag a failed version — re-cut as +1.
202
+ // 3.1.12 approved by Sumeet 2026-07-21: re-cut of 3.1.11. Owner-gated /voice
203
+ // preset picker over the Pocket TTS catalog (live-read, persists across restart,
204
+ // Custom hf:// escape hatch) plus voice cloning — send a clip and it's transcoded
205
+ // (ogg->24kHz mono wav) and registered via the speech gateway /v1/voices as the
206
+ // active stored:<digest> voice. Plus this version-pin bump.
207
+ assert.strictEqual(pkg.version, "3.1.12", "release version must remain unchanged without explicit approval");
190
208
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
191
209
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
192
210
  }