@inetafrica/open-claudia 3.1.20 → 3.1.22

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.22 — Group silence is no longer context-blind: engaged turns catch up from the cursor
4
+
5
+ - Follow-through on the v3.1.21 group gate, per the owner's direction: the bot must not reply in a group unless tagged (owner included) — "but should get messages since its last cursor."
6
+ - The kazee adapter now implements the router's `buildThreadContext` hook (the same contract Spaces uses): an engaged group turn opens with a context block of every message since the bot's last engaged turn — exactly the messages the participation gate stayed silent through — with sender attribution ("You" for its own), `[type] caption` rendering for media, deleted messages dropped, bodies clipped at 280 chars, at most 40 messages.
7
+ - The cursor is a persisted per-chat watermark (`kazee-state.json`, atomic writes) holding the newest `createdAt` already folded into a turn. The delta is server-derived (`GET /messages/:chatId/:botUserId?since=`, oldest-first, server-capped at 500), so missed socket events and restarts self-heal. A chat's first-ever engage pulls a recent 15-message page instead of the whole backlog.
8
+ - Best-effort: a fetch failure returns no block, leaves the watermark alone, and never delays the reply.
9
+ - New release-chain test `test-kazee-group-context.js`: cold-start window, warm since-delta, You-attribution, media/deletion/clipping rendering, trigger exclusion, trigger-only watermark advance, failure semantics, DM no-op, on-disk persistence.
10
+
11
+ ## v3.1.21 — Kazee public participation: owner seeding, group tag-gating, guarded external turns
12
+
13
+ - **Owner seeding for kazee-only pods.** A pod whose only owner signal is `KAZEE_OWNER_USER_ID` (empty `auth.json`) never got a people-store owner record, so every unknown inbound — group or DM — was refused with a false "no owner configured" (observed on network-mon, 2026-07-23). `seedOwnerFromLegacy` now falls back to the kazee env owner; legacy `auth.json` owners keep precedence and seeding stays idempotent.
14
+ - **Group chats engage only when addressed.** The kazee adapter fetches chat meta (5-min cache) and, in group chats, engages only on an @-mention of the bot or a reply to the bot's own message — matching chat-central's own `assistantAction` default `assistant_tagged_reply`. Chats set to `assistant_free_reply` engage on everything. Untagged group chatter is dropped silently (no roster prompts, no refusals — owner included). 1:1 chats are unchanged; a meta-fetch failure fails open to pre-gate behavior and is not cached.
15
+ - **Tagged externals get the guarded turn.** An engaged group message authorizes the turn via adapter-attested `envelope.engaged` (never derived from message content), but the speaker still classifies external: replies are vetted by the enforcer and escalate to the owner's approve-once / deny / make-a-rule card — the same pattern as Spaces. Slash commands stay closed to externals.
16
+ - **Escalation cards reach the kazee owner.** `ownerTarget` used to return env-seeded kazee owner handles as the raw user id, but kazee sends need a chat-document id, so approval cards would 404. User-id handles now resolve through `openDirectChat`, falling back to the configured owner target.
17
+ - New release-chain tests: `test-people-owner-seed.js` (env seed, legacy precedence, no-signal refusal) and `test-kazee-group-gate.js` (mention/reply engagement, free-reply, 1:1, meta failure, meta cache, engaged access pass-through).
18
+
3
19
  ## v3.1.17 — Call bridge joins through the caller's room-setup race (fixes "Call declined")
4
20
 
5
21
  - Answering a Kazee web call could fail deterministically fast: the caller creates the Janus videoroom during its own dial-time setup, and the bot answers quicker than any human — its `join` hit Janus before the room existed, got 426 "No such room", and the whole call was rejected as `connection_failed` (surfacing as "Call declined" in the web UI).
@@ -14,6 +14,14 @@ const { inlineKeyboardToPortable } = require("../types");
14
14
  const { createKazeeSocket } = require("./socket");
15
15
  const { normalize } = require("./format");
16
16
  const { KazeeCallManager } = require("./call/manager");
17
+ const kazeeState = require("./state");
18
+
19
+ // Group catch-up context sizing: how many messages an engaged turn may carry,
20
+ // how long each rendered body may be, and how far back a chat's very first
21
+ // engage looks (no watermark yet — recent window instead of the whole backlog).
22
+ const CONTEXT_MAX_MESSAGES = 40;
23
+ const CONTEXT_BODY_LIMIT = 280;
24
+ const COLD_START_WINDOW = 15;
17
25
 
18
26
  class KazeeAdapter {
19
27
  constructor({ id = "kazee", url, token, ownerUserId, botUserId }) {
@@ -28,9 +36,34 @@ class KazeeAdapter {
28
36
  this.botUserId = botUserId || null;
29
37
  this._listeners = { message: new Set(), action: new Set() };
30
38
  this._socket = null;
39
+ this._chatMetaCache = new Map();
31
40
  this._callManager = new KazeeCallManager({ adapter: this });
32
41
  }
33
42
 
43
+ // chat-central's chat doc carries the group flag and the per-chat assistant
44
+ // policy (assistant_tagged_reply is the platform default for groups). Cached
45
+ // briefly so the participation gate costs one REST fetch per chat, not per
46
+ // message. Failures are NOT cached: a transient error must not stick a chat
47
+ // open or closed.
48
+ async _chatMeta(chatId) {
49
+ const cached = this._chatMetaCache.get(chatId);
50
+ if (cached && Date.now() - cached.at < 5 * 60 * 1000) return cached.meta;
51
+ try {
52
+ const res = await this._request("GET", `/chats/${encodeURIComponent(chatId)}`);
53
+ const chat = res?.chat || res || {};
54
+ if (chat.error) throw new Error(String(chat.error));
55
+ const meta = {
56
+ isGroupChat: !!chat.isGroupChat,
57
+ assistantAction: String(chat.assistantAction || ""),
58
+ };
59
+ this._chatMetaCache.set(chatId, { meta, at: Date.now() });
60
+ return meta;
61
+ } catch (e) {
62
+ console.error(`Kazee chat meta fetch failed (${chatId}):`, e.message);
63
+ return null;
64
+ }
65
+ }
66
+
34
67
  on(event, fn) {
35
68
  if (!this._listeners[event]) return () => {};
36
69
  this._listeners[event].add(fn);
@@ -190,12 +223,18 @@ class KazeeAdapter {
190
223
  }
191
224
  }
192
225
 
193
- // Reply-to context: fetch the replied-to message content so the router
194
- // has the same context Telegram provides. Best-effort — failure is silent.
226
+ // Reply-to context: the socket payload may carry the full replied-to doc
227
+ // (chat-central V2) or just its id — resolve either into the same shape
228
+ // Telegram provides. Best-effort — failure is silent.
195
229
  if (msg.replyTo) {
196
230
  try {
197
- const data = await this._request("GET", `/message/${encodeURIComponent(String(msg.replyTo))}`);
198
- const m = data?.message || data;
231
+ let m = null;
232
+ if (typeof msg.replyTo === "object") {
233
+ m = msg.replyTo;
234
+ } else {
235
+ const data = await this._request("GET", `/message/${encodeURIComponent(String(msg.replyTo))}`);
236
+ m = data?.message || data;
237
+ }
199
238
  if (m && (m._id || m.id)) {
200
239
  envelope.reply = {
201
240
  text: m.content || "",
@@ -210,9 +249,109 @@ class KazeeAdapter {
210
249
  } catch (e) { /* best-effort — envelope is still valid without reply context */ }
211
250
  }
212
251
 
252
+ // Group participation gate: in a group chat the bot engages only when
253
+ // addressed — an @-mention of the bot user or a reply to one of its own
254
+ // messages — unless the chat's assistantAction opts into free replies.
255
+ // Everyone is gated, owner included: a bot answering every group message
256
+ // drowns the room. 1:1 chats are never gated. Engaged messages are marked
257
+ // so the router can authorize the turn for non-owner speakers, who then
258
+ // run guarded (mandate/guardrail vetting + owner escalation).
259
+ if (this.botUserId) {
260
+ const meta = await this._chatMeta(channelId);
261
+ if (meta && meta.isGroupChat) {
262
+ const botId = String(this.botUserId);
263
+ const mentions = (Array.isArray(msg.mentions) ? msg.mentions : [])
264
+ .map((m) => String(m?._id || m?.id || m));
265
+ const mentioned = mentions.includes(botId);
266
+ const replyToBot = String(envelope.reply?.from?.id || "") === botId;
267
+ const freeReply = meta.assistantAction === "assistant_free_reply";
268
+ if (!mentioned && !replyToBot && !freeReply) {
269
+ console.log(`Kazee group gate: silent (chat=${channelId} from=${userId})`);
270
+ return;
271
+ }
272
+ envelope.engaged = true;
273
+ envelope.surface = "group";
274
+ }
275
+ }
276
+
213
277
  this._emit("message", envelope);
214
278
  }
215
279
 
280
+ // Channel-agnostic thread-context provider (ChannelAdapter contract, same
281
+ // hook Spaces implements). The router calls this at the start of an engaged
282
+ // group turn and prepends the returned contextBlock to the prompt, so the
283
+ // bot arrives knowing everything said in the room since it last spoke — the
284
+ // messages the participation gate stayed silent through are caught up here,
285
+ // exactly once, via a persisted per-chat watermark. The delta is
286
+ // server-derived (GET /messages?since=), so a missed socket event or a
287
+ // restart self-heals: the next engage still pulls the true set. Best-effort:
288
+ // any failure returns null, leaves the watermark alone, and never blocks the
289
+ // reply.
290
+ async buildThreadContext(envelope) {
291
+ if (!envelope || envelope.surface !== "group" || !this.botUserId) return null;
292
+ const chatId = String(envelope.channelId || "");
293
+ if (!chatId) return null;
294
+ try {
295
+ const since = kazeeState.getContextWatermark(chatId);
296
+ const msgs = await this._messagesSince(chatId, since);
297
+ if (!msgs.length) return null;
298
+ const newest = msgs[msgs.length - 1]?.createdAt;
299
+ if (newest) kazeeState.setContextWatermark(chatId, newest);
300
+ const triggerId = String(envelope.messageId || "");
301
+ const botId = String(this.botUserId);
302
+ const rendered = msgs
303
+ .filter((m) => !m.isDeleted && String(m._id || m.id || "") !== triggerId)
304
+ .map((m) => {
305
+ const senderId = String(m.sender?._id || m.sender?.id || "");
306
+ const who = senderId === botId ? "You" : (m.sender?.name || "Someone");
307
+ let body = (typeof m.content === "string" ? m.content : "").trim();
308
+ if (m.type && m.type !== "text" && m.type !== "interactive") {
309
+ body = `[${m.type}]${body ? " " + body : ""}`;
310
+ }
311
+ if (body.length > CONTEXT_BODY_LIMIT) body = body.slice(0, CONTEXT_BODY_LIMIT).trimEnd() + " …";
312
+ return { who, body, ago: this._ago(m.createdAt) };
313
+ })
314
+ .filter((m) => m.body);
315
+ if (!rendered.length) return null;
316
+ const lines = ["[Kazee group chat context]"];
317
+ lines.push(since
318
+ ? "Messages in this group since your last engaged turn (oldest→newest):"
319
+ : "Recent messages in this group (oldest→newest):");
320
+ for (const m of rendered.slice(-CONTEXT_MAX_MESSAGES)) lines.push(` [${m.ago}] ${m.who}: ${m.body}`);
321
+ return { contextBlock: lines.join("\n") };
322
+ } catch (e) {
323
+ console.error("kazee buildThreadContext:", e.message);
324
+ return null;
325
+ }
326
+ }
327
+
328
+ // Message delta for a chat. With a watermark: chat-central's since-sync
329
+ // (oldest-first, server-capped at 500). Without one — the chat's first-ever
330
+ // engage — one recent page, which the server already returns in
331
+ // chronological order.
332
+ async _messagesSince(chatId, sinceIso) {
333
+ const base = `/messages/${encodeURIComponent(chatId)}/${encodeURIComponent(String(this.botUserId))}`;
334
+ const query = sinceIso
335
+ ? `?since=${encodeURIComponent(sinceIso)}`
336
+ : `?page=1&messageCount=${COLD_START_WINDOW}`;
337
+ const res = await this._request("GET", base + query);
338
+ const data = res?.data || res || {};
339
+ if (data.error) throw new Error(String(data.error));
340
+ return Array.isArray(data.messages) ? data.messages : [];
341
+ }
342
+
343
+ _ago(iso) {
344
+ const t = Date.parse(iso || 0);
345
+ if (!t) return "?";
346
+ const s = Math.max(0, Math.floor((Date.now() - t) / 1000));
347
+ if (s < 60) return `${s}s ago`;
348
+ const m = Math.floor(s / 60);
349
+ if (m < 60) return `${m}m ago`;
350
+ const h = Math.floor(m / 60);
351
+ if (h < 24) return `${h}h ago`;
352
+ return `${Math.floor(h / 24)}d ago`;
353
+ }
354
+
216
355
  _normalizeKeyboard(keyboard) {
217
356
  if (!keyboard) return null;
218
357
  if (keyboard.buttons) return keyboard.buttons;
@@ -0,0 +1,42 @@
1
+ // Persistent per-chat context watermark for the Kazee group participation
2
+ // model: the createdAt (ISO) of the newest message already folded into an
3
+ // engaged turn's context block. buildThreadContext reads it to compute the
4
+ // "messages since I last engaged" delta and advances it on every engage, so
5
+ // group chatter the gate stayed silent through is caught up exactly once.
6
+ // Parent-process only; small, atomic writes.
7
+
8
+ const path = require("path");
9
+ const { CONFIG_DIR } = require("../../core/config");
10
+ const { atomicWriteFileSync, readJsonWithFallback } = require("../../core/fsutil");
11
+
12
+ const STATE_FILE = path.join(CONFIG_DIR, "kazee-state.json");
13
+
14
+ let store = null;
15
+
16
+ function load() {
17
+ if (store) return store;
18
+ const raw = readJsonWithFallback(STATE_FILE, null) || {};
19
+ store = { chats: raw.chats && typeof raw.chats === "object" ? raw.chats : {} };
20
+ return store;
21
+ }
22
+
23
+ function save() {
24
+ try { atomicWriteFileSync(STATE_FILE, JSON.stringify(store, null, 2)); }
25
+ catch (e) { console.error("kazee state save:", e.message); }
26
+ }
27
+
28
+ function setContextWatermark(chatId, iso) {
29
+ const id = String(chatId || "");
30
+ if (!id || !iso) return;
31
+ const s = load();
32
+ s.chats[id] = { ...(s.chats[id] || {}), contextAt: String(iso) };
33
+ save();
34
+ }
35
+
36
+ function getContextWatermark(chatId) {
37
+ const s = load();
38
+ const c = s.chats[String(chatId || "")];
39
+ return (c && c.contextAt) || null;
40
+ }
41
+
42
+ module.exports = { setContextWatermark, getContextWatermark, STATE_FILE };
package/core/access.js CHANGED
@@ -48,10 +48,17 @@ function saveAuth(auth) {
48
48
  fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
49
49
  }
50
50
 
51
- function isChatAuthorized(chatId) {
51
+ function isChatAuthorized(chatId, envelope) {
52
52
  if (matchesTransportOwner()) return true;
53
53
  const id = String(chatId || "");
54
54
  if (!id) return false;
55
+ // Adapter-attested engagement (e.g. a kazee group @-mention of the bot):
56
+ // the platform semantics invited the bot into this exchange, so the turn
57
+ // proceeds — the speaker still classifies external and runs fully guarded
58
+ // (reply vetting, action enforcement, owner escalation), same as the Spaces
59
+ // participation rule below. The flag is set only by adapter code, never
60
+ // derived from message content.
61
+ if (envelope && envelope.engaged === true) return true;
55
62
  // Spaces task threads authorize by participation: the bot only receives a
56
63
  // Spaces notification for a task it follows/was mentioned on, so the inbound
57
64
  // itself is the authorization signal. Effective only while the conversational
package/core/people.js CHANGED
@@ -255,7 +255,16 @@ function seedOwnerFromLegacy() {
255
255
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
256
256
  legacyOwner = (auth.authorized || []).find((a) => a.isOwner === true) || null;
257
257
  } catch (e) {}
258
- if (!legacyOwner) return null;
258
+ if (!legacyOwner) {
259
+ // Pods configured with only KAZEE_OWNER_USER_ID (empty auth.json) DO have
260
+ // an owner — without a person record here, every unknown inbound was
261
+ // refused with a false "no owner configured" message.
262
+ const kazeeOnlyOwner = (config && config.KAZEE_OWNER_USER_ID) || "";
263
+ if (!kazeeOnlyOwner) return null;
264
+ const person = add({ name: "Owner", isOwner: true, bio: "Seeded from KAZEE_OWNER_USER_ID" });
265
+ try { linkHandle(person.id, { adapter: "kazee", channelId: String(kazeeOnlyOwner), approvedBy: "bootstrap" }); } catch (e) {}
266
+ return person;
267
+ }
259
268
 
260
269
  const adapter = "telegram";
261
270
  const channelId = String(legacyOwner.chatId);
@@ -161,10 +161,35 @@ async function ownerTarget() {
161
161
  String(h.channelId) === String(owner.primaryChannel.channelId))
162
162
  : null;
163
163
  const handle = primary || handles[0];
164
- if (handle) return { adapter: handle.adapter, channelId: String(handle.channelId), person: owner };
164
+ if (handle) {
165
+ const resolved = await resolveHandleSendTarget(handle);
166
+ if (resolved) return { ...resolved, person: owner };
167
+ }
165
168
  }
166
169
  } catch (e) { /* fall through to the env fallback */ }
167
170
  return configuredOwnerTarget();
168
171
  }
169
172
 
173
+ // Env/legacy-seeded kazee owner handles store the owner's USER id, but kazee
174
+ // sends address a chat-document id — posting buttons to the raw user id 404s
175
+ // and escalation silently dies. Resolve those handles to the 1:1 DM the same
176
+ // way configuredOwnerTarget does; every other handle is already a send target.
177
+ async function resolveHandleSendTarget(handle) {
178
+ const adapter = String(handle.adapter || "");
179
+ const channelId = String(handle.channelId || "");
180
+ if (adapter !== "kazee") return { adapter, channelId };
181
+ let kzOwner = "";
182
+ try { const cfg = require("./config"); kzOwner = String((cfg.config && cfg.config.KAZEE_OWNER_USER_ID) || ""); } catch (e) {}
183
+ if (!kzOwner || channelId !== kzOwner) return { adapter, channelId };
184
+ try {
185
+ const registry = require("./adapter-registry");
186
+ const kazee = (registry.getAdapters() || []).find((a) => a.type === "kazee");
187
+ if (kazee && typeof kazee.openDirectChat === "function") {
188
+ const chatId = await kazee.openDirectChat(channelId);
189
+ if (chatId) return { adapter, channelId: String(chatId) };
190
+ }
191
+ } catch (e) { /* outreach unavailable → let the env fallback decide */ }
192
+ return null;
193
+ }
194
+
170
195
  module.exports = { speakerFor, currentSpeaker, guardActive, isCurrentSpeakerGuarded, ownerTarget, configuredOwnerTarget };
package/core/router.js CHANGED
@@ -140,7 +140,7 @@ async function onMessage(envelope) {
140
140
  return;
141
141
  }
142
142
 
143
- if (!isChatAuthorized(envelope.channelId)) {
143
+ if (!isChatAuthorized(envelope.channelId, envelope)) {
144
144
  if (envelope.type === "text") {
145
145
  if (isDuplicate(envelope)) return;
146
146
  await introFlow.handleInbound(envelope, (text) => send(text));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.1.20",
3
+ "version": "3.1.22",
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": {
@@ -11,7 +11,7 @@
11
11
  "setup": "node setup.js",
12
12
  "start": "node bot.js",
13
13
  "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 && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
14
- "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
+ "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-kazee-group-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-kazee-group-context.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-people-owner-seed.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"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -251,7 +251,27 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
251
251
  // stop being estimates — plus the Janus keepalive ack fix (Janus answers
252
252
  // keepalive with ONLY an ack; acks now settle keepalive requests alone,
253
253
  // ending the bogus per-interval "keepalive timed out" log noise).
254
- assert.strictEqual(pkg.version, "3.1.20", "release version must remain unchanged without explicit approval");
254
+ // 3.1.21 approved by Sumeet 2026-07-23 ("Proceed as you see fit and fix in a
255
+ // production ready way work through till it's fully fixed you have my approval
256
+ // unless you need confirmation"): kazee public participation — pods whose only
257
+ // owner signal is KAZEE_OWNER_USER_ID now seed an owner person record (an
258
+ // empty auth.json used to turn every unknown inbound into a false "no owner
259
+ // configured" refusal — network-mon, 2026-07-23); kazee group chats honor
260
+ // chat-central's assistantAction (default tagged-reply: the bot engages only
261
+ // on @-mention or a reply to its own message; free-reply engages everything);
262
+ // engaged externals run the guarded external turn with the owner
263
+ // approve/deny/make-a-rule escalation, same pattern as Spaces; and
264
+ // ownerTarget resolves kazee user-id owner handles to a DM chat id via
265
+ // openDirectChat so escalation cards actually deliver.
266
+ // 3.1.22 approved by Sumeet 2026-07-23 ("It should not reply to owner messages
267
+ // also on a group unless tagged but should get messages since it's last
268
+ // cursor"): group catch-up context — an engaged kazee group turn now opens
269
+ // with every message since the bot's last engaged turn (persisted per-chat
270
+ // watermark; server-derived GET /messages?since= delta; a chat's first engage
271
+ // uses a recent-page window), rendered Spaces-style with sender attribution,
272
+ // so the chatter the gate stayed silent through is caught up exactly once:
273
+ // the gate keeps the bot reply-silent, never context-blind.
274
+ assert.strictEqual(pkg.version, "3.1.22", "release version must remain unchanged without explicit approval");
255
275
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
256
276
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
257
277
  }