@inetafrica/open-claudia 3.3.1 → 3.3.2

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.3.2 — Known people are recognized by transport id, approved owners stay owners, and a mandate can no longer land on the wrong note
4
+
5
+ - **Every known person got interrogated on a fresh Kazee DM.** The intro flow matched inbound chats only by `(adapter, channelId)` — but Kazee DMs key the envelope's channelId by the conversation *document* id while people handles store the sender's *user* id, so nothing ever matched: known teammates, and the owner on pods without `KAZEE_OWNER_USER_ID`, all got "who are you" (the Coders Africa incident, 2026-08-19/20). Now a sender whose transport user id already maps to a person is silently **adopted**: the DM channel is linked to their record, an auth entry is written, any stale pending intro is resolved, and `handleInbound` returns false so the router falls through and handles the turn normally. Adoption is gated on adapter-attested `surface: "dm"` — the Kazee adapter sets it only on a positive chat-meta read (`isGroupChat: false`); a failed meta read attests nothing (fail closed) — because auth.json standing is per-CHANNEL and adopting a room would hand every member the sender's standing. Display names are never matched; only the transport id, the same trust level as the env owner match.
6
+ - **Approving an owner onto a second channel demoted them there.** `applyApproval` hardcoded `isOwner: false` into the auth entry while `isChatOwner` reads exactly that file. The entry now mirrors `person.isOwner`, and a stale `false` entry from the bug era is upgraded in place on the next approval.
7
+ - **The mandate writer can no longer be steered by a display name.** `appendMandateLine`'s legacy fallback upserted by `personName` when the record carried no slug — a spoofable transport display name could land owner-authored security text on a same-named OTHER person's note (reachable from telegram-group externals, where there is no person record). Writes are now strictly slug-keyed: `entitySlug`, else the person REGISTRY (`personId` → `resolveEntitySlug`), else **refused** — never the name. The router's rule-capture path passes `personId` so per-person rules keep landing after the fallback's removal.
8
+ - **Stranded grants are surfaced the moment they bite.** The dual-file trap — a mandate granted on the name-obvious note (`rahil-malde.md`) while the enforcer reads the person's hashed record slug — made grants silently do nothing. Escalation cards now carry a ⚠️ hint naming the file holding the dead grant and pointing at Verify / Make-a-rule, whenever the speaker's live mandate is empty but a same-named sibling note (that no other person owns) holds one. Detection only: the stranded text is never applied and never quoted.
9
+ - **Router plumbing for the fall-through:** the cross-channel dedup check is memoized per envelope object, so the adopted first message isn't swallowed by `handleText`'s re-check while true redeliveries (fresh envelope, same message id) still dedup globally.
10
+ - Regression coverage: `test-intro-adoption.js` joins the chain (adoption unit + through-the-router integration, fail-closed attestation, approval owner-mirror and in-place repair); `test-mandate-hygiene.js` pins the name-fallback refusal and personId resolution; `test-kazee-group-gate.js` pins the dm attestation and its fail-closed absence; `test-enforcer.js` pins the stranded-mandate hint and that the stranded text never reaches the live mandate.
11
+
3
12
  ## v3.3.1 — Message length can no longer kill the voice: chunked pocket TTS (re-cut carrying unpublished v3.3.0)
4
13
 
5
14
  - **Long voice notes died at 60 seconds of synthesis.** `pocketToVoice` sent the ENTIRE cleaned text to the speech gateway as one request under `AbortSignal.timeout(60000)`. A long reply blew that window, the pocket route returned null, and on pods the fallback chain has nowhere to go (no ElevenLabs key, no macOS `say`) — so `say` bounced with "no TTS route available" and agents improvised manual per-paragraph chunking (sales-bot, 2026-08-19). Long text is now sentence-packed into ≤450-char chunks (`chunkForTTS`; run-on sentences hard-split at word boundaries so nothing can recreate the timeout), each chunk synthesized in its own 120s window with one retry (a transient gateway blip no longer wastes the chunks already done), and the wavs concatenated via ffmpeg's concat demuxer into ONE ogg/opus. Short text keeps the exact single-request behavior. Benefits every consumer of `textToVoice`/`textToVoiceFile`: `say`, `tts`, and whole voice-note replies; the voice channel's streamed per-sentence path is untouched. Regression pinned in `test-speech-loopback.js`: multiple gateway calls each inside the pack limit, same voice across chunks, lossless reassembly, temp cleanup, and chunker unit coverage.
@@ -301,6 +301,11 @@ class KazeeAdapter {
301
301
  }
302
302
  envelope.engaged = true;
303
303
  envelope.surface = "group";
304
+ } else if (meta) {
305
+ // Adapter-attested 1:1 conversation — required before intro-flow may
306
+ // auto-adopt the chat for a known sender. Set only on a positive meta
307
+ // read: an unresolvable chat stays unattested (fail closed).
308
+ envelope.surface = "dm";
304
309
  }
305
310
  }
306
311
 
package/core/actions.js CHANGED
@@ -89,7 +89,21 @@ function appendMandate(rec, line) {
89
89
  function appendMandateLine(rec, lineText, log, opts) {
90
90
  const entities = require("./entities");
91
91
  const mandates = require("./mandates");
92
- const ent = rec.entitySlug ? entities.readEntity(rec.entitySlug) : null;
92
+ // Strictly slug-keyed: resolve through the record's entitySlug, else through
93
+ // the person record by id. NEVER name-keyed — a display name is spoofable,
94
+ // and a name-keyed upsert can land security text on a same-named OTHER
95
+ // person's note (the exact escalation the hashed auto-contact slugs exist
96
+ // to prevent).
97
+ let slug = String(rec.entitySlug || "").trim();
98
+ if (!slug && rec.personId) {
99
+ try {
100
+ const people = require("./people");
101
+ const person = people.findById(String(rec.personId));
102
+ slug = String((person && people.resolveEntitySlug(person)) || "").trim();
103
+ } catch (e) { slug = ""; }
104
+ }
105
+ if (!slug) return { ok: false, reason: "no linked contact record to hold the grant" };
106
+ const ent = entities.readEntity(slug);
93
107
  const name = (ent && ent.name) || rec.personName;
94
108
  if (!name) return { ok: false, reason: "no contact record" };
95
109
  const coreText = mandates.splitExpiry(String(lineText || "")).text;
@@ -121,14 +135,12 @@ function appendMandateLine(rec, lineText, log, opts) {
121
135
  return { ok: false, reason: "their mandate is full — trim it with /guardrails" };
122
136
  }
123
137
  const up = entities.upsertEntity({
124
- slug: (ent && ent.slug) || rec.entitySlug || undefined,
138
+ slug,
125
139
  name,
126
140
  relationship: (ent && ent.relationship) || "external",
127
141
  mandate,
128
142
  log,
129
143
  });
130
- // id is slug-scoped, so derive it from the entity the write actually landed
131
- // on (legacy name-keyed upserts can resolve to a pre-existing slug).
132
144
  const res = { ok: true, id: mandates.lineId(up.entity.slug, coreText), expiresAt: expiresAt || null };
133
145
  if (refreshed) res.refreshed = true;
134
146
  return res;
package/core/enforcer.js CHANGED
@@ -336,7 +336,7 @@ function esc(s) {
336
336
  // Framing: the owner is deciding whether the bot should ACT on a request from
337
337
  // this person — the card leads with WHO is asking WHAT, then what the bot
338
338
  // proposes to do about it, never with the bot's output as the headline.
339
- async function postOwnerButtons(rec, owner, { kind, summary, reason }) {
339
+ async function postOwnerButtons(rec, owner, { kind, summary, reason, strandedMandate = "" }) {
340
340
  const adapter = adapterFor(owner.adapter);
341
341
  if (!adapter) return false;
342
342
  const who = rec.personName || "An external contact";
@@ -380,6 +380,7 @@ async function postOwnerButtons(rec, owner, { kind, summary, reason }) {
380
380
  } catch (e) { /* card still posts without the verify affordance */ }
381
381
  const lines = [head, ``, ...askBlock, ...body,
382
382
  unverifiedLine ? `\n${unverifiedLine}` : ``,
383
+ strandedMandate ? `\n⚠️ ${esc(strandedMandate)}` : ``,
383
384
  reason ? `\nWhy I'm checking: ${esc(reason)}` : ``];
384
385
  const yes = kind === "triage" ? "✅ Yes, handle it" : "✅ Approve once";
385
386
  const no = kind === "triage" ? "⛔ No, decline" : "⛔ Deny";
@@ -450,6 +451,33 @@ async function escalate({
450
451
  channelLabel = String((await originAdapter.channelLabel(String(speaker.channelId))) || "").trim();
451
452
  }
452
453
  } catch (e) {}
454
+ // Dual-file trap detection: a mandate granted on the name-obvious note while
455
+ // the enforcer reads this person's (hashed) record slug silently does
456
+ // nothing. Detect it at the moment it bites — the escalation the owner
457
+ // thought a grant had already prevented — and say where the grant actually
458
+ // lives. Detection only: the stranded text is NEVER applied (names are
459
+ // spoofable; only the owner links the two, via verify).
460
+ let strandedMandate = "";
461
+ try {
462
+ if (speaker && speaker.person && speaker.slug && !String(speaker.mandate || "").trim()) {
463
+ const entities = require("./entities");
464
+ const sibling = entities.slugify(speaker.person.name || "");
465
+ if (sibling && sibling !== speaker.slug) {
466
+ const other = entities.readEntity(sibling);
467
+ const text = other && other.type === "person"
468
+ ? require("./mandates").activeMandateText(String((other.sections && other.sections.Mandate) || "")).trim()
469
+ : "";
470
+ if (text) {
471
+ const people = require("./people");
472
+ const holder = people.list().find((p) => p.id !== speaker.person.id && people.resolveEntitySlug(p) === sibling);
473
+ if (!holder) {
474
+ strandedMandate = `A mandate exists on ${sibling}.md, but ${speaker.person.name}'s record reads ${speaker.slug}.md — those grants have NO effect. Merge them with the Verify button (or /people verify), or re-add via Make a rule.`;
475
+ if (audit) audit.log("enforcer.stranded-mandate", { personId: speaker.person.id, slug: speaker.slug, sibling });
476
+ }
477
+ }
478
+ }
479
+ }
480
+ } catch (e) { strandedMandate = ""; }
453
481
  const rec = approvals.create({
454
482
  tool: kind === "reply" ? "external-reply" : kind === "triage" ? "external-triage" : "external-action",
455
483
  verb: "",
@@ -480,7 +508,7 @@ async function escalate({
480
508
  inboundText,
481
509
  channelLabel,
482
510
  });
483
- const posted = await postOwnerButtons(rec, owner, { kind, summary, reason });
511
+ const posted = await postOwnerButtons(rec, owner, { kind, summary, reason, strandedMandate });
484
512
  if (!posted) {
485
513
  approvals.decide(rec.id, "denied", "owner-unreachable");
486
514
  if (audit) audit.log("enforcer.escalate.failed", { reason: "could not post to owner", id: rec.id });
@@ -98,11 +98,80 @@ async function notifyOwnerAboutIntro(intro) {
98
98
  }
99
99
  }
100
100
 
101
+ // Legacy auth.json entry for a channel, created — or upgraded in place — so
102
+ // isChatAuthorized/isChatOwner agree with the people record. isOwner flows
103
+ // from people.isOwner: dropping it here is what demoted owners approved onto
104
+ // a second channel (isChatOwner reads this file, not the person).
105
+ function ensureAuthEntry(channelId, { name = null, username = null, isOwner = false } = {}) {
106
+ const auth = loadAuth();
107
+ const id = String(channelId);
108
+ const existing = auth.authorized.find((a) => String(a.chatId) === id);
109
+ if (existing) {
110
+ if (isOwner && existing.isOwner !== true) {
111
+ existing.isOwner = true;
112
+ saveAuth(auth);
113
+ }
114
+ return auth;
115
+ }
116
+ auth.authorized.push({
117
+ chatId: id,
118
+ name: name || "",
119
+ username: username || null,
120
+ isOwner: !!isOwner,
121
+ authorizedAt: new Date().toISOString(),
122
+ });
123
+ saveAuth(auth);
124
+ return auth;
125
+ }
126
+
127
+ // A sender whose transport user id already maps to a person is never
128
+ // interrogated: the id is adapter-attested (the same trust level as the
129
+ // KAZEE_OWNER_USER_ID owner match) — unlike display names, which are
130
+ // spoofable and are never matched. Only 1:1 conversations are adopted: the
131
+ // adapter must attest surface "dm", because auth.json and isChatOwner are
132
+ // per-CHANNEL, so adopting a multi-party room would hand every member the
133
+ // sender's standing. No attestation, no adoption (fail closed).
134
+ function adoptKnownSender(envelope, adapter, channelId) {
135
+ if (envelope.surface !== "dm") return null;
136
+ const senderId = envelope.userId != null ? String(envelope.userId) : "";
137
+ if (!senderId || senderId === channelId) return null;
138
+ const person = people.findByHandle(adapter, senderId);
139
+ if (!person) return null;
140
+ try {
141
+ people.linkHandle(person.id, {
142
+ adapter, channelId,
143
+ displayName: displayNameFromEnvelope(envelope),
144
+ approvedBy: "auto:transport-id",
145
+ });
146
+ } catch (e) {
147
+ return null; // handle conflict — leave the channel to the normal intro flow
148
+ }
149
+ ensureAuthEntry(channelId, {
150
+ name: person.name || displayNameFromEnvelope(envelope),
151
+ username: usernameFromEnvelope(envelope),
152
+ isOwner: person.isOwner === true,
153
+ });
154
+ const pending = intros.activeForChannel(adapter, channelId);
155
+ if (pending) intros.remove(pending.id);
156
+ audit.log("intro.auto-adopted", {
157
+ personId: person.id, adapter, channelId, senderId,
158
+ isOwner: person.isOwner === true,
159
+ resolvedIntroId: pending ? pending.id : null,
160
+ });
161
+ return person;
162
+ }
163
+
164
+ // Returns true when the intro flow consumed the message (the router must not
165
+ // process it further); false ONLY when the sender was recognised by transport
166
+ // id and the channel auto-adopted — the router then handles the turn normally.
101
167
  async function handleInbound(envelope, sendFn) {
102
168
  intros.sweep();
103
169
  const adapter = envAdapterType(envelope);
104
170
  const channelId = String(envelope.channelId);
105
- if (!adapter) return false;
171
+ if (!adapter) return true;
172
+
173
+ const known = adoptKnownSender(envelope, adapter, channelId);
174
+ if (known) return false;
106
175
 
107
176
  let intro = intros.activeForChannel(adapter, channelId);
108
177
  const text = String(envelope.text || "").trim();
@@ -200,7 +269,7 @@ async function handleInbound(envelope, sendFn) {
200
269
  return true;
201
270
  }
202
271
 
203
- return false;
272
+ return true;
204
273
  }
205
274
 
206
275
  async function applyApproval(intro, by) {
@@ -227,20 +296,16 @@ async function applyApproval(intro, by) {
227
296
  } else {
228
297
  throw new Error("intro has no claim — owner approved a half-finished intro");
229
298
  }
230
- // Also write into auth.json so legacy isChatAuthorized + isChatOwner still work.
231
- const auth = loadAuth();
232
- if (!auth.authorized.some((a) => String(a.chatId) === String(intro.channelId))) {
233
- auth.authorized.push({
234
- chatId: String(intro.channelId),
235
- name: intro.displayName || person.name,
236
- username: intro.username || null,
237
- isOwner: false,
238
- authorizedAt: new Date().toISOString(),
239
- });
240
- saveAuth(auth);
241
- if (intro.adapter === "telegram") {
242
- try { updateAuthorizedChatEnv(auth); } catch (e) {}
243
- }
299
+ // Also write into auth.json so legacy isChatAuthorized + isChatOwner still
300
+ // work. isOwner mirrors the person record: an owner approved onto a new
301
+ // channel keeps owner status there (hardcoding false demoted Kazee owners).
302
+ const auth = ensureAuthEntry(intro.channelId, {
303
+ name: intro.displayName || person.name,
304
+ username: intro.username,
305
+ isOwner: person.isOwner === true,
306
+ });
307
+ if (intro.adapter === "telegram") {
308
+ try { updateAuthorizedChatEnv(auth); } catch (e) {}
244
309
  }
245
310
  intros.approve(intro.id, by);
246
311
  audit.log("intro.approved", { introId: intro.id, personId: person.id, by });
package/core/router.js CHANGED
@@ -63,15 +63,24 @@ async function deliverRouterError(error) {
63
63
  // person is still caught once the owner's channels are unified.
64
64
  const processedMessages = new Set();
65
65
  function isDuplicate(envelope) {
66
+ // Memoized per envelope object: the unauthorized path checks dedup before
67
+ // intro handling, then falls through to the normal handlers (which check
68
+ // again) when a known sender's chat was auto-adopted — that second check
69
+ // must not mistake the envelope's own first check for a redelivery.
70
+ if (envelope._dedupSeen !== undefined) return envelope._dedupSeen;
66
71
  const canonical = envelope.canonicalUserId || envelope.channelId;
67
72
  const key = `${canonical}:${envelope.adapter?.id || envelope.adapter?.type}:${envelope.messageId}`;
68
- if (processedMessages.has(key)) return true;
73
+ if (processedMessages.has(key)) {
74
+ envelope._dedupSeen = true;
75
+ return true;
76
+ }
69
77
  processedMessages.add(key);
70
78
  if (processedMessages.size > 400) {
71
79
  const arr = [...processedMessages];
72
80
  processedMessages.clear();
73
81
  arr.slice(-200).forEach((k) => processedMessages.add(k));
74
82
  }
83
+ envelope._dedupSeen = false;
75
84
  return false;
76
85
  }
77
86
 
@@ -141,11 +150,12 @@ async function onMessage(envelope) {
141
150
  }
142
151
 
143
152
  if (!isChatAuthorized(envelope.channelId, envelope)) {
144
- if (envelope.type === "text") {
145
- if (isDuplicate(envelope)) return;
146
- await introFlow.handleInbound(envelope, (text) => send(text));
147
- }
148
- return;
153
+ if (envelope.type !== "text") return;
154
+ if (isDuplicate(envelope)) return;
155
+ const consumed = await introFlow.handleInbound(envelope, (text) => send(text));
156
+ if (consumed) return;
157
+ // false = a transport-verified known sender was auto-adopted and the
158
+ // channel is authorized now — fall through and handle the turn.
149
159
  }
150
160
 
151
161
  // Auto-contact: an authorized speaker the people store doesn't know yet
@@ -604,7 +614,7 @@ async function handleText(envelope) {
604
614
  // ("... for 14d" / "... until 2026-08-30").
605
615
  const { text: coreRule, expiresAt } = require("./mandates").extractExpiry(ruleText);
606
616
  const r = require("./actions").appendMandateLine(
607
- { entitySlug: pend.entitySlug, personName: pend.personName },
617
+ { entitySlug: pend.entitySlug, personId: pend.personId, personName: pend.personName },
608
618
  coreRule,
609
619
  "owner added a per-person rule via approval card",
610
620
  { expiresAt },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
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-socket-reconnect.js && node test-speech-loopback.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-mandate-hygiene.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-external-rule-scopes.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-cards.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-typing-heartbeat.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 && 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-union.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-subst-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-section-budgets.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-dedup-adjudicator.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-prospective.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-regressions.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-cost-report.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-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-approval-mining.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-certification.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-apprenticeship.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-attest.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-verify-contact.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-loopback-owner-gate.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-mandate-hygiene.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-external-rule-scopes.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-cards.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-typing-heartbeat.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 && 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-union.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-subst-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-section-budgets.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-dedup-adjudicator.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-prospective.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-regressions.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-cost-report.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-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-approval-mining.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-certification.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-apprenticeship.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-attest.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-verify-contact.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-loopback-owner-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-intro-adoption.js"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
package/test-enforcer.js CHANGED
@@ -378,5 +378,36 @@ const extSpeaker = relationship.speakerFor("telegram", CH_EXT);
378
378
  await runInChat({ ...grantCtx, triageApprovalId: escReply.id }, () => enforcer.vetReply(extSpeaker, "reply-kind-grant-payload"));
379
379
  assert.ok(!/TURN GRANT/.test(grantPrompt), "only a triage-kind approval grants the turn");
380
380
 
381
+ // ── stranded-mandate hint (dual-file trap): a grant written to the
382
+ // name-obvious note while the speaker's record reads a hashed slug does
383
+ // nothing — the card must SAY so at the moment it bites, and the stranded
384
+ // text must never be applied or quoted. ──
385
+ const rahil = people.add({ name: "Rahil Malde" });
386
+ people.setEntitySlug(rahil.id, "rahil-malde-abc123");
387
+ people.linkHandle(rahil.id, { adapter: "telegram", channelId: "333" });
388
+ entities.upsertEntity({ slug: "rahil-malde-abc123", name: "Rahil Malde", type: "person", relationship: "external" });
389
+ entities.upsertEntity({ slug: "rahil-malde", name: "Rahil Malde", type: "person", relationship: "external",
390
+ mandate: "- May coordinate ticket triage." });
391
+ const strandedSpeaker = relationship.speakerFor("telegram", "333");
392
+ assert.strictEqual(String(strandedSpeaker.mandate || "").trim(), "",
393
+ "the stranded text never reaches the live mandate");
394
+ sends.length = 0;
395
+ const escStr = await enforcer.escalate({ speaker: strandedSpeaker, kind: "reply", payloadText: "p", summary: "s", reason: "r" });
396
+ assert.strictEqual(escStr.escalated, true, "stranded-mandate escalation still posts");
397
+ const strCard = sends.find((s) => s.channelId === CH_OWNER);
398
+ assert.ok(strCard && /rahil-malde\.md/.test(strCard.text) && /NO effect/.test(strCard.text),
399
+ "the owner card names the file holding the dead grant");
400
+ assert.ok(!/ticket triage/.test(strCard.text), "the stranded text itself stays off the card");
401
+
402
+ // A different person legitimately holding the name slug silences the hint —
403
+ // that mandate is THEIRS, not a stranded copy of this speaker's.
404
+ const rahil2 = people.add({ name: "Rahil M Other" });
405
+ people.setEntitySlug(rahil2.id, "rahil-malde");
406
+ sends.length = 0;
407
+ await enforcer.escalate({ speaker: relationship.speakerFor("telegram", "333"), kind: "reply", payloadText: "p", summary: "s", reason: "r" });
408
+ const strCard2 = sends.find((s) => s.channelId === CH_OWNER);
409
+ assert.ok(strCard2 && !/NO effect/.test(strCard2.text),
410
+ "no hint when the sibling note belongs to a real other person");
411
+
381
412
  console.log("enforcer OK");
382
413
  })().catch((e) => { console.error(e); process.exit(1); });
@@ -63,13 +63,31 @@ assert.strictEqual(entities.readEntity("busy-contact").sections.Mandate, nearFul
63
63
  // ── no record: nothing to write to → refused with a reason ──
64
64
  const r4 = appendMandate({ entitySlug: "", personName: "" }, "anything");
65
65
  assert.strictEqual(r4.ok, false, "no contact record → refused");
66
- assert.ok(/no contact record/.test(r4.reason));
66
+ assert.ok(/no linked contact record/.test(r4.reason));
67
67
 
68
- // ── legacy fallback: a record with only a personName still lands (by name) ──
68
+ // ── name-only records are REFUSED, never name-keyed: a display name is
69
+ // spoofable, and a name-keyed upsert could land security text on a same-named
70
+ // OTHER person's note (v3.3.2 — this replaced the legacy fallback). ──
69
71
  const r5 = appendMandate({ entitySlug: "", personName: "Legacy Larry" }, "asking build status");
70
- assert.strictEqual(r5.ok, true, "name-only record still writes");
71
- assert.ok(/build status/.test((entities.readEntity("legacy-larry") || { sections: {} }).sections.Mandate || ""),
72
- "fallback upserts the slugified-name entity");
72
+ assert.strictEqual(r5.ok, false, "name-only record is refused");
73
+ assert.ok(/no linked contact record/.test(r5.reason));
74
+ assert.strictEqual(entities.readEntity("legacy-larry"), null,
75
+ "no entity is ever minted from a bare display name");
76
+
77
+ // ── personId resolution: a record without an entitySlug resolves through the
78
+ // person REGISTRY (id → entitySlug), never through the spoofable name ──
79
+ const people = require("./core/people");
80
+ const harry = people.add({ name: "Hash Harry" });
81
+ people.setEntitySlug(harry.id, "hash-harry-9f9f9f");
82
+ entities.upsertEntity({ slug: "hash-harry-9f9f9f", name: "Hash Harry", relationship: "external" });
83
+ const r6 = appendMandate({ personId: harry.id, personName: "Spoofed Name" }, "asking build status");
84
+ assert.strictEqual(r6.ok, true, "personId-only record resolves via the registry");
85
+ assert.ok(/build status/.test(entities.readEntity("hash-harry-9f9f9f").sections.Mandate || ""),
86
+ "the grant lands on the person's registered slug");
87
+ assert.strictEqual(entities.readEntity("spoofed-name"), null,
88
+ "the transport display name never picks the target");
89
+ const r7 = appendMandate({ personId: "missing-person-id", personName: "Ghost" }, "anything");
90
+ assert.strictEqual(r7.ok, false, "an unresolvable personId is refused, not name-keyed");
73
91
 
74
92
  // ── P3: capability-scoped grants — expiry, refresh, ids, revoke ──
75
93
  const mandates = require("./core/mandates");
@@ -379,7 +379,17 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
379
379
  // say) voiceless. Carries the unpublished 3.3.0 content plus the Kazee calls
380
380
  // batch (trickle ICE + streaming TTS playback, ringing-on-invite/deferred
381
381
  // publish, werift ICE rejection suppression). Plus this pin repair.
382
- assert.strictEqual(pkg.version, "3.3.1", "release version must remain unchanged without explicit approval");
382
+ // 3.3.2 approved by Sumeet 2026-08-20 ("Okay, I'll go with your
383
+ // recommendations. Can you make sure you fix everything ... push everything
384
+ // out once the new versions have landed for the dockers update the entire
385
+ // fleet"): the identity/guardrail batch behind the Coders Africa incident —
386
+ // transport-verified sender adoption (a known transport user id in an
387
+ // adapter-attested DM is never interrogated; the router falls through and
388
+ // handles the adopted turn), applyApproval mirrors person.isOwner instead of
389
+ // demoting approved owners, and the mandate writer is strictly slug-keyed
390
+ // (name-keyed fallback removed) with stranded dual-file grants surfaced on
391
+ // the escalation card.
392
+ assert.strictEqual(pkg.version, "3.3.2", "release version must remain unchanged without explicit approval");
383
393
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
384
394
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
385
395
  }