@inetafrica/open-claudia 3.2.1 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/runner.js CHANGED
@@ -1707,7 +1707,13 @@ function decorateForegroundInvocation(invocation, runContext, bundle) {
1707
1707
  if (runContext.origin?.adapterId) env.OC_CHANNEL_ADAPTER = runContext.origin.adapterId;
1708
1708
  if (runContext.channelId) env.OC_CHANNEL_ID = String(runContext.channelId);
1709
1709
  if (runContext.origin?.userId) env.OC_CHANNEL_USER_ID = String(runContext.origin.userId);
1710
- if (runContext.canonicalUserId) env.OC_CANONICAL_USER_ID = String(runContext.canonicalUserId);
1710
+ if (runContext.canonicalUserId) {
1711
+ env.OC_CANONICAL_USER_ID = String(runContext.canonicalUserId);
1712
+ // Server-side attestation of the id above: the loopback's owner-gated
1713
+ // kinds only honor a canonicalUserId whose attest verifies, so a shell
1714
+ // that rewrites OC_CANONICAL_USER_ID cannot impersonate the owner.
1715
+ try { env.OC_SPEAKER_ATTEST = require("./speaker-attest").mint(runContext.canonicalUserId); } catch (e) {}
1716
+ }
1711
1717
  if (runContext.sessionId) env.OC_LAST_SESSION_ID = String(runContext.sessionId);
1712
1718
  if (lb?.url) env.OC_SEND_URL = lb.url;
1713
1719
  }
@@ -1725,17 +1731,24 @@ function decorateForegroundInvocation(invocation, runContext, bundle) {
1725
1731
  return { ...invocation, env };
1726
1732
  }
1727
1733
 
1734
+ // Singleton per chat state: a new start supersedes any running heartbeat
1735
+ // (pre-turn "thinking" started at message receipt hands off to the run's own
1736
+ // heartbeat here). A stopper only acts while its own interval is current, so
1737
+ // a stale stopper from a superseded start can never kill a newer heartbeat.
1728
1738
  function startTypingHeartbeat(state, store) {
1729
1739
  const adapter = store?.adapter || currentAdapter();
1730
1740
  const channelId = store?.channelId || currentChannelId();
1731
1741
  if (!adapter || !channelId) return () => {};
1742
+ if (state.typingHeartbeat) clearInterval(state.typingHeartbeat);
1732
1743
  const tick = () => adapter.typing(channelId, state.thinkingPhase ? "thinking" : undefined).catch(() => {});
1733
1744
  state.thinkingPhase = true;
1734
1745
  tick();
1735
- state.typingHeartbeat = setInterval(tick, 4000);
1746
+ const interval = setInterval(tick, 4000);
1747
+ state.typingHeartbeat = interval;
1736
1748
  return () => {
1749
+ if (state.typingHeartbeat !== interval) return;
1737
1750
  state.thinkingPhase = false;
1738
- if (state.typingHeartbeat) clearInterval(state.typingHeartbeat);
1751
+ clearInterval(interval);
1739
1752
  state.typingHeartbeat = null;
1740
1753
  try { adapter.typingStop?.(channelId); } catch (_) {}
1741
1754
  };
@@ -1880,7 +1893,10 @@ function createForegroundService({ state, store, stopTyping, capture, ownsAdmiss
1880
1893
  observer.onRecallSurfaced(bundle.recallMetadata);
1881
1894
  },
1882
1895
  onEvent(event, runContext) {
1883
- if (["text_delta", "text_final", "tool_start", "result", "error"].includes(event.type)) state.thinkingPhase = false;
1896
+ // Streaming text reads as "typing"; tool work re-enters "thinking" so
1897
+ // the chip doesn't decay to a stale "typing" through long tool stretches.
1898
+ if (["text_delta", "text_final", "result", "error"].includes(event.type)) state.thinkingPhase = false;
1899
+ else if (event.type === "tool_start") state.thinkingPhase = true;
1884
1900
  if (event.type === "tool_start" && ["Shell", "Bash"].includes(event.name)) {
1885
1901
  const command = typeof event.detail === "string" ? event.detail : event.detail?.command;
1886
1902
  if (command) { try { require("./tool-guard").noteRawOp(command); } catch (_) {} }
@@ -2248,6 +2264,7 @@ module.exports = {
2248
2264
  runAgent,
2249
2265
  runAgentCapture,
2250
2266
  runAgentSilent,
2267
+ startTypingHeartbeat,
2251
2268
  shouldRetryRefusedRun,
2252
2269
  quarantineRefusedRun,
2253
2270
  refusalRetryContext,
package/core/scheduler.js CHANGED
@@ -43,6 +43,10 @@ const RETRY_DELAY_MS = 30 * 1000;
43
43
  const BUSY_DELAY_MS = 30 * 1000;
44
44
  // A watch's `check` command is capped so a hung poll can't wedge the tick.
45
45
  const WATCH_CHECK_TIMEOUT_MS = 30 * 1000;
46
+ // Every watch carries a hard expiry so none can idle-poll forever: when the
47
+ // deadline lapses with the check still failing, the next poll tick fires the
48
+ // wake anyway (marked expired) instead of skipping silently.
49
+ const DEFAULT_WATCH_TIMEOUT_MS = 2 * 60 * 60 * 1000;
46
50
  const WATCH_LOG_FILE = process.env.OPEN_CLAUDIA_WATCH_LOG_FILE || path.join(CONFIG_DIR, "watch.log");
47
51
  const WATCHES_ENABLED_FILE = process.env.OPEN_CLAUDIA_WATCHES_ENABLED_FILE || path.join(CONFIG_DIR, "watches-enabled");
48
52
 
@@ -107,6 +111,7 @@ function createSchedulerService(dependencies = {}) {
107
111
  const retryDelayMs = Number.isFinite(dependencies.retryDelayMs) ? dependencies.retryDelayMs : RETRY_DELAY_MS;
108
112
  const busyDelayMs = Number.isFinite(dependencies.busyDelayMs) ? dependencies.busyDelayMs : BUSY_DELAY_MS;
109
113
  const missedGraceMs = Number.isFinite(dependencies.missedGraceMs) ? dependencies.missedGraceMs : MISSED_WAKEUP_GRACE_MS;
114
+ const watchTimeoutMs = Number.isFinite(dependencies.watchTimeoutMs) ? dependencies.watchTimeoutMs : DEFAULT_WATCH_TIMEOUT_MS;
110
115
  const occurrenceIdFactory = dependencies.occurrenceIdFactory || (() => uniqueId("occ"));
111
116
  const legacyCronsFile = dependencies.legacyCronsFile === undefined
112
117
  ? (dependencies.jobsStore ? null : CRONS_FILE)
@@ -273,22 +278,21 @@ function createSchedulerService(dependencies = {}) {
273
278
 
274
279
  function wrappedPrompt(job, target) {
275
280
  const firedAt = new Date(now()).toISOString();
276
- const header = job.check ? "Watch triggered" : job.kind === "cron" ? "Scheduled cron" : "Scheduled wakeup";
277
- if (target.fallbackFrom) {
278
- return [
279
- `[${header} "${job.label}" fired at ${firedAt}; occurrence ${job.occurrenceId}]`,
280
- `[Archived provider-neutral brief: the originating ${target.fallbackFrom} provider is unavailable. Its native session is not being resumed; this explicitly configured fallback starts fresh.]`,
281
- job.prompt,
282
- ].join("\n");
281
+ const expired = !!(job.check && job.watchExpired);
282
+ const header = job.check
283
+ ? (expired ? "Watch expired" : "Watch triggered")
284
+ : job.kind === "cron" ? "Scheduled cron" : "Scheduled wakeup";
285
+ const lines = [`[${header} "${job.label}" fired at ${firedAt}; occurrence ${job.occurrenceId}]`];
286
+ if (expired) {
287
+ lines.push("[This watch reached its timeout without its check ever passing — the watched condition did NOT happen. This is a deadline wake-up, not a trigger: investigate why the condition never became true, and arm a new watch if it is still worth waiting for.]");
283
288
  }
284
- if (target.switchedFrom) {
285
- return [
286
- `[${header} "${job.label}" fired at ${firedAt}; occurrence ${job.occurrenceId}]`,
287
- `[Provider now tracks your current selection (${target.provider}); this was scheduled under ${target.switchedFrom}, whose native session cannot cross providers, so this run starts a fresh thread.]`,
288
- job.prompt,
289
- ].join("\n");
289
+ if (target.fallbackFrom) {
290
+ lines.push(`[Archived provider-neutral brief: the originating ${target.fallbackFrom} provider is unavailable. Its native session is not being resumed; this explicitly configured fallback starts fresh.]`);
291
+ } else if (target.switchedFrom) {
292
+ lines.push(`[Provider now tracks your current selection (${target.provider}); this was scheduled under ${target.switchedFrom}, whose native session cannot cross providers, so this run starts a fresh thread.]`);
290
293
  }
291
- return `[${header} "${job.label}" fired at ${firedAt}; occurrence ${job.occurrenceId}]\n${job.prompt}`;
294
+ lines.push(job.prompt);
295
+ return lines.join("\n");
292
296
  }
293
297
 
294
298
  function unscheduleJob(id) {
@@ -394,7 +398,7 @@ function createSchedulerService(dependencies = {}) {
394
398
 
395
399
  async function fireJob(idOrJob, options = {}) {
396
400
  const requested = typeof idOrJob === "string" ? jobsStore.get(idOrJob) : idOrJob;
397
- const job = requested?.id && jobsStore.get(requested.id) ? jobsStore.get(requested.id) : requested;
401
+ let job = requested?.id && jobsStore.get(requested.id) ? jobsStore.get(requested.id) : requested;
398
402
  if (!job) return { ok: false, status: "missing" };
399
403
  if (job.disabled || job.status === "dead-letter" || job.status === "archived") {
400
404
  return { ok: false, status: job.status, skipped: true };
@@ -424,10 +428,35 @@ function createSchedulerService(dependencies = {}) {
424
428
  catch (_) { check = { code: 1, timedOut: false }; }
425
429
  finally { checksInFlight.delete(job.id); }
426
430
  if (!check || check.code !== 0) {
427
- logWatch(job, check && check.timedOut ? "idle (check timed out)" : `idle (check exit ${check ? check.code : "?"})`);
428
- return { ok: false, status: "watch-idle", skipped: true };
431
+ // Expiry backstop: a watch whose deadline has lapsed fires the wake
432
+ // anyway (marked expired) so no watch can ever end silently. Watches
433
+ // persisted before expiry existed (no expiresAt) keep polling forever.
434
+ const expiresAt = Number(job.expiresAt || 0);
435
+ if (!(expiresAt > 0 && now() >= expiresAt)) {
436
+ logWatch(job, check && check.timedOut ? "idle (check timed out)" : `idle (check exit ${check ? check.code : "?"})`);
437
+ return { ok: false, status: "watch-idle", skipped: true };
438
+ }
439
+ logWatch(job, `expired without firing (check exit ${check ? check.code : "?"}) — waking agent`);
440
+ // Expiry ends the watch's window: one-shot removal even for --repeat
441
+ // (the woken agent re-arms if still wanted). Persisted so a retry of a
442
+ // failed expiry fire keeps the same semantics across restarts.
443
+ try {
444
+ const marked = jobsStore.update(job.id, { repeat: false, watchExpired: true });
445
+ if (marked) job = marked;
446
+ else { job.repeat = false; job.watchExpired = true; }
447
+ } catch (_) { job.repeat = false; job.watchExpired = true; }
448
+ } else {
449
+ logWatch(job, "triggered (check passed) — waking agent");
450
+ if (job.watchExpired) {
451
+ // A prior expiry fire failed and the condition came true before the
452
+ // retry: report it as a real trigger, not an expiry.
453
+ try {
454
+ const cleared = jobsStore.update(job.id, { watchExpired: false });
455
+ if (cleared) job = cleared;
456
+ else job.watchExpired = false;
457
+ } catch (_) { job.watchExpired = false; }
458
+ }
429
459
  }
430
- logWatch(job, "triggered (check passed) — waking agent");
431
460
  }
432
461
 
433
462
  const adapter = resolveAdapter(job);
@@ -504,7 +533,10 @@ function createSchedulerService(dependencies = {}) {
504
533
  return { ok: false, status: "failed", error: stableFailure(error) };
505
534
  }
506
535
 
507
- try { await send(job.check ? `🔔 Watch "${job.label}" triggered` : `Wakeup: ${job.label}`); } catch (_) { /* advisory only */ }
536
+ const advisory = job.check
537
+ ? (job.watchExpired ? `⏰ Watch "${job.label}" timed out without triggering — waking anyway` : `🔔 Watch "${job.label}" triggered`)
538
+ : `Wakeup: ${job.label}`;
539
+ try { await send(advisory); } catch (_) { /* advisory only */ }
508
540
  let result;
509
541
  try { result = await runInProvider(prompt, runContext.cwd, null, { ...runOptions, runContext }); }
510
542
  catch (error) {
@@ -655,15 +687,21 @@ function createSchedulerService(dependencies = {}) {
655
687
  }
656
688
 
657
689
  // A watch is a cron carrying a shell `check`; it rides the migration-verified
658
- // jobs store untouched (check/repeat survive normalizeRunnableJob as extra
659
- // fields). Default schedule polls every minute; default is one-shot.
690
+ // jobs store untouched (check/repeat/expiresAt survive normalizeRunnableJob
691
+ // as extra fields). Default schedule polls every minute; default is one-shot.
692
+ // Every new watch gets an expiry (caller's expiresAt or the default timeout)
693
+ // so it can never idle-poll silently forever.
660
694
  function addWatch(spec) {
695
+ const requestedExpiry = Number(spec.expiresAt);
661
696
  return addJob({
662
697
  ...spec,
663
698
  kind: "cron",
664
699
  schedule: spec.schedule || "* * * * *",
665
700
  check: String(spec.check || "").trim(),
666
701
  repeat: !!spec.repeat,
702
+ expiresAt: Number.isFinite(requestedExpiry) && requestedExpiry > 0
703
+ ? requestedExpiry
704
+ : now() + watchTimeoutMs,
667
705
  label: spec.label || String(spec.prompt || "watch").slice(0, 60),
668
706
  });
669
707
  }
@@ -710,6 +748,7 @@ module.exports = {
710
748
  MISSED_WAKEUP_GRACE_MS,
711
749
  RETRY_DELAY_MS,
712
750
  BUSY_DELAY_MS,
751
+ DEFAULT_WATCH_TIMEOUT_MS,
713
752
  createSchedulerService,
714
753
  parseFallbacks,
715
754
  terminalRunSucceeded,
@@ -0,0 +1,56 @@
1
+ // Speaker attestation: lets the loopback server trust OC_CANONICAL_USER_ID.
2
+ //
3
+ // The problem: the loopback's owner-gated kinds (people mutations, auth,
4
+ // intros) identified the caller by payload.canonicalUserId, which the CLI read
5
+ // from env — and env is writable by the agent's shell, so a guarded external
6
+ // turn could forge `OC_CANONICAL_USER_ID=<owner-id> open-claudia people link …`
7
+ // and rewrite the identity map the relationship guard depends on.
8
+ //
9
+ // The fix: the RUNNER (which knows the turn's true speaker from the inbound
10
+ // envelope, server-side) mints an HMAC over the canonical user id with a
11
+ // per-boot secret that lives ONLY in bot-process memory — never on disk, never
12
+ // in the loopback info file, stripped from provider env like all control-plane
13
+ // keys would be (it is simply never exported). The provider spawn env carries
14
+ // OC_SPEAKER_ATTEST next to OC_CANONICAL_USER_ID; the CLI forwards both; the
15
+ // loopback verifies the pair before honoring an owner claim. An agent can
16
+ // still LIE about OC_CANONICAL_USER_ID, but it cannot mint a matching
17
+ // signature for any id other than the one the runner stamped for its turn.
18
+ //
19
+ // Scope and honesty: this authenticates "the runner said this turn speaks as
20
+ // X". It is per-boot (a restart invalidates in-flight attests — same blast
21
+ // radius as the loopback port/token rotating) and deliberately unforgeable
22
+ // rather than merely obscure: no attest, wrong attest, or an attest for a
23
+ // different id all fail closed. It does NOT bind adapter/channel — those stay
24
+ // best-effort inputs to the guarded-speaker checks, whose spoof failure modes
25
+ // are self-limiting (buttons/files land on the spoofed channel, i.e. the
26
+ // owner's own chat, never silently on the attacker's).
27
+
28
+ const crypto = require("crypto");
29
+
30
+ // Lazily created so tests get a fresh secret per process, like production.
31
+ let SECRET = null;
32
+
33
+ function secret() {
34
+ if (!SECRET) SECRET = crypto.randomBytes(32);
35
+ return SECRET;
36
+ }
37
+
38
+ function mint(canonicalUserId) {
39
+ const id = String(canonicalUserId || "");
40
+ if (!id) return "";
41
+ return crypto.createHmac("sha256", secret()).update(id).digest("hex");
42
+ }
43
+
44
+ // Constant-time compare; every malformed shape is a plain false (fail closed).
45
+ function verify(canonicalUserId, attest) {
46
+ const id = String(canonicalUserId || "");
47
+ const sig = String(attest || "");
48
+ if (!id || !sig) return false;
49
+ const expected = mint(id);
50
+ const a = Buffer.from(sig, "utf-8");
51
+ const b = Buffer.from(expected, "utf-8");
52
+ if (a.length !== b.length) return false;
53
+ try { return crypto.timingSafeEqual(a, b); } catch (e) { return false; }
54
+ }
55
+
56
+ module.exports = { mint, verify };
@@ -255,6 +255,7 @@ Wake-ups and crons (real schedulers, not hallucinations — use them instead of
255
255
 
256
256
  Watches (a silent background monitor — poll a condition WITHOUT waking every tick; wake only when it actually happens). Use this instead of a minute-by-minute cron whenever you'd otherwise fire a turn just to check "is it done yet?" and stay quiet most times:
257
257
  - \`open-claudia watch-add "<check-cmd>" "<wake-prompt>"\` — the scheduler polls \`<check-cmd>\` as a shell command (default every minute). The poll is SILENT: nothing reaches the chat while the check keeps failing. The instant \`<check-cmd>\` exits 0, you wake in this same conversation with \`<wake-prompt>\`, exactly like a wakeup. Non-zero exit = still waiting = nothing happens. Default is ONE-SHOT (fires once, then removes itself); add \`--repeat\` to keep polling after each hit. \`--every "<5-field cron>"\` changes the poll cadence.
258
+ - Every watch has a hard timeout — \`--timeout <duration|ISO>\` (default 2h). A watch that reaches its timeout without the check ever passing FIRES anyway on the next poll, with the prompt marked "Watch expired" so you know the condition did NOT happen. No watch can end silently, so you no longer need a separate backup wakeup for every watch — but size \`--timeout\` honestly to the work being watched.
258
259
  - \`open-claudia watch-list\` / \`open-claudia watch-remove <id>\` — inspect/cancel watches on this channel.
259
260
  - The owner has a global master switch: \`/watch off\` pauses ALL watch polling (and \`/watch on\` resumes). If \`watch-list\` shows the switch OFF, watches won't fire until it's back on.
260
261
  - Example: after kicking off a long external job, \`open-claudia watch-add "test -f /tmp/build-done || exit 1" "the build finished — verify and report"\` beats a wakeup guess, because it fires the moment it's actually done, not on a fixed timer. (Never use a watch to poll harness-tracked work you started — that notifies you automatically. Watches are for EXTERNAL state the harness can't see.)
@@ -864,20 +865,29 @@ function buildSpeakerPersonaBlock(opts = {}) {
864
865
  if (!speaker.isOwner && (s.Mandate || "").trim()) {
865
866
  rows.push(`Mandate — what you may do/discuss with them: ${s.Mandate.trim()}`);
866
867
  }
867
- if (rows.length === 0) return "";
868
+ // A bare note used to render nothing — which hid the guard's policy source
869
+ // exactly when it mattered most (auto-added stubs are born bare, so the
870
+ // agent couldn't see WHICH file governs the speaker, or that a same-named
871
+ // note elsewhere is a different identity). For non-owners the block always
872
+ // renders; an owner with an empty note still gets the quiet no-op.
873
+ if (rows.length === 0 && speaker.isOwner) return "";
868
874
  const runIdentity = promptDedupeIdentity(opts);
869
875
  const key = `${adapter.id || "?"}:${channelId}`;
870
876
  const stamp = `${runIdentity}:${slug}:${ent.updated}`;
871
877
  if (speakerPersonaInjectedFor.get(key) === stamp) return "";
872
878
  speakerPersonaInjectedFor.set(key, stamp);
873
879
  const rel = ent.relationship || (speaker.isOwner ? "owner" : "");
880
+ const unverified = !speaker.isOwner && !!speaker.autoCreated;
874
881
  const clip = (str) => (str.length > SPEAKER_PERSONA_MAX_CHARS ? str.slice(0, SPEAKER_PERSONA_MAX_CHARS) + "\n…[truncated — `open-claudia entity show " + slug + "`]" : str);
875
882
  return clip([
876
- `\n\n## Who you're speaking with: ${speaker.name}${rel ? ` (${rel})` : ""}`,
883
+ `\n\n## Who you're speaking with: ${speaker.name}${rel ? ` (${rel})` : ""}${unverified ? " — UNVERIFIED auto-added contact" : ""}`,
877
884
  `Their persona pack (source: ${entitiesLib.ENTITIES_DIR}/${slug}.md) — adapt your voice and scope to it. Edit that file to refine how you treat them.`,
885
+ ...(speaker.isOwner ? [] : [
886
+ `That file is the ONLY note the relationship guard reads for them — a same-named note anywhere else belongs to a DIFFERENT identity and grants nothing here.${unverified ? " The owner has not yet confirmed who they are (verification happens on the owner's approval card or /people verify)." : ""}`,
887
+ ]),
878
888
  "",
879
889
  rows.join("\n"),
880
- ].join("\n"));
890
+ ].join("\n")).trimEnd();
881
891
  }
882
892
 
883
893
  // External-mode posture block (Phase 3). For a non-owner (guarded) speaker,
@@ -895,9 +905,17 @@ function buildExternalModeBlock() {
895
905
  const who = speaker.name || "this person";
896
906
  const mandate = (speaker.mandate || "").trim();
897
907
  const unverified = !!(speaker.person && speaker.person.autoCreated);
908
+ let sourceLine = "";
909
+ if (speaker.slug) {
910
+ try {
911
+ const entitiesLib = require("./entities");
912
+ sourceLine = `Guard policy source: ${entitiesLib.ENTITIES_DIR}/${speaker.slug}.md — the ONLY note the guard reads for ${who}. A same-named note elsewhere is a different identity and grants nothing here.`;
913
+ } catch (e) {}
914
+ }
898
915
  const lines = [
899
916
  `\n\n## External speaker — restricted mode`,
900
- `You are talking to ${who}, who is NOT the owner. You are operating under a strict, owner-defined scope.`,
917
+ `You are talking to ${who}, who is NOT the owner. You are operating under a strict, owner-defined scope.${unverified ? ` Their identity is UNVERIFIED (auto-added on first contact — only the owner can verify them, via the approval card or /people verify).` : ""}`,
918
+ ...(sourceLine ? [sourceLine] : []),
901
919
  "",
902
920
  mandate
903
921
  ? `Mandate — the ONLY things you are cleared to do or discuss with ${who}:\n${mandate}`
@@ -0,0 +1,156 @@
1
+ // Owner verification of auto-added contacts — the second half of the
2
+ // ensureContact story. ensureContact deliberately NEVER links a first-time
3
+ // speaker to an existing note by display name (names are claimable, so that
4
+ // would be an impersonation path); it mints an unverified stub instead. This
5
+ // module gives the OWNER the explicit, audited act that closes the loop:
6
+ // confirm who the speaker is, optionally bind them to the existing trusted
7
+ // note, retire the stub, and clear the unverified flag the enforcer surfaces.
8
+ //
9
+ // Security invariants (each refusal below is one of them):
10
+ // - Verification is an OWNER decision arriving via a chat-envelope surface
11
+ // (approval-card button / /people slash command). There is deliberately no
12
+ // CLI or loopback verb: identity decisions never run on agent-forgeable
13
+ // input. Callers must gate on the transport envelope BEFORE calling here.
14
+ // - Verification is NOT trust: it settles who the person is. Relationship
15
+ // upgrades and mandate lines remain separate explicit owner grants.
16
+ // - Never link to an owner-relationship note (R1 defense-in-depth: speakerFor
17
+ // already coerces such a claim, but this surface refuses to create it).
18
+ // - Never link two people to one note: a persona pack carries a Mandate, and
19
+ // sharing one across identities would silently share granted capabilities.
20
+ // - Only auto-added (autoCreated) contacts are verifiable here. Re-pointing an
21
+ // already-verified person is intentionally NOT offered, so this surface can
22
+ // never be used as a general re-identity tool.
23
+
24
+ const fs = require("fs");
25
+ const path = require("path");
26
+ const people = require("./people");
27
+ const entities = require("./entities");
28
+ const audit = require("./audit");
29
+
30
+ // Same-name suggestions for the owner surfaces: existing person-type notes
31
+ // this unverified contact could plausibly BE. Excludes the contact's own stub,
32
+ // owner-relationship notes, and notes already bound to another person record.
33
+ // Suggestion-only — verifyContact re-validates whatever the owner picks.
34
+ function candidateEntitiesFor(person) {
35
+ if (!person) return [];
36
+ const stubSlug = people.resolveEntitySlug(person);
37
+ const wanted = String(person.name || "").trim().toLowerCase();
38
+ if (!wanted) return [];
39
+ const taken = new Set(
40
+ people.list()
41
+ .filter((p) => p.id !== person.id)
42
+ .map((p) => people.resolveEntitySlug(p))
43
+ .filter(Boolean),
44
+ );
45
+ const out = [];
46
+ for (const e of entities.listEntities()) {
47
+ if (e.type !== "person") continue;
48
+ if (e.slug === stubSlug) continue;
49
+ if (e.relationship === "owner") continue;
50
+ if (taken.has(e.slug)) continue;
51
+ const names = [e.name, ...(e.aliases || [])]
52
+ .map((n) => String(n || "").trim().toLowerCase())
53
+ .filter(Boolean);
54
+ if (names.includes(wanted)) out.push(e);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function refuse(reason, extra = {}) {
60
+ return { ok: false, reason, ...extra };
61
+ }
62
+
63
+ // Verify (and optionally link) one auto-added contact.
64
+ // personId — the people.json record to verify
65
+ // linkSlug — optional existing note to bind them to; omitted = "this stub
66
+ // IS the right note, just mark them verified"
67
+ // by / via — provenance for the audit trail (owner id, surface name)
68
+ function verifyContact({ personId, linkSlug = null, by = "", via = "" } = {}) {
69
+ const person = people.findById(String(personId || ""));
70
+ if (!person) return refuse("person not found");
71
+ if (person.isOwner) return refuse("the owner record is never routed through contact verification");
72
+ if (!person.autoCreated) return refuse(`${person.name} is already verified`, { already: true });
73
+
74
+ const stubSlug = people.resolveEntitySlug(person);
75
+ let targetSlug = stubSlug;
76
+ let supersededStub = false;
77
+
78
+ if (linkSlug) {
79
+ const wantedSlug = entities.slugify(String(linkSlug));
80
+ if (wantedSlug && wantedSlug !== stubSlug) {
81
+ const target = entities.readEntity(wantedSlug);
82
+ if (!target) return refuse(`no note found at ${wantedSlug}`);
83
+ if (target.type !== "person") return refuse(`${wantedSlug} is a ${target.type} note, not a person`);
84
+ if (target.relationship === "owner") {
85
+ return refuse("refusing to link a contact to an owner-relationship note — ownership only ever comes from the people record");
86
+ }
87
+ const holder = people.list().find(
88
+ (p) => p.id !== person.id && people.resolveEntitySlug(p) === target.slug,
89
+ );
90
+ if (holder) {
91
+ return refuse(`that note already belongs to ${holder.name} — two people can never share one persona pack`);
92
+ }
93
+ targetSlug = target.slug;
94
+
95
+ // Provenance onto the surviving note, then retire the stub file as
96
+ // .superseded (kept on disk — the merge is inspectable, not destructive).
97
+ entities.upsertEntity({
98
+ slug: target.slug,
99
+ name: target.name,
100
+ log: `owner verified ${person.name} as this contact (absorbed auto-added stub ${stubSlug})`,
101
+ }, "owner-verify");
102
+ if (stubSlug) {
103
+ const stubFile = path.join(entities.ENTITIES_DIR, `${stubSlug}.md`);
104
+ try {
105
+ if (fs.existsSync(stubFile)) {
106
+ fs.renameSync(stubFile, `${stubFile}.superseded`);
107
+ supersededStub = true;
108
+ }
109
+ } catch (e) { /* best-effort: a stranded stub is inert once unlinked */ }
110
+ try { entities.markIndexDirty(); } catch (e) {}
111
+ }
112
+ people.setEntitySlug(person.id, target.slug);
113
+ }
114
+ }
115
+
116
+ if (targetSlug === stubSlug && stubSlug) {
117
+ // Verify-only: the stub becomes the canonical note for this person. The
118
+ // description keeps its first-seen provenance, minus the unverified label.
119
+ const stub = entities.readEntity(stubSlug);
120
+ const description = stub && stub.description
121
+ ? stub.description
122
+ .replace(/^Auto-added contact \(unverified\)/, "Verified contact")
123
+ .replace(/\s*Possible existing note:.*$/, "")
124
+ : "Verified contact — identity confirmed by the owner.";
125
+ entities.upsertEntity({
126
+ slug: stubSlug,
127
+ name: person.name,
128
+ description,
129
+ log: "owner verified this contact; this note is now their canonical persona pack",
130
+ }, "owner-verify");
131
+ }
132
+
133
+ const fresh = people.findById(person.id) || person;
134
+ const updated = people.update(person.id, {
135
+ autoCreated: false,
136
+ verifiedAt: new Date().toISOString(),
137
+ verifiedBy: String(by || "owner"),
138
+ verifiedVia: String(via || ""),
139
+ handles: (fresh.handles || []).map((h) => (
140
+ h.approvedBy === "auto-contact" ? { ...h, approvedBy: String(by || "owner") } : h
141
+ )),
142
+ });
143
+
144
+ audit.log("people.verify", {
145
+ by: String(by || "owner"),
146
+ via: String(via || ""),
147
+ personId: person.id,
148
+ name: person.name,
149
+ slug: targetSlug,
150
+ supersededStub: supersededStub ? stubSlug : null,
151
+ });
152
+
153
+ return { ok: true, person: updated, slug: targetSlug, superseded: supersededStub ? stubSlug : null };
154
+ }
155
+
156
+ module.exports = { verifyContact, candidateEntitiesFor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.2.1",
3
+ "version": "3.3.1",
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-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"
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"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -25,7 +25,10 @@ function fixture() {
25
25
  assert.strictEqual(f.events[0].payload.reason, "unsupported_media");
26
26
  f = fixture();
27
27
  await f.manager.incoming({ fromUserId: "owner-1", fromUserName: "Owner", chatId: "chat", roomId: 12, callType: "audio" });
28
- assert.strictEqual(f.events[0].name, "call:accept");
28
+ // Ringing must go out first (caller UI feedback), then the accept.
29
+ assert.strictEqual(f.events[0].name, "call:ringing");
30
+ assert.deepStrictEqual(f.events[0].payload, { toUserId: "owner-1", chatId: "chat" });
31
+ assert.strictEqual(f.events[1].name, "call:accept");
29
32
  f.manager.session.options.onTranscript("What is on my agenda?");
30
33
  assert.strictEqual(f.inbound[0].payload.raw.ephemeralAudio, true);
31
34
  // Call turns must mute observer banners (spoken otherwise) and flag live-call