@inetafrica/open-claudia 3.5.17 → 3.5.18

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.
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+
3
+ // Plan usage limits: the 5-hour and weekly windows of a Claude Pro/Max plan
4
+ // and of the ChatGPT plan behind Codex.
5
+ //
6
+ // - recordWindows() keeps the latest use of each window (Claude's
7
+ // rate_limit_event, Codex's rollout snapshot) and says which warning
8
+ // percentages a turn has just crossed, once per window;
9
+ // - limitHitDetails() works out when the limit that stopped a turn resets,
10
+ // from the provider's own reset time, else from the wording of its notice;
11
+ // - the format* helpers word the chat lines.
12
+ //
13
+ // The pick-up job that re-runs a stopped message at the reset lives in
14
+ // core/limit-pickup.js.
15
+
16
+ const path = require("path");
17
+ const { CONFIG_DIR, config } = require("./config");
18
+ const { atomicWriteFileSync, readJsonWithFallback, withFileLock } = require("./fsutil");
19
+
20
+ const DEFAULT_FILE = path.join(CONFIG_DIR, "usage-limits.json");
21
+ const DEFAULT_WARN_PERCENTS = [80, 95];
22
+ // A window counts as new (warnings start over) when its reset time moves by
23
+ // more than this; Claude and Codex both report a fixed reset per window.
24
+ const NEW_WINDOW_SLACK_MS = 10 * 60 * 1000;
25
+ const PROVIDER_LABELS = { claude: "Claude", codex: "Codex" };
26
+ const WINDOW_LABELS = {
27
+ five_hour: "5-hour limit",
28
+ seven_day: "weekly limit",
29
+ seven_day_opus: "weekly Opus limit",
30
+ seven_day_sonnet: "weekly Sonnet limit",
31
+ seven_day_overage_included: "weekly limit",
32
+ overage: "extra-usage limit",
33
+ };
34
+ const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
35
+
36
+ const timeZoneDefault = () => process.env.OC_TIMEZONE || config.OC_TIMEZONE || "Africa/Nairobi";
37
+
38
+ // LIMIT_WARN_PERCENTS: comma-separated (default 80,95); "off" for none.
39
+ function warnPercents(raw = process.env.LIMIT_WARN_PERCENTS ?? config.LIMIT_WARN_PERCENTS) {
40
+ if (raw === undefined || raw === null || String(raw).trim() === "") return [...DEFAULT_WARN_PERCENTS];
41
+ if (/^(off|no|none|false|0)$/i.test(String(raw).trim())) return [];
42
+ return String(raw).split(",").map((s) => Number(s.trim()))
43
+ .filter((n) => Number.isFinite(n) && n > 0 && n <= 100)
44
+ .sort((a, b) => a - b);
45
+ }
46
+
47
+ function providerLabel(provider) {
48
+ return PROVIDER_LABELS[provider] || String(provider || "The provider");
49
+ }
50
+
51
+ function windowLabel(id) {
52
+ if (!id) return "usage limit";
53
+ if (WINDOW_LABELS[id]) return WINDOW_LABELS[id];
54
+ const minutes = /^(\d+)m$/.exec(id);
55
+ if (minutes) {
56
+ const n = Number(minutes[1]);
57
+ return n % 60 === 0 ? `${n / 60}-hour limit` : `${n}-minute limit`;
58
+ }
59
+ return "usage limit";
60
+ }
61
+
62
+ // Codex's rollout snapshot ({ primary, secondary } windows with used_percent,
63
+ // window_minutes, resets_at) as windows named like Claude's. Which window is
64
+ // "primary" depends on the plan: on a Pro account it is the weekly one.
65
+ function codexWindows(rateLimits) {
66
+ if (!rateLimits || typeof rateLimits !== "object") return [];
67
+ const windows = [];
68
+ for (const window of [rateLimits.primary, rateLimits.secondary]) {
69
+ if (!window || !Number.isFinite(window.usedPercent)) continue;
70
+ const minutes = Number(window.windowMinutes);
71
+ const id = minutes === 300 ? "five_hour" : (minutes === 10080 ? "seven_day" : (Number.isFinite(minutes) && minutes > 0 ? `${minutes}m` : "plan"));
72
+ const resetsAt = window.resetsAt ? Date.parse(window.resetsAt) : NaN;
73
+ windows.push({ id, usedPercent: window.usedPercent, resetsAt: Number.isFinite(resetsAt) ? resetsAt : null });
74
+ }
75
+ return windows;
76
+ }
77
+
78
+ function readState(file) {
79
+ const state = readJsonWithFallback(file, null);
80
+ return state && typeof state === "object" && state.windows && typeof state.windows === "object"
81
+ ? state
82
+ : { windows: {} };
83
+ }
84
+
85
+ /**
86
+ * Remember the latest use of each window and return the warnings now due:
87
+ * at most one per window, for the highest percentage crossed that this window
88
+ * has not been warned about. A new window (reset time moved) or a drop below a
89
+ * percentage (window reset, or another account) clears its warnings.
90
+ */
91
+ function recordWindows({ provider, windows, file = DEFAULT_FILE, percents = warnPercents(), now = Date.now() } = {}) {
92
+ const usable = (windows || []).filter((w) => w && w.id && Number.isFinite(w.usedPercent));
93
+ if (!provider || !usable.length) return [];
94
+ // A test run that did not point the config dir somewhere else never writes
95
+ // the live bot's file.
96
+ if (file === DEFAULT_FILE && process.env.OPEN_CLAUDIA_TEST === "1" && !process.env.OPEN_CLAUDIA_CONFIG_DIR) return [];
97
+ return withFileLock(file, () => {
98
+ const state = readState(file);
99
+ const due = [];
100
+ for (const window of usable) {
101
+ const key = `${provider}:${window.id}`;
102
+ const previous = state.windows[key] || null;
103
+ const sameWindow = previous
104
+ && (!previous.resetsAt || !window.resetsAt || Math.abs(previous.resetsAt - window.resetsAt) <= NEW_WINDOW_SLACK_MS);
105
+ const warned = (sameWindow ? previous.warned || [] : []).filter((p) => window.usedPercent >= p);
106
+ const crossed = percents.filter((p) => window.usedPercent >= p && !warned.includes(p));
107
+ if (crossed.length) {
108
+ due.push({ provider, id: window.id, percent: crossed[crossed.length - 1], usedPercent: window.usedPercent, resetsAt: window.resetsAt || null });
109
+ }
110
+ state.windows[key] = {
111
+ usedPercent: window.usedPercent,
112
+ resetsAt: window.resetsAt || null,
113
+ updatedAt: new Date(now).toISOString(),
114
+ warned: [...new Set([...warned, ...crossed])].sort((a, b) => a - b),
115
+ };
116
+ }
117
+ atomicWriteFileSync(file, JSON.stringify(state, null, 2) + "\n");
118
+ return due;
119
+ });
120
+ }
121
+
122
+ /** The windows last seen per provider, newest reading first, for /usage. */
123
+ function knownWindows({ file = DEFAULT_FILE, now = Date.now() } = {}) {
124
+ const state = readState(file);
125
+ return Object.entries(state.windows)
126
+ .map(([key, value]) => {
127
+ const [provider, id] = key.split(":");
128
+ // A window whose reset has passed no longer says anything about now.
129
+ const stale = value.resetsAt && value.resetsAt <= now;
130
+ return stale ? null : { provider, id, ...value };
131
+ })
132
+ .filter(Boolean)
133
+ .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Time zones and wording
138
+
139
+ function zonedParts(t, timeZone) {
140
+ const parts = new Intl.DateTimeFormat("en-US", {
141
+ timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
142
+ }).formatToParts(new Date(t));
143
+ const p = Object.fromEntries(parts.map((x) => [x.type, Number(x.value)]));
144
+ return { year: p.year, month: p.month - 1, day: p.day, hour: p.hour % 24, minute: p.minute, second: p.second };
145
+ }
146
+
147
+ // The instant a wall-clock time in `timeZone` names (month is 0-based; day
148
+ // may overflow, Date.UTC rolls it). Two passes settle a DST change.
149
+ function zonedInstant({ year, month, day, hour, minute }, timeZone) {
150
+ const wall = Date.UTC(year, month, day, hour, minute);
151
+ let guess = wall;
152
+ for (let i = 0; i < 2; i += 1) {
153
+ const p = zonedParts(guess, timeZone);
154
+ const offset = Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second) - Math.floor(guess / 1000) * 1000;
155
+ guess = wall - offset;
156
+ }
157
+ return guess;
158
+ }
159
+
160
+ function validTimeZone(timeZone) {
161
+ try { new Intl.DateTimeFormat("en-US", { timeZone }); return true; } catch (_) { return false; }
162
+ }
163
+
164
+ function whenParts(at, { now = Date.now(), timeZone = timeZoneDefault() } = {}) {
165
+ const tz = validTimeZone(timeZone) ? timeZone : "UTC";
166
+ const time = new Intl.DateTimeFormat("en-GB", { timeZone: tz, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(new Date(at));
167
+ const day = (t) => { const p = zonedParts(t, tz); return Date.UTC(p.year, p.month, p.day); };
168
+ const days = Math.round((day(at) - day(now)) / 86400000);
169
+ const date = new Intl.DateTimeFormat("en-GB", { timeZone: tz, weekday: "short", day: "numeric", month: "short" }).format(new Date(at)).replace(",", "");
170
+ return { time, days, date };
171
+ }
172
+
173
+ /** "17:20" today, "tomorrow 08:00", "yesterday 12:59", else "Fri 9 Oct, 08:00" (local time). */
174
+ function formatResetTime(at, options = {}) {
175
+ const { time, days, date } = whenParts(at, options);
176
+ if (days === 0) return time;
177
+ if (days === 1) return `tomorrow ${time}`;
178
+ if (days === -1) return `yesterday ${time}`;
179
+ return `${date}, ${time}`;
180
+ }
181
+
182
+ /** The same with its preposition: "at 17:20", "tomorrow at 08:00", "on Fri 9 Oct at 08:00". */
183
+ function whenPhrase(at, options = {}) {
184
+ const { time, days, date } = whenParts(at, options);
185
+ if (days === 0) return `at ${time}`;
186
+ if (days === 1) return `tomorrow at ${time}`;
187
+ if (days === -1) return `yesterday at ${time}`;
188
+ return `on ${date} at ${time}`;
189
+ }
190
+
191
+ function formatWarning(due, options = {}) {
192
+ const used = Math.min(100, Math.round(due.usedPercent));
193
+ const resets = due.resetsAt ? ` It resets ${whenPhrase(due.resetsAt, options)}.` : "";
194
+ return `⚠️ ${providerLabel(due.provider)}: ${used}% of the ${windowLabel(due.id)} is used.${resets}`;
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // When does the limit that stopped a turn reset?
199
+
200
+ function hour24(hour, ampm) {
201
+ let h = Number(hour);
202
+ if (!ampm) return h;
203
+ h %= 12;
204
+ return /pm/i.test(ampm) ? h + 12 : h;
205
+ }
206
+
207
+ function monthIndex(name) {
208
+ return MONTHS.indexOf(String(name || "").slice(0, 3).toLowerCase());
209
+ }
210
+
211
+ // Claude Code: "You've hit your session limit · resets 11:30am (Africa/Nairobi)",
212
+ // "… resets Aug 21 at 8am (Africa/Nairobi)", "… resets Oct 9, 5am (UTC)".
213
+ function parseClaudeReset(text, { now, timeZone }) {
214
+ const m = /resets\s+(?:([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?(?:\s*\(([^)]+)\))?/i.exec(text);
215
+ if (!m) return null;
216
+ const tz = m[6] && validTimeZone(m[6].trim()) ? m[6].trim() : timeZone;
217
+ const hour = hour24(m[3], m[5]);
218
+ const minute = Number(m[4] || 0);
219
+ if (hour > 23 || minute > 59) return null;
220
+ const today = zonedParts(now, tz);
221
+ if (m[1]) {
222
+ const month = monthIndex(m[1]);
223
+ if (month < 0) return null;
224
+ let at = zonedInstant({ year: today.year, month, day: Number(m[2]), hour, minute }, tz);
225
+ // "Jan 2" read in late December means next year.
226
+ if (at < now - 180 * 86400000) at = zonedInstant({ year: today.year + 1, month, day: Number(m[2]), hour, minute }, tz);
227
+ return at;
228
+ }
229
+ let at = zonedInstant({ year: today.year, month: today.month, day: today.day, hour, minute }, tz);
230
+ if (at <= now) at = zonedInstant({ year: today.year, month: today.month, day: today.day + 1, hour, minute }, tz);
231
+ return at;
232
+ }
233
+
234
+ // Codex: "… or try again at Sep 7th, 2026 5:28 AM." / "try again at 5:28 AM."
235
+ // (the CLI's local time, which is this process's), or "try again in 2 days 3 hours".
236
+ function parseCodexReset(text, { now }) {
237
+ const at = /try again at\s+(?:([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})\s+)?(\d{1,2}):(\d{2})\s*(AM|PM)?/i.exec(text);
238
+ if (at) {
239
+ const hour = hour24(at[4], at[6]);
240
+ const minute = Number(at[5]);
241
+ if (at[1]) {
242
+ const month = monthIndex(at[1]);
243
+ if (month < 0) return null;
244
+ return new Date(Number(at[3]), month, Number(at[2]), hour, minute).getTime();
245
+ }
246
+ const base = new Date(now);
247
+ let t = new Date(base.getFullYear(), base.getMonth(), base.getDate(), hour, minute).getTime();
248
+ if (t <= now) t += 86400000;
249
+ return t;
250
+ }
251
+ const inMatch = /try again in\s+([^.]+)/i.exec(text);
252
+ if (!inMatch) return null;
253
+ const units = { day: 86400000, hour: 3600000, minute: 60000, min: 60000, second: 1000, sec: 1000 };
254
+ let total = 0;
255
+ for (const part of inMatch[1].matchAll(/(\d+)\s*(day|hour|minute|min|second|sec)s?/gi)) {
256
+ total += Number(part[1]) * units[part[2].toLowerCase()];
257
+ }
258
+ return total > 0 ? now + total : null;
259
+ }
260
+
261
+ function parseResetText(text, { now = Date.now(), timeZone = timeZoneDefault() } = {}) {
262
+ const s = String(text || "");
263
+ if (/resets\s/i.test(s)) return parseClaudeReset(s, { now, timeZone });
264
+ if (/try again (?:at|in)\s/i.test(s)) return parseCodexReset(s, { now });
265
+ return null;
266
+ }
267
+
268
+ function limitTypeFromText(text) {
269
+ const m = /hit your\s+(session|weekly|opus|sonnet|fable)?\s*limit/i.exec(String(text || ""));
270
+ if (!m || !m[1]) return null;
271
+ return { session: "five_hour", weekly: "seven_day", opus: "seven_day_opus", sonnet: "seven_day_sonnet", fable: "seven_day" }[m[1].toLowerCase()] || null;
272
+ }
273
+
274
+ /**
275
+ * What stopped a turn and when it lifts. `error` is the run's failure (its
276
+ * details carry Claude's limitResetsAt/limitType), `rateLimit` the run's last
277
+ * Claude reading, `windows` Codex's rollout windows. Returns null when the
278
+ * failure was not a usage limit; resetsAt is null when no reset time is known.
279
+ */
280
+ function limitHitDetails({ provider, error, rateLimit = null, windows = [], now = Date.now(), timeZone = timeZoneDefault() } = {}) {
281
+ if (!error || error.code !== "PROVIDER_USAGE_LIMIT") return null;
282
+ const details = error.details || {};
283
+ const text = String(details.message || error.message || "");
284
+ let resetsAt = Number.isFinite(details.limitResetsAt) ? details.limitResetsAt : null;
285
+ let limitType = details.limitType || null;
286
+ if (!resetsAt && rateLimit && rateLimit.status === "rejected" && rateLimit.resetsAt) {
287
+ resetsAt = rateLimit.resetsAt;
288
+ limitType = limitType || rateLimit.limitType || null;
289
+ }
290
+ if (!resetsAt) {
291
+ // Codex: the window the plan used up.
292
+ const spent = (windows || []).filter((w) => w.usedPercent >= 100 && w.resetsAt && w.resetsAt > now)
293
+ .sort((a, b) => b.resetsAt - a.resetsAt)[0];
294
+ if (spent) { resetsAt = spent.resetsAt; limitType = limitType || spent.id; }
295
+ }
296
+ if (!resetsAt) resetsAt = parseResetText(text, { now, timeZone });
297
+ return {
298
+ provider,
299
+ limitType: limitType || limitTypeFromText(text),
300
+ resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
301
+ text,
302
+ };
303
+ }
304
+
305
+ module.exports = {
306
+ DEFAULT_FILE,
307
+ DEFAULT_WARN_PERCENTS,
308
+ warnPercents,
309
+ providerLabel,
310
+ windowLabel,
311
+ codexWindows,
312
+ recordWindows,
313
+ knownWindows,
314
+ formatResetTime,
315
+ whenPhrase,
316
+ formatWarning,
317
+ parseResetText,
318
+ limitTypeFromText,
319
+ limitHitDetails,
320
+ zonedInstant,
321
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.5.17",
3
+ "version": "3.5.18",
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-codex-output-schema.js && node test-codex-device-login.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-upgrade-path.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-kazee-context-media.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.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-group-status-notes.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-call-speech.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 && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-transcript-room-scope.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-read-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-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-kazee-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-thread-lanes.js && node test-thread-titles.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-thread-stop.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-stream-preview-purpose.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-loopback-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-run-attest.js && node test-sandbox-probe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-bot-endpoint.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-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-cli-owner-gates.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-watch-check-env.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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 && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js && node test-kazee-cards.js && node test-decision-upkeep.js && node test-passive-replies.js && node test-kazee-bot-loops.js && node test-kazee-bot-handoffs.js && node test-placeholder-reply.js && node test-media-speech.js && node test-passive-replies-router.js && node test-loopback-cards.js && node test-task-injection.js && node test-speaker-extras.js && node test-token-diet.js && node test-dream-tasks.js && node test-task-archive.js && node test-cron-tolerance.js && node test-dream-schedule.js && node test-job-failures.js && node test-scheduled-placeholder.js && node test-lessons-since.js && node test-pack-reinject.js && node test-turn-journal.js && node test-browser-view.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-view.js && node test-safe-filename.js && node test-media-filename-paths.js && node test-router-document-guard.js && OPEN_CLAUDIA_TEST=1 node test-web-dashboard-escape.js && OPEN_CLAUDIA_TEST=1 node test-web-bad-requests.js && OPEN_CLAUDIA_TEST=1 node test-kazee-download-limit.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-auth-routes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-otp-link-preview.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 && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-transcript-room-scope.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-read-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-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-kazee-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-thread-lanes.js && node test-thread-titles.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-thread-stop.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-stream-preview-purpose.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-loopback-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-run-attest.js && node test-sandbox-probe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-bot-endpoint.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-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-cli-owner-gates.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-watch-check-env.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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 && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js && node test-kazee-cards.js && node test-decision-upkeep.js && node test-passive-replies.js && node test-kazee-bot-loops.js && node test-kazee-bot-handoffs.js && node test-placeholder-reply.js && node test-media-speech.js && node test-passive-replies-router.js && node test-loopback-cards.js && node test-task-injection.js && node test-speaker-extras.js && node test-token-diet.js && node test-dream-tasks.js && node test-task-archive.js && node test-cron-tolerance.js && node test-dream-schedule.js && node test-job-failures.js && node test-scheduled-placeholder.js && node test-lessons-since.js && node test-pack-reinject.js && node test-turn-journal.js && node test-usage-limits.js && node test-limit-pickup.js && node test-browser-view.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-view.js && node test-safe-filename.js && node test-media-filename-paths.js && node test-router-document-guard.js && OPEN_CLAUDIA_TEST=1 node test-web-dashboard-escape.js && OPEN_CLAUDIA_TEST=1 node test-web-bad-requests.js && OPEN_CLAUDIA_TEST=1 node test-kazee-download-limit.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-auth-routes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-otp-link-preview.js"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -290,7 +290,28 @@ function claudeEvents(sessionId) {
290
290
  ];
291
291
  }
292
292
 
293
+ if (scenario === "usage-limit") {
294
+ // Claude Code 2.1.280 at a plan limit: the rejected rate_limit_event, the
295
+ // synthetic notice (error "rate_limit") and an error result.
296
+ const resetsAt = integerControl("FAKE_AGENT_LIMIT_RESETS_AT", Math.floor(Date.now() / 1000) + 3600, { min: 0, max: 2 ** 31 });
297
+ const text = process.env.FAKE_AGENT_LIMIT_TEXT || "You've hit your session limit · resets 5pm (UTC)";
298
+ return [
299
+ init,
300
+ {
301
+ type: "rate_limit_event",
302
+ rate_limit_info: { status: "rejected", resetsAt, rateLimitType: process.env.FAKE_AGENT_LIMIT_TYPE || "five_hour", isUsingOverage: false },
303
+ session_id: sessionId,
304
+ },
305
+ { type: "assistant", error: "rate_limit", message: { model: "<synthetic>", role: "assistant", content: [{ type: "text", text }] }, session_id: sessionId },
306
+ { type: "result", subtype: "success", is_error: true, session_id: sessionId, result: text },
307
+ ];
308
+ }
309
+
293
310
  const events = [init];
311
+ // Plan use as Claude Code reports it on a fresh run (JSON rate_limit_info).
312
+ if (process.env.FAKE_AGENT_RATE_LIMIT_INFO) {
313
+ events.push({ type: "rate_limit_event", rate_limit_info: JSON.parse(process.env.FAKE_AGENT_RATE_LIMIT_INFO), session_id: sessionId });
314
+ }
294
315
  if (process.env.FAKE_AGENT_THINKING) {
295
316
  events.push({
296
317
  type: "assistant",
@@ -589,7 +610,7 @@ async function main() {
589
610
  setInterval(() => {}, 1000);
590
611
  return;
591
612
  }
592
- if (scenario === "auth-failure" || scenario === "refusal") {
613
+ if (scenario === "auth-failure" || scenario === "refusal" || scenario === "usage-limit") {
593
614
  process.exitCode = 1;
594
615
  return;
595
616
  }
@@ -97,9 +97,11 @@ async function immutableSuccessProbe() {
97
97
  assert.strictEqual(stateApi.acquireRunLock(state), false, "foreground admission stays closed between compaction stages");
98
98
  alphaLane().isCompacting = false;
99
99
  const contexts = [];
100
- const service = serviceWith(async (_prompt, _cwd, options) => {
100
+ const prompts = [];
101
+ const service = serviceWith(async (prompt, _cwd, options) => {
101
102
  assert.strictEqual(options.persist, false, "compaction provider stages have no ordinary capture persistence");
102
103
  contexts.push(options.runContext);
104
+ prompts.push(String(prompt));
103
105
  assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "old-claude", "pointer stays old during both provider stages");
104
106
  if (contexts.length === 1) {
105
107
  state.currentSession = { name: "beta", dir: betaDir };
@@ -125,6 +127,11 @@ async function immutableSuccessProbe() {
125
127
  assert.strictEqual(contexts[1].timeoutMs, contexts[0].timeoutMs);
126
128
  assert.strictEqual(contexts[0].purpose, "compact-summary");
127
129
  assert.strictEqual(contexts[0].recallQuery, null, "summary stage carries no recall query");
130
+ // Claude runs the summary stage with --max-turns 1: a tool call (a transcript
131
+ // search, a status check) uses up the one turn and ends it with no brief, so
132
+ // the compaction fails and its cost is wasted (4 of 23 failed on 7-8 Oct).
133
+ assert.strictEqual(contexts[0].maxTurns, 1);
134
+ assert.ok(prompts[0].includes("Do NOT use any tools"), "the one-turn summary stage forbids tool calls");
128
135
  assert.strictEqual(contexts[1].purpose, "compact-seed");
129
136
  assert.ok(String(contexts[1].recallQuery || "").includes("A sufficiently detailed condensed continuation"),
130
137
  "seed recall query carries the condensed brief, not seed-prompt boilerplate");
@@ -156,6 +163,63 @@ async function immutableSuccessProbe() {
156
163
  assert.strictEqual(alphaLane().isCompacting, false);
157
164
  }
158
165
 
166
+ // A scheduled job's own conversation (a branch beside the thread's live
167
+ // session) is compacted in place of the live one: the summary reads the
168
+ // branch, the lineage lands in the history and the index, and the thread's
169
+ // live pointer stays where it is. Only the job follows the new session.
170
+ async function branchCompactionProbe() {
171
+ resetFixture("claude");
172
+ state.usageBySession["alpha\0claude\0job-branch"] = { ...stateApi.freshUsage(), turns: 17, liveContextTokens: 894000 };
173
+ const contexts = [];
174
+ const recorded = [];
175
+ const service = serviceWith(async (_prompt, _cwd, options) => {
176
+ contexts.push(options.runContext);
177
+ return result({ sessionId: contexts.length === 1 ? "job-branch" : "job-branch-new" });
178
+ }, {
179
+ commitBranchCompaction: stateApi.commitBranchCompaction,
180
+ recordCompaction: (entry) => recorded.push(entry),
181
+ });
182
+ const compacted = await service.compact(alphaDir, {
183
+ operationId: "compact-branch", sessionId: "job-branch", provider: "claude", compactReason: "cold-resume",
184
+ });
185
+ assert.strictEqual(compacted.compacted, true, JSON.stringify(compacted));
186
+ assert.strictEqual(compacted.branch, true);
187
+ assert.strictEqual(compacted.oldSessionId, "job-branch");
188
+ assert.strictEqual(contexts[0].sessionId, "job-branch", "the summary stage reads the job's conversation");
189
+ assert.strictEqual(contexts[0].resumeSessionId, "job-branch");
190
+ assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "old-claude", "the thread's live pointer stays");
191
+ const record = stateApi.getProjectSessions(state.userId, "alpha", "claude").find((entry) => entry.id === "job-branch-new");
192
+ assert.deepStrictEqual(record.lineage, {
193
+ type: "compaction",
194
+ operationId: "compact-branch",
195
+ previousSessionId: "job-branch",
196
+ newSessionId: "job-branch-new",
197
+ branch: true,
198
+ });
199
+ assert.strictEqual(state.completedCompactions["compact-branch"].previousSessionId, "job-branch");
200
+ assert.strictEqual(state.completedCompactions["compact-branch"].newSessionId, "job-branch-new");
201
+ const usage = stateApi.getUsageForSession(state, "alpha", "claude", "job-branch-new");
202
+ assert.strictEqual(usage.turns, 0, "the seed is not billed as a turn");
203
+ assert.strictEqual(usage.compactionCheckpoint.previousSessionId, "job-branch");
204
+ assert.ok(usage.lastCompactedAt > 0, "the new session carries the compaction spacing");
205
+ assert.strictEqual(recorded[0].branch, true);
206
+ assert.strictEqual(recorded[0].contextTokensBefore, 894000, "the context it replaced is the branch's, not the thread's");
207
+ assert.strictEqual(runner.recordCompactionUsage({ ...recorded[0] }, { appendUsageRecord: (entry) => entry }).scope, "job", "the ledger marks a job conversation");
208
+ assert.ok(!("scope" in runner.recordCompactionUsage({ compactReason: "threshold", provider: "claude" }, { appendUsageRecord: (entry) => entry })), "and only that");
209
+ assert.strictEqual(alphaLane().isCompacting, false);
210
+
211
+ const again = stateApi.commitBranchCompaction(state, {
212
+ operationId: "compact-branch", canonicalUserId: state.userId, project: "alpha", provider: "claude",
213
+ previousSessionId: "job-branch", newSessionId: "job-branch-new",
214
+ });
215
+ assert.strictEqual(again.duplicate, true, "a repeated commit is a no-op");
216
+ assert.throws(() => stateApi.commitBranchCompaction(state, {
217
+ operationId: "compact-became-live", canonicalUserId: state.userId, project: "alpha", provider: "claude",
218
+ previousSessionId: "old-claude", newSessionId: "other-new",
219
+ }), /became the live session/, "a branch that is now the live session is the thread's to compact");
220
+ assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "old-claude");
221
+ }
222
+
159
223
  async function failureProbe() {
160
224
  resetFixture("claude");
161
225
  alphaLane().runningProcess = { pid: 123 };
@@ -492,6 +556,7 @@ function restartRecoveryProbe() {
492
556
  assert.strictEqual(typeof runner.createCompactionService, "function");
493
557
  assert.strictEqual(typeof stateApi.commitProviderCompaction, "function");
494
558
  await immutableSuccessProbe();
559
+ await branchCompactionProbe();
495
560
  await failureProbe();
496
561
  await codexRestrictionProbe();
497
562
  await deferredWatchAcrossCompactionProbe();