@inetafrica/open-claudia 3.5.66 → 3.5.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.68
4
+
5
+ 3.5.67 plus a guard on test runs (Sumeet, 19 Oct 11:47 EAT, card "Mac: stop parallel threads from running it out of memory": "Both: checker rule and safety net"). The Mac froze twice that morning (power button held at 10:51 and 11:09 EAT): parallel conversations' test runs wanted about 26 GB on the 16 GB Mac, 12 of those GB from one helper's `npx jest` with no worker cap. The same morning the Chat Spaces bot pod was OOM-killed twice by `jest --maxWorkers=4` under its 3 Gi limit. The rule in lessons.md never reached helpers; the tool hook does.
6
+
7
+ - **Test-run guard** (`core/tool-guard.js`, every Bash call of every conversation and helper). A jest or vitest run is refused, with the exact fix, when it would start more than 2 workers (`--maxWorkers=2`, `--runInBand`, or a project config that sets `maxWorkers` all pass); when another test run is already going on the machine (only one at a time, plus a 30 s claim so two cannot start in the same second); or when free memory is under 40% (macOS `memory_pressure`; on pods the cgroup limit, else MemAvailable). It follows `cd`, package scripts (`npm test` -> `jest --ci`, `npm test -- --maxWorkers=2`), `npx`/`npm exec`/`yarn`/`pnpm exec`, `node .../jest.js` and `bash -c`; greps, reads and quoted text that mention jest stay free. Runs ahead of the relaxed tooling mode (it protects the machine). Refusals log as `kind: "test-run"`, outside the tool-first KPI. `OC_TEST_RUN_MAX_WORKERS`, `OC_TEST_RUN_MIN_FREE_PCT` change the limits; `OC_TEST_RUN_GUARD=off` turns it off. Any error in the guard allows.
8
+ - Proven with a real `claude -p` run: an Agent-tool helper's `npx jest` was refused by the hook before jest started. Not covered: a raw `claude -p` started by hand without the bot's hook settings, and test runners inside script files; on the Mac the memory guard (mac-health `memory-guard`, a launchd agent that stops the newest big test/build run below 20% free) is the net under this.
9
+
10
+ Tests: `test-test-run-guard.js` (worker cap by flag, script and config; one at a time; claim; memory floor; cgroup and meminfo; env limits; fail-open; through the hook).
11
+
12
+ ## 3.5.67
13
+
14
+ 3.5.66 plus card building blocks (APP-31; Sumeet, 16 Oct 08:28 EAT: "ok build and push, ensure you also update open claudia so it knows how to use these, we also need to nudge it to try to discover them as it works"). Pairs with chat-central 5d28c9c (v2 cards: table block; tick, date, slots, rows, board questions), Kazee Chat web and phone 4.6.0.
15
+
16
+ - **Cards built from pieces** (`bin/cards.js`, `core/cards.js`). New examples `due-dates` (rows with a tick, a date and a Close it option per row), `board`, `meeting` (time slots), `table` and `checks`; `open-claudia card pieces` lists every piece. A form answer with the new questions wakes with one line per question, ids in brackets: tick yes/no, a date and what it was, picked slots, rows as counts plus only the rows that changed, board moves with their columns.
17
+ - **Bots reach for them** (`core/system-prompt.js`): the card line names the pieces and the rule: a list for someone to tick, date, pick, approve or move is ONE card built from pieces, never a text list plus a separate question.
18
+ - **Card coach** (`core/card-coach.js`, `core/runner.js`): after a plain Kazee reply that lists 4+ items and asks the reader to decide, the conversation's next turn gets one "Card coach:" line suggesting a card, then it is cleared. At most once per conversation per 6 hours, Kazee only; `OC_CARD_COACH=off` turns it off. Seen 16 Oct 07:40: the Spaces assistant listed 17 proposed due dates as text under a separate card.
19
+
20
+ Tests: `test-card-blocks.js` (examples, wake wording for the three shared fixtures, v 2 wakes like v 1, the card line, the coach).
21
+
3
22
  ## 3.5.66
4
23
 
5
24
  3.5.65 plus typed answers on form questions (APP-30; Sumeet, 15 Oct 18:09 EAT: "a small itallics short circuit for all questions which allows the user to type something instead of selecting from the options"). Pairs with chat-central 630835c, which takes `{ text }` as a choice question's answer.
package/bin/cards.js CHANGED
@@ -58,8 +58,124 @@ const EXAMPLES = {
58
58
  ],
59
59
  footer: "Source: Claude Code logs",
60
60
  },
61
+ // Card building blocks (Sumeet, 16 Oct 2026): a list for someone to tick,
62
+ // date, pick or move is ONE card built from pieces, never a text list plus a
63
+ // question. chat-central stores a card with any new piece as v 2 itself.
64
+ "due-dates": {
65
+ v: 1, kind: "form", kicker: "Spaces · due dates", tag: "6 tasks",
66
+ title: "New due dates I'm proposing",
67
+ body: "Tick the ones that are fine. Tap a date to change it.",
68
+ input: {
69
+ fields: [
70
+ { id: "tasks", type: "rows", label: "Tasks", tick: true, checked: true,
71
+ options: [{ id: "close", label: "Close it" }],
72
+ rows: [
73
+ { id: "clickhouse", label: "ClickHouse public access", detail: "Remove it, or office/VPN only", date: "2026-10-20" },
74
+ { id: "liquid", label: "Liquid Telkom 60/Mb price", date: "2026-10-20" },
75
+ { id: "sanmarco", label: "San Marco agreements", date: "2026-10-20" },
76
+ { id: "mailcow", label: "Mail move to Mailcow", date: "2026-10-23" },
77
+ { id: "galana", label: "Galana floor (on hold)", date: "2026-11-30" },
78
+ { id: "mtn", label: "MTN April invoice", detail: "Credit note cancels it", option: "close" },
79
+ ] },
80
+ { id: "why", type: "text", label: "Anything I should tell the owners?", placeholder: "Optional" },
81
+ ],
82
+ submitLabel: "Send",
83
+ },
84
+ policy: { responders: ["<kazeeUserId>"], mode: "first", results: "hidden" },
85
+ },
86
+ board: {
87
+ v: 1, kind: "form", kicker: "Spaces · this week", tag: "3 tasks",
88
+ title: "Where do these stand?",
89
+ body: "Move each card to where it really is, then press Send once.",
90
+ input: {
91
+ fields: [
92
+ { id: "week", type: "board", label: "This week's board",
93
+ columns: [{ id: "todo", label: "To do" }, { id: "doing", label: "Doing" }, { id: "done", label: "Done" }],
94
+ items: [
95
+ { id: "dog-06", label: "Clean iNet's CRM data", detail: "DOG-06 · 23 Oct", column: "todo" },
96
+ { id: "dog-14", label: "Stop old Zoho history going out as new", detail: "DOG-14 · 20 Oct", column: "todo" },
97
+ { id: "dog-04", label: "Fix the nightly Zoho import", detail: "DOG-04", column: "done" },
98
+ ] },
99
+ ],
100
+ submitLabel: "Send",
101
+ },
102
+ policy: { responders: ["<kazeeUserId>"], mode: "first", results: "hidden" },
103
+ },
104
+ meeting: {
105
+ v: 1, kind: "form", kicker: "Meeting", tag: "Liquid Telkom",
106
+ title: "Call with Liquid Telkom on the 60/Mb price",
107
+ body: "Pick any times that suit you, then press Send once.",
108
+ input: {
109
+ fields: [
110
+ { id: "when", type: "slots", label: "Which times work?", multiple: true,
111
+ slots: [
112
+ { id: "mon10", day: "Mon 19 Oct", time: "10:00" },
113
+ { id: "tue10", day: "Tue 20 Oct", time: "10:00", busy: true },
114
+ { id: "tue14", day: "Tue 20 Oct", time: "14:00" },
115
+ ] },
116
+ { id: "ask", type: "text", label: "Anything I should ask them first?" },
117
+ ],
118
+ submitLabel: "Send",
119
+ },
120
+ policy: { responders: ["<kazeeUserId>"], mode: "first", results: "hidden" },
121
+ },
122
+ table: {
123
+ v: 1, kind: "report", kicker: "Books", title: "Receipts matched this week",
124
+ blocks: [
125
+ { type: "stats", items: [{ value: "KES 16,500", label: "matched" }, { value: "2", label: "receipts" }] },
126
+ { type: "table",
127
+ columns: [{ label: "Receipt" }, { label: "Amount", align: "right" }],
128
+ rows: [["R-1042", "KES 4,500"], ["R-1043", "KES 12,000"]],
129
+ total: ["Total", "KES 16,500"] },
130
+ ],
131
+ footer: "Source: Books",
132
+ },
133
+ checks: {
134
+ v: 1, kind: "form", kicker: "Before it goes out", tag: "Quote",
135
+ title: "Ready to send the quote?",
136
+ body: "Tick, check the date, pick what happens next, then press Send once.",
137
+ input: {
138
+ fields: [
139
+ { id: "agree", type: "tick", label: "I've checked the quote with Hellen", value: false },
140
+ { id: "due", type: "date", label: "New due date", value: "2026-10-23", min: "2026-10-16", max: "2026-12-31" },
141
+ { id: "next", type: "choice", label: "What happens next?",
142
+ options: [{ id: "send", label: "Send it to the customer" }, { id: "hold", label: "Hold it for now" }] },
143
+ ],
144
+ submitLabel: "Send",
145
+ },
146
+ policy: { responders: ["<kazeeUserId>"], mode: "first", results: "hidden" },
147
+ },
61
148
  };
62
149
 
150
+ // Every piece a card is built from, one line each for `card pieces`: where it
151
+ // goes, what it is for, and which `card example` shows it.
152
+ const PIECES = [
153
+ { name: "text", where: "block", use: "a short paragraph of extra detail under the body", example: '{"type":"text","text":"..."}' },
154
+ { name: "stats", where: "block", use: "1-4 big numbers, each with a label and a change", example: "report, table" },
155
+ { name: "bars", where: "block", use: "a small bar chart (2-14 bars, optional reference line)", example: "report" },
156
+ { name: "facts", where: "block", use: "label: value pairs (source, owner, dates)", example: "report" },
157
+ { name: "table", where: "block", use: "rows and columns with an optional bold total (receipts, invoices)", example: "table" },
158
+ { name: "choice", where: "question", use: "pick one or several options; they can type their own answer", example: "form, checks" },
159
+ { name: "rating", where: "question", use: "stars (3-10)", example: "form" },
160
+ { name: "text", where: "question", use: "a written answer", example: "form, due-dates, meeting" },
161
+ { name: "tick", where: "question", use: "one yes/no tick box, a confirmation", example: "checks" },
162
+ { name: "date", where: "question", use: "one date picker, optional min and max", example: "checks" },
163
+ { name: "slots", where: "question", use: "pick meeting times from a day x time grid; busy slots can't be picked", example: "meeting" },
164
+ { name: "rows", where: "question", use: "a list where each row gets a tick, a date and/or option chips (approve, re-date, close items)", example: "due-dates" },
165
+ { name: "board", where: "question", use: "move items between columns (to do, doing, done)", example: "board" },
166
+ ];
167
+
168
+ function piecesText() {
169
+ const width = Math.max(...PIECES.map((p) => p.name.length));
170
+ const line = (p) => ` ${p.name.padEnd(width)} ${p.where.padEnd(8)} ${p.use} (${/^[{]/.test(p.example) ? p.example : `card example ${p.example}`})`;
171
+ return [
172
+ "Card pieces. Blocks go in card.blocks (any kind, at most 6); questions go in input.fields of a kind \"form\" card (at most 8, one Send, one wake-up).",
173
+ ...PIECES.map(line),
174
+ "Kinds: decision, choice, rating, slider, form, report (open-claudia card example <name>).",
175
+ "When a reply would list items for someone to tick, date, pick, approve or move, send ONE card built from these pieces instead of a text list plus a separate question.",
176
+ ].join("\n");
177
+ }
178
+
63
179
  function flag(args, name) {
64
180
  const i = args.indexOf(name);
65
181
  if (i < 0) return undefined;
@@ -88,20 +204,27 @@ const CARD_USAGE = `Usage:
88
204
  open-claudia card send <spec.json | '{json}' | -> [--wake each|first|none] [--content "plain text"]
89
205
  open-claudia card update <messageId> [--close | --open] [--note "text"] [--spec <file|json>]
90
206
  open-claudia card show <messageId>
91
- open-claudia card example <decision|choice|rating|slider|form|report>
207
+ open-claudia card example <decision|choice|rating|slider|form|report|due-dates|board|meeting|table|checks>
208
+ open-claudia card pieces
92
209
  Cards are Kazee Chat only. Answers wake this conversation when --wake is each
93
210
  (default for a card a few named people answer) or first; polls anyone answers
94
- default to none. Spec: chat-central CARDS-V1 (v:1, kind, title, input, policy...).`;
211
+ default to none. Spec: chat-central CARDS-V1 (v:1, kind, title, input, policy...).
212
+ Pieces (card pieces lists them): blocks text, stats, bars, facts, table; form
213
+ questions choice, rating, text, tick, date, slots, rows, board.`;
95
214
 
96
215
  async function runCard(args) {
97
216
  const sub = args.shift();
98
217
  try {
99
218
  if (sub === "example") {
100
219
  const kind = args[0];
101
- if (!EXAMPLES[kind]) fail(`kinds: ${Object.keys(EXAMPLES).join(", ")}`);
220
+ if (!EXAMPLES[kind]) fail(`examples: ${Object.keys(EXAMPLES).join(", ")}`);
102
221
  console.log(JSON.stringify(EXAMPLES[kind], null, 2));
103
222
  return;
104
223
  }
224
+ if (sub === "pieces") {
225
+ console.log(piecesText());
226
+ return;
227
+ }
105
228
  if (sub === "send") {
106
229
  const wake = flag(args, "--wake");
107
230
  const content = flag(args, "--content");
@@ -180,4 +303,4 @@ async function runDecide(args) {
180
303
  }
181
304
  }
182
305
 
183
- module.exports = { runCard, runDecide, EXAMPLES };
306
+ module.exports = { runCard, runDecide, EXAMPLES, PIECES, piecesText, CARD_USAGE };
package/bin/cli.js CHANGED
@@ -632,8 +632,9 @@ Send tools (only work inside an active bot-spawned task):
632
632
  narration; ext picks format; --voice <id> optional)
633
633
  open-claudia transcribe <file> Speech-to-text: print the transcript of an audio/video file
634
634
  open-claudia call [toUserId] [--about "<why>"] Place a live voice call on the current channel (kazee; owner-only); --about tells the opening line why
635
- open-claudia card send <spec|-> Send a Kazee Card (decision, choice, rating, slider, report); answers wake this conversation
636
- open-claudia card update <id> ... Close/open a card you sent, add a note, or replace its spec (card example <kind> for specs)
635
+ open-claudia card send <spec|-> Send a Kazee Card (decision, choice, rating, slider, form, report); answers wake this conversation
636
+ open-claudia card pieces List the pieces a card is built from (table, tick, date, slots, rows, board...) and their examples
637
+ open-claudia card update <id> ... Close/open a card you sent, add a note, or replace its spec (card example <name> for specs)
637
638
  open-claudia decide "<q>" --option id="label" ... Queue a decision card for the owner (one at a time; decide list|cancel)
638
639
  open-claudia teach list|show|finish|cancel <id> Browser recordings from Teach me; finish deletes the pictures, cancel the whole recording
639
640
 
@@ -0,0 +1,107 @@
1
+ // The card coach (Card building blocks v1). Sumeet, 16 Oct 2026 08:28 EAT:
2
+ // "we also need to nudge it to try to discover them as it works so it tries
3
+ // to use them to ensure the ux is good". That morning a bot proposed six new
4
+ // due dates as a text list and asked him to "answer the card" separately.
5
+ //
6
+ // After the bot posts a plain Kazee Chat reply (not a card) that lists 4+
7
+ // items AND asks the reader to decide, this arms a one-shot hint for that
8
+ // conversation. The conversation's next turn gets ONE line in its per-turn
9
+ // context (never shown to the person), then the hint is gone. At most once
10
+ // per conversation per 6 hours. OC_CARD_COACH=off switches it off.
11
+ //
12
+ // card-coach.json "<adapter>|<channel>|<thread>" -> { pending, items,
13
+ // armedAt, shownAt }
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const { CONFIG_DIR } = require("./config");
18
+ const { atomicWriteFileSync } = require("./fsutil");
19
+
20
+ const COACH_FILE = () => process.env.OC_CARD_COACH_FILE || path.join(CONFIG_DIR, "card-coach.json");
21
+ const MIN_ITEMS = 4;
22
+ const WINDOW_MS = 6 * 60 * 60 * 1000;
23
+ const KEEP_MS = 7 * 24 * 60 * 60 * 1000;
24
+
25
+ // A list line: "- x", "* x", "• x", "+ x", "1. x", "1) x".
26
+ const LIST_ITEM = /^\s*(?:[-*•+]|\d{1,2}[.)])\s+\S/;
27
+ // Asking the reader to decide: a question, or one of these words.
28
+ const QUESTION = /\?(?=\s|$|["'”’)\]*_])/;
29
+ const ASK_WORDS = /\b(?:approve|pick|choose|confirm|decide|tick|due|dates?|which)\b|\banswer the card\b/i;
30
+
31
+ function enabled() {
32
+ return !/^(off|false|0|no|disabled)$/i.test(String(process.env.OC_CARD_COACH || "").trim());
33
+ }
34
+
35
+ // Code blocks are output, not a list for someone to decide on.
36
+ function withoutCode(text) {
37
+ return String(text || "").replace(/```[\s\S]*?```/g, "\n");
38
+ }
39
+
40
+ /** { items } when the text lists 4+ items and asks the reader to decide, else null. */
41
+ function detect(text) {
42
+ const t = withoutCode(text);
43
+ const items = t.split(/\r?\n/).filter((line) => LIST_ITEM.test(line)).length;
44
+ if (items < MIN_ITEMS) return null;
45
+ if (!QUESTION.test(t) && !ASK_WORDS.test(t)) return null;
46
+ return { items };
47
+ }
48
+
49
+ function hintLine(items) {
50
+ return `Card coach: your last message listed ${items} items for the reader to decide in plain text. One card built from pieces (rows with ticks, dates or options; board; time slots; table) lets them answer with one Send. See open-claudia card pieces.`;
51
+ }
52
+
53
+ function conversationKey({ adapterId, channelId, threadId }) {
54
+ return `${adapterId || "?"}|${channelId}|${threadId || ""}`;
55
+ }
56
+
57
+ function load() {
58
+ try {
59
+ const data = JSON.parse(fs.readFileSync(COACH_FILE(), "utf8"));
60
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
61
+ } catch (_) { return {}; }
62
+ }
63
+
64
+ function save(data, now) {
65
+ const kept = Object.entries(data).filter(([, e]) => e && now - lastAt(e) <= KEEP_MS);
66
+ const file = COACH_FILE();
67
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
68
+ atomicWriteFileSync(file, `${JSON.stringify(Object.fromEntries(kept), null, 2)}\n`, { mode: 0o600 });
69
+ }
70
+
71
+ function lastAt(entry) {
72
+ return Math.max(Date.parse(entry?.armedAt || 0) || 0, Date.parse(entry?.shownAt || 0) || 0);
73
+ }
74
+
75
+ /**
76
+ * The bot just posted `text` as a plain reply. Arms the hint for this
77
+ * conversation when the reply lists items to decide in words. Returns
78
+ * { armed: true, items } or { armed: false, reason }.
79
+ */
80
+ function noteReply({ text, transport, adapterId = null, channelId, threadId = null, sentCard = false, now = Date.now() } = {}) {
81
+ if (!enabled()) return { armed: false, reason: "off" };
82
+ if (transport !== "kazee") return { armed: false, reason: "not kazee" };
83
+ if (!channelId) return { armed: false, reason: "no conversation" };
84
+ if (sentCard) return { armed: false, reason: "sent a card" };
85
+ const found = detect(text);
86
+ if (!found) return { armed: false, reason: "no list to decide" };
87
+ const data = load();
88
+ const key = conversationKey({ adapterId, channelId, threadId });
89
+ if (data[key] && now - lastAt(data[key]) < WINDOW_MS) return { armed: false, reason: "coached recently" };
90
+ data[key] = { pending: true, items: found.items, armedAt: new Date(now).toISOString(), shownAt: data[key]?.shownAt || null };
91
+ save(data, now);
92
+ return { armed: true, items: found.items };
93
+ }
94
+
95
+ /** The one hint line for this conversation's turn, cleared as it is taken; "" when none. */
96
+ function takeHint({ transport, adapterId = null, channelId, threadId = null, now = Date.now() } = {}) {
97
+ if (!enabled() || transport !== "kazee" || !channelId) return "";
98
+ const data = load();
99
+ const key = conversationKey({ adapterId, channelId, threadId });
100
+ const entry = data[key];
101
+ if (!entry || !entry.pending) return "";
102
+ data[key] = { ...entry, pending: false, shownAt: new Date(now).toISOString() };
103
+ save(data, now);
104
+ return hintLine(entry.items);
105
+ }
106
+
107
+ module.exports = { COACH_FILE, WINDOW_MS, MIN_ITEMS, detect, hintLine, noteReply, takeHint, enabled };
package/core/cards.js CHANGED
@@ -104,6 +104,22 @@ function getCard(messageId) {
104
104
  return loadCards()[String(messageId)] || null;
105
105
  }
106
106
 
107
+ /**
108
+ * Whether a turn sent a card: one its run posted, or one posted in its
109
+ * conversation (channel and thread) since the turn began. The card coach
110
+ * leaves a turn alone when it did.
111
+ */
112
+ function cardSentDuringRun({ runId = null, channelId = null, threadId = null, sinceMs = null } = {}) {
113
+ return Object.values(loadCards()).some((c) => {
114
+ if (!c || !c.origin) return false;
115
+ if (runId && c.origin.originRunId && String(c.origin.originRunId) === String(runId)) return true;
116
+ return Number.isFinite(sinceMs) && channelId != null
117
+ && String(c.origin.channelId || "") === String(channelId)
118
+ && String(c.origin.threadId || "") === String(threadId || "")
119
+ && Date.parse(c.sentAt || 0) >= sinceMs;
120
+ });
121
+ }
122
+
107
123
  function updateCardRecord(messageId, patch) {
108
124
  const cards = loadCards();
109
125
  const cur = cards[String(messageId)];
@@ -233,9 +249,13 @@ function totalsLine(card) {
233
249
  const parts = [];
234
250
  if (Number.isFinite(state.responses)) parts.push(`${state.responses} answer${state.responses === 1 ? "" : "s"} so far`);
235
251
  if (state.counts && card?.kind === "form") {
236
- // Form totals are keyed "<question id>:<option id or rating>".
252
+ // Form totals are keyed "<question id>:<option id or rating>"; a time
253
+ // slot's by its slot id ("Mon 19 Oct 10:00 2"), a tick's "yes" / "no".
237
254
  (card.input?.fields || []).forEach((f, i) => {
238
- const byId = Object.fromEntries((f.options || []).map((o) => [o.id, o.label]));
255
+ const byId = Object.fromEntries([
256
+ ...(f.options || []).map((o) => [o.id, o.label]),
257
+ ...(f.type === "slots" ? (f.slots || []).map((s) => [s.id, slotLabel(s)]) : []),
258
+ ]);
239
259
  const counts = Object.entries(state.counts)
240
260
  .filter(([key, n]) => n > 0 && key.startsWith(`${f.id}:`))
241
261
  .map(([key, n]) => `${byId[key.slice(f.id.length + 1)] || key.slice(f.id.length + 1)} ${n}`);
@@ -260,10 +280,118 @@ function lateAnswerLine(sentAt, now = Date.now()) {
260
280
  return `This was asked ${ageWords(age)} ago. Before acting on it, check it still applies; if things have moved on, say so briefly instead of acting.`;
261
281
  }
262
282
 
283
+ // ---------------------------------------------------------------------------
284
+ // Card building blocks (SPEC-BLOCKS, 16 Oct 2026): tick, date, slots, rows and
285
+ // board questions. The wake-up names every id in brackets so the bot can map
286
+ // an answer back to what it proposed; rows and boards list only what changed.
287
+
288
+ const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
289
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
290
+
291
+ /** "2026-10-26" -> "Mon 26 Oct 2026" (always with the year); anything else as given. */
292
+ function dayLabel(ymd) {
293
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(ymd || ""));
294
+ if (!m) return String(ymd || "");
295
+ const t = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
296
+ if (Number.isNaN(t.getTime())) return String(ymd);
297
+ return `${WEEKDAYS[t.getUTCDay()]} ${t.getUTCDate()} ${MONTHS[t.getUTCMonth()]} ${t.getUTCFullYear()}`;
298
+ }
299
+
300
+ function slotLabel(s) {
301
+ return [s?.day, s?.time].filter(Boolean).map(String).join(" ") || String(s?.id || "");
302
+ }
303
+
304
+ function plain(obj) {
305
+ return obj && typeof obj === "object" && !Array.isArray(obj) ? obj : null;
306
+ }
307
+
308
+ /** A row's starting state: its own `checked` over the question's, its date, its option. */
309
+ function rowStart(f, row) {
310
+ return {
311
+ checked: typeof row.checked === "boolean" ? row.checked : f.checked === true,
312
+ date: row.date || null,
313
+ option: row.option || null,
314
+ };
315
+ }
316
+
317
+ function rowsAnswer(f, v) {
318
+ const sent = plain(v) || {};
319
+ const rows = Array.isArray(f.rows) ? f.rows : [];
320
+ const tick = f.tick !== false;
321
+ const optionLabel = Object.fromEntries((f.options || []).map((o) => [o.id, o.label]));
322
+ const counts = { ticked: 0, unticked: 0, dates: 0 };
323
+ const picked = new Map(); // option label -> rows that have it now
324
+ const changed = [];
325
+ for (const row of rows) {
326
+ const start = rowStart(f, row);
327
+ // The server fills every row and keeps `option` only when one is picked,
328
+ // so a row that is there without one has none (a cleared starting pick).
329
+ const got = plain(sent[row.id]);
330
+ const now = {
331
+ checked: typeof got?.checked === "boolean" ? got.checked : start.checked,
332
+ date: typeof got?.date === "string" && got.date ? got.date : start.date,
333
+ option: !got ? start.option : (typeof got.option === "string" && got.option ? got.option : null),
334
+ };
335
+ if (tick) counts[now.checked ? "ticked" : "unticked"] += 1;
336
+ if (now.date !== start.date) counts.dates += 1;
337
+ if (now.option) {
338
+ const label = optionLabel[now.option] || now.option;
339
+ picked.set(label, (picked.get(label) || 0) + 1);
340
+ }
341
+ const parts = [];
342
+ if (tick && now.checked !== start.checked) parts.push(now.checked ? "ticked" : "unticked");
343
+ if (now.date !== start.date) {
344
+ parts.push(start.date ? `date ${dayLabel(start.date)} → ${dayLabel(now.date)}` : `date ${dayLabel(now.date)} (added)`);
345
+ }
346
+ if (now.option !== start.option) {
347
+ parts.push(now.option
348
+ ? `${optionLabel[now.option] || now.option}${start.option ? ` (was ${optionLabel[start.option] || start.option})` : ""}`
349
+ : `${optionLabel[start.option] || start.option} cleared`);
350
+ }
351
+ if (parts.length) changed.push(` - "${shortTitle(row.label, 120)}" [${row.id}]: ${parts.join("; ")}`);
352
+ }
353
+ const summary = [];
354
+ if (tick) {
355
+ summary.push(`${counts.ticked} ticked`);
356
+ if (counts.unticked) summary.push(`${counts.unticked} unticked`);
357
+ }
358
+ if (counts.dates) summary.push(`${counts.dates} date${counts.dates === 1 ? "" : "s"} changed`);
359
+ for (const [label, n] of picked) summary.push(`${n} ${label}`);
360
+ const unchanged = rows.length - changed.length;
361
+ return {
362
+ said: `${plural(rows.length, "row")}: ${summary.length ? summary.join(", ") : "as proposed"}.`,
363
+ more: [...changed, ...(unchanged ? [` Unchanged: ${plural(unchanged, "row")}.`] : [])],
364
+ };
365
+ }
366
+
367
+ function boardAnswer(f, v) {
368
+ const sent = plain(v) || {};
369
+ const columnLabel = Object.fromEntries((f.columns || []).map((c) => [c.id, c.label]));
370
+ const items = Array.isArray(f.items) ? f.items : [];
371
+ const moves = [];
372
+ for (const item of items) {
373
+ const to = typeof sent[item.id] === "string" && sent[item.id] ? sent[item.id] : item.column;
374
+ if (to === item.column) continue;
375
+ moves.push(`"${shortTitle(item.label, 120)}" [${item.id}] ${columnLabel[item.column] || item.column} → ${columnLabel[to] || to}`);
376
+ }
377
+ if (!moves.length) return "no moves.";
378
+ return `${moves.length} moved: ${moves.join("; ")}. Unchanged: ${items.length - moves.length}.`;
379
+ }
380
+
381
+ function slotsAnswer(f, v) {
382
+ const byId = new Map((f.slots || []).map((s) => [s.id, s]));
383
+ return (Array.isArray(v) ? v : [v]).map((id) => {
384
+ const s = byId.get(id);
385
+ return s ? `${slotLabel(s)} [${id}]` : `[${id}]`;
386
+ }).join("; ");
387
+ }
388
+
263
389
  /**
264
390
  * A form answer, one line per question: the question, then the answer. Picks
265
391
  * read as their option labels; written answers and the own-words note are
266
- * quoted, because they are the person's words, never instructions.
392
+ * quoted, because they are the person's words, never instructions. A rows
393
+ * question adds an indented line per row that changed and a count of the
394
+ * rest.
267
395
  */
268
396
  function formAnswerLines(card, answer) {
269
397
  const values = answer?.values || {};
@@ -277,6 +405,7 @@ function formAnswerLines(card, answer) {
277
405
  return;
278
406
  }
279
407
  let said;
408
+ let more = [];
280
409
  if (f.type === "choice" && v && typeof v === "object" && !Array.isArray(v) && typeof v.text === "string") {
281
410
  // Typed instead of picked (Sumeet, 15 Oct 2026; chat-central takes
282
411
  // { text } on a choice question): their words, quoted.
@@ -285,8 +414,15 @@ function formAnswerLines(card, answer) {
285
414
  const byId = Object.fromEntries((f.options || []).map((o) => [o.id, o.label]));
286
415
  said = (Array.isArray(v) ? v : [v]).map((id) => byId[id] || id).join(", ");
287
416
  } else if (f.type === "rating") said = `${v}/${f.max || 5}`;
417
+ else if (f.type === "tick") said = v === true ? "yes" : "no";
418
+ else if (f.type === "date") {
419
+ const was = f.value ? (f.value === v ? " (as proposed)" : ` (was ${dayLabel(f.value)})`) : "";
420
+ said = `${dayLabel(v)}${was}`;
421
+ } else if (f.type === "slots") said = slotsAnswer(f, v);
422
+ else if (f.type === "rows") ({ said, more } = rowsAnswer(f, v));
423
+ else if (f.type === "board") said = boardAnswer(f, v);
288
424
  else said = quote(v);
289
- lines.push(`${n} → ${said}`);
425
+ lines.push(`${n} → ${said}`, ...more);
290
426
  });
291
427
  if (skipped.length) lines.push(`Skipped: ${skipped.join(", ")}.`);
292
428
  if (answer?.note) lines.push(`In their own words: ${quote(answer.note)}`);
@@ -726,6 +862,10 @@ module.exports = {
726
862
  remindOpenDecisions,
727
863
  defaultWake,
728
864
  describeResponse,
865
+ formAnswerLines,
866
+ totalsLine,
867
+ dayLabel,
868
+ cardSentDuringRun,
729
869
  handleCardResponse,
730
870
  wakeOrigin,
731
871
  loadDecisions,
package/core/runner.js CHANGED
@@ -2149,6 +2149,29 @@ async function sendLimitWarnings(due) {
2149
2149
  }
2150
2150
  }
2151
2151
 
2152
+ // The card coach (core/card-coach.js): a plain Kazee Chat reply that lists 4+
2153
+ // items for the reader to decide arms a one-line hint for this conversation's
2154
+ // next turn, unless the turn sent a card. Never on a live call; never breaks
2155
+ // delivery.
2156
+ function noteCardCoach(text, runContext) {
2157
+ if (runContext.liveCall) return;
2158
+ try {
2159
+ const adapter = currentAdapter();
2160
+ if (adapter?.type !== "kazee") return;
2161
+ const channelId = currentChannelId() || runContext.channelId;
2162
+ const threadId = runContext.threadId || currentThreadId() || null;
2163
+ const sentCard = require("./cards").cardSentDuringRun({
2164
+ runId: runContext.runId, channelId, threadId, sinceMs: runContext.admittedAt,
2165
+ });
2166
+ const out = require("./card-coach").noteReply({
2167
+ text, transport: adapter.type, adapterId: adapter.id, channelId, threadId, sentCard,
2168
+ });
2169
+ if (out.armed) console.log(`[card-coach] ${channelId}${threadId ? `#${threadId}` : ""}: reply listed ${out.items} items to decide in text; hint armed for the next turn`);
2170
+ } catch (error) {
2171
+ console.warn(`[card-coach] skipped: ${redactSensitive(String(error.message || error))}`);
2172
+ }
2173
+ }
2174
+
2152
2175
  async function deliverForegroundResult(result, runContext, state = currentState()) {
2153
2176
  const lane = laneForRun(state, runContext);
2154
2177
  if (runContext.lastInputWasVoice) lane.lastInputWasVoice = false;
@@ -2224,6 +2247,7 @@ async function deliverForegroundResult(result, runContext, state = currentState(
2224
2247
  if (!sent.ok) sent = await send(chunks[index], telegramHtmlOpts({ botTags: true }));
2225
2248
  if (!sent.ok) throw new Error(`failed to deliver response chunk ${index + 1}`);
2226
2249
  }
2250
+ if (result.ok) noteCardCoach(finalText, runContext);
2227
2251
  if (limitWarnings.length) await sendLimitWarnings(limitWarnings);
2228
2252
 
2229
2253
  const shouldSpeak = shouldReplyWithVoice(VOICE_REPLY_MODE, runContext.lastInputWasVoice);
@@ -299,7 +299,7 @@ Reply normally in your final answer. To send a file, image, or voice clip back t
299
299
  - \`open-claudia send-photo <path> [caption]\` — image with inline preview
300
300
  - \`open-claudia send-voice <path>\` — ogg/opus voice note
301
301
  - \`open-claudia say "<text>"\` — synthesize the text and send it as a voice note (use this when asked for a voice reply)
302
- - \`open-claudia card send <spec.json|->\` — (Kazee Chat) an interactive or data card: decision, choice (poll), rating, slider, form (several questions, one Send), or report (stat tiles, bar chart, facts). Answers come back to this conversation as a wake-up. Two or more questions for the same person go in ONE form card, never several cards. \`open-claudia card example <kind>\` prints a spec; \`card update <id> --close | --note "..."\` changes a card you sent.
302
+ - \`open-claudia card send <spec.json|->\` — (Kazee Chat) a decision, choice (poll), rating, slider, form or report card; answers wake this conversation. Cards are built from pieces: blocks (text, stats, bars, facts, table) and form questions (choice, rating, text, tick, date, slots, rows with a tick/date/option per row, board). When a reply would list items for someone to tick, date, pick, approve or move, or asks one person 2+ questions, send ONE card built from pieces instead of a text list plus a separate question; \`open-claudia card pieces\` lists them, \`card example <name>\` prints a spec, \`card update <id> --close | --note "..."\` changes one you sent.
303
303
  - \`open-claudia decide "<question>" --option yes="..." --option no="..." [--pick yes] [--body "why"]\` — (owner, Kazee Chat) queue a decision for the owner as a card: one is out at a time, his answer wakes this conversation. Prefer it to asking a yes/no question in text when work waits on his decision.
304
304
  Never print or embed bot tokens in prompts, commands, logs, or messages.
305
305
  ${hasVoice || hasEars ? `
@@ -602,6 +602,7 @@ function buildSpeakerContextBlock(opts = {}) {
602
602
  // (tasks.formatConversationSection, capped). Later turns get a one-line
603
603
  // count; the agent runs `open-claudia task list` when it needs detail.
604
604
  const taskTreeInjectedFor = new Map(); // `${adapterId}:${conversationKey}` -> captured run identity
605
+ const CARD_COACH_PURPOSES = new Set(["foreground", "scheduled", "passive-reply"]);
605
606
 
606
607
  function buildDynamicContextBlock(opts = {}) {
607
608
  const state = promptState(opts);
@@ -634,6 +635,19 @@ function buildDynamicContextBlock(opts = {}) {
634
635
  });
635
636
  if (section) lines.push("", section);
636
637
  } catch (_) { /* no open decisions shown */ }
638
+ // The card coach's one-shot hint (core/card-coach.js): the last reply in
639
+ // this conversation listed items to decide in plain text. Only a turn that
640
+ // answers in the chat takes it (never a capture run or a live call), and
641
+ // taking it clears it.
642
+ const run = opts.runContext || {};
643
+ if (!run.liveCall && CARD_COACH_PURPOSES.has(run.purpose || "foreground")) {
644
+ try {
645
+ const hint = require("./card-coach").takeHint({
646
+ transport: adapter.type, adapterId: adapter.id, channelId, threadId: run.threadId || currentThreadId() || null,
647
+ });
648
+ if (hint) lines.push("", hint);
649
+ } catch (_) { /* no hint shown */ }
650
+ }
637
651
  const pending = tasksStore.pendingSummary(adapter.id, channelId);
638
652
  if (pending.length > 0) {
639
653
  const key = dedupeScope(adapter, channelId, opts);
@@ -994,6 +994,375 @@ function browserDriverDenial(command, now = Date.now()) {
994
994
  }
995
995
  }
996
996
 
997
+ // ── test runs: worker cap, one at a time, enough free memory ──────────────
998
+ // 19 Oct 2026: Sumeet's 16 GB Mac froze twice (power button held at 10:51 and
999
+ // 11:09 EAT) when parallel conversations' test runs wanted ~26 GB: a helper's
1000
+ // `npx jest` with no worker cap started 12 workers of ~1 GB each next to
1001
+ // another thread's jest. The same morning the Chat Spaces bot pod was
1002
+ // OOM-killed twice by `jest --maxWorkers=4` under its 3 Gi limit. A rule in
1003
+ // lessons.md did not reach helpers; this hook does, because every Bash call
1004
+ // of every conversation and helper passes here. So a jest/vitest run must cap
1005
+ // its workers (2 by default), only one runs at a time on the machine, and
1006
+ // none starts while free memory is already low. On the Mac, mac-health
1007
+ // memory-guard is the net under this for anything that slips past (a script
1008
+ // file, a runner this does not know). Deliberately ahead of the relaxed
1009
+ // tooling-mode switch: it protects the machine, it is not a workflow
1010
+ // preference. Any error here allows: the net still holds.
1011
+ const TEST_RUNNERS = new Set(["jest", "vitest"]);
1012
+ const PACKAGE_MANAGERS = new Set(["npm", "yarn", "pnpm", "bun"]);
1013
+ const TEST_RUN_CLAIM_MS = 30000;
1014
+ const NODE_VALUE_FLAGS = new Set(["-r", "--require", "--import", "--loader", "--experimental-loader", "--env-file", "--conditions", "-C"]);
1015
+ const NOT_A_RUN_FLAGS = new Set(["--listTests", "--showConfig", "--version", "-v", "--help", "-h", "--clearCache"]);
1016
+ // A process that IS a test run (not a shell or grep that mentions one).
1017
+ const TEST_PROCESS_RE = /(?:^|[\s/])(?:jest|jest-cli|vitest)(?:\.c?m?js)?(?=\s|$)|jest-worker\/|vitest\/dist\/|tinypool\//;
1018
+ const TEST_PROCESS_BINS = new Set(["node", "nodejs", "jest", "vitest", "npm", "npx", "yarn", "pnpm", "pnpx", "bun", "bunx"]);
1019
+
1020
+ function positiveInt(value, fallback) {
1021
+ const n = Number(value);
1022
+ return Number.isInteger(n) && n > 0 ? n : fallback;
1023
+ }
1024
+
1025
+ function testRunLimits() {
1026
+ return {
1027
+ maxWorkers: positiveInt(process.env.OC_TEST_RUN_MAX_WORKERS, 2),
1028
+ minFreePct: positiveInt(process.env.OC_TEST_RUN_MIN_FREE_PCT, 40),
1029
+ claimFile: process.env.OC_TEST_RUN_CLAIM || path.join(CONFIG_DIR, "test-run.claim.json"),
1030
+ };
1031
+ }
1032
+
1033
+ function readText(file, limit = 256 * 1024) {
1034
+ try {
1035
+ const st = fs.statSync(file);
1036
+ if (!st.isFile() || st.size > limit) return null;
1037
+ return fs.readFileSync(file, "utf8");
1038
+ } catch (e) { return null; }
1039
+ }
1040
+
1041
+ function readPackageJson(dir) {
1042
+ const text = readText(path.join(dir, "package.json"));
1043
+ if (text === null) return null;
1044
+ try { return JSON.parse(text); } catch (e) { return null; }
1045
+ }
1046
+
1047
+ // jest/vitest by name or path: jest, npx jest, node_modules/.bin/jest,
1048
+ // node [flags] node_modules/jest/bin/jest.js, and the package managers' own
1049
+ // ways of running a binary (npm exec, yarn, pnpm exec/dlx, bun x).
1050
+ function directTestRunner(words, at, pkg) {
1051
+ const name = baseName(words[at]).replace(/\.c?m?js$/, "");
1052
+ if (TEST_RUNNERS.has(name)) return { runner: name, args: words.slice(at + 1) };
1053
+ if (name === "node" || name === "nodejs") {
1054
+ let j = at + 1;
1055
+ while (j < words.length && words[j].startsWith("-")) j += NODE_VALUE_FLAGS.has(words[j]) ? 2 : 1;
1056
+ const script = words[j] ? baseName(words[j]).replace(/\.c?m?js$/, "") : "";
1057
+ if (TEST_RUNNERS.has(script)) return { runner: script, args: words.slice(j + 1) };
1058
+ return null;
1059
+ }
1060
+ const sub = words[at + 1];
1061
+ const bin = (k) => {
1062
+ let j = k;
1063
+ if (words[j] === "--") j += 1;
1064
+ const runner = words[j] ? baseName(words[j]) : "";
1065
+ return TEST_RUNNERS.has(runner) ? { runner, args: words.slice(j + 1) } : null;
1066
+ };
1067
+ if (name === "npm" && (sub === "exec" || sub === "x")) return bin(at + 2);
1068
+ if (name === "pnpm" && (sub === "exec" || sub === "dlx")) return bin(at + 2);
1069
+ if (name === "bun" && sub === "x") return bin(at + 2);
1070
+ if (name === "yarn" && sub === "exec") return bin(at + 2);
1071
+ const scripts = (pkg && pkg.scripts) || {};
1072
+ if ((name === "yarn" || name === "pnpm") && TEST_RUNNERS.has(sub) && !scripts[sub]) return bin(at + 1);
1073
+ return null;
1074
+ }
1075
+
1076
+ // A package script that runs tests: npm test / npm run <s> -- args,
1077
+ // yarn [run] <s> args, pnpm [run] <s> args, bun run <s> args. Returns the
1078
+ // script's text with the extra args appended, or null.
1079
+ function packageScriptRun(words, at, pkg) {
1080
+ const name = baseName(words[at]);
1081
+ if (!PACKAGE_MANAGERS.has(name) || !pkg || !pkg.scripts) return null;
1082
+ let j = at + 1;
1083
+ while (j < words.length && words[j].startsWith("-")) j += 1; // --silent, -s ...
1084
+ const sub = words[j];
1085
+ if (!sub) return null;
1086
+ let script = null;
1087
+ let extra = [];
1088
+ const after = (k) => {
1089
+ const rest = words.slice(k);
1090
+ if (name === "npm") {
1091
+ const dash = rest.indexOf("--");
1092
+ return dash < 0 ? [] : rest.slice(dash + 1);
1093
+ }
1094
+ return rest[0] === "--" ? rest.slice(1) : rest;
1095
+ };
1096
+ if (["test", "t", "tst"].includes(sub) && name !== "bun") { script = "test"; extra = after(j + 1); }
1097
+ else if (["run", "run-script", "rum", "urn"].includes(sub)) { script = words[j + 1]; extra = after(j + 2); }
1098
+ else if ((name === "yarn" || name === "pnpm") && pkg.scripts[sub]) { script = sub; extra = after(j + 1); }
1099
+ if (!script || typeof pkg.scripts[script] !== "string") return null;
1100
+ const quoted = extra.map((arg) => require("./providers/hook-command").shellQuote(arg));
1101
+ // How to pass a flag through to the script, for the deny message.
1102
+ const example = name === "npm"
1103
+ ? (script === "test" ? "npm test --" : `npm run ${script} --`)
1104
+ : name === "bun" ? `bun run ${script}` : `${name} ${script}`;
1105
+ return { script, example, text: [pkg.scripts[script], ...quoted].join(" ") };
1106
+ }
1107
+
1108
+ // Every jest/vitest run a command line would start: [{ runner, args, cwd }].
1109
+ // Follows `cd` within the line, package scripts (npm test → "jest --ci"),
1110
+ // and what a shell runs from a string (bash -c, eval, $(...)).
1111
+ function testRunInvocations(command, cwd, depth = 0) {
1112
+ const runs = [];
1113
+ if (depth > 4 || !/jest|vitest|test|run|\bt\b|exec|yarn|pnpm/.test(String(command || ""))) return runs;
1114
+ let dir = path.resolve(String(cwd || process.cwd()));
1115
+ const { commands, nested } = shellCommands(command);
1116
+ const scripts = [...nested];
1117
+ for (const { words, input } of commands) {
1118
+ const at = commandWordIndex(words, scripts);
1119
+ if (at < 0) continue;
1120
+ const name = baseName(words[at]);
1121
+ if (name === "cd" || name === "pushd") {
1122
+ const target = words.slice(at + 1).find((w) => !w.startsWith("-"));
1123
+ dir = target ? path.resolve(dir, expandHome(target)) : os.homedir();
1124
+ continue;
1125
+ }
1126
+ if (name === "eval") { scripts.push(words.slice(at + 1).join(" ")); continue; }
1127
+ if (SHELLS.has(name)) {
1128
+ const flag = words.findIndex((w, k) => k > at && /^-[A-Za-z]*c[A-Za-z]*$/.test(w));
1129
+ if (flag > 0 && words[flag + 1] !== undefined) runs.push(...testRunInvocations(words[flag + 1], dir, depth + 1));
1130
+ else for (const text of input) runs.push(...testRunInvocations(text, dir, depth + 1));
1131
+ continue;
1132
+ }
1133
+ const pkg = PACKAGE_MANAGERS.has(name) ? readPackageJson(dir) : null;
1134
+ const direct = directTestRunner(words, at, pkg);
1135
+ if (direct) { runs.push({ ...direct, cwd: dir }); continue; }
1136
+ const scriptRun = packageScriptRun(words, at, pkg);
1137
+ if (scriptRun) {
1138
+ for (const run of testRunInvocations(scriptRun.text, dir, depth + 1)) runs.push({ ...run, via: scriptRun.example });
1139
+ }
1140
+ }
1141
+ for (const script of scripts) runs.push(...testRunInvocations(script, dir, depth + 1));
1142
+ return runs;
1143
+ }
1144
+
1145
+ function flagValue(args, names) {
1146
+ for (let k = 0; k < args.length; k += 1) {
1147
+ for (const name of names) {
1148
+ if (args[k] === name && args[k + 1] !== undefined) return args[k + 1];
1149
+ if (args[k].startsWith(`${name}=`)) return args[k].slice(name.length + 1);
1150
+ }
1151
+ }
1152
+ return null;
1153
+ }
1154
+
1155
+ function workersFrom(value, cpus) {
1156
+ const text = String(value).trim().replace(/^['"]|['"]$/g, "");
1157
+ const pct = text.match(/^(\d+(?:\.\d+)?)%$/);
1158
+ if (pct) return Math.max(1, Math.floor((cpus * Number(pct[1])) / 100));
1159
+ const n = Number(text);
1160
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
1161
+ }
1162
+
1163
+ const CONFIG_CAP_RE = {
1164
+ jest: /\bmaxWorkers\b\s*[:=]?\s*(['"]?\d+(?:\.\d+)?%?['"]?)?/,
1165
+ vitest: /\b(maxWorkers|maxThreads|maxForks|singleThread|singleFork|fileParallelism)\b\s*[:=]?\s*(['"]?[\w.%]+['"]?)?/,
1166
+ };
1167
+ const DEFAULT_CONFIGS = {
1168
+ jest: ["jest.config.js", "jest.config.ts", "jest.config.mjs", "jest.config.cjs", "jest.config.json"],
1169
+ vitest: ["vitest.config.ts", "vitest.config.mts", "vitest.config.js", "vitest.config.mjs", "vitest.config.cjs", "vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"],
1170
+ };
1171
+
1172
+ // The project's own config caps the workers: { workers } (null = set by code
1173
+ // we cannot read, trusted) or null when the config sets no cap.
1174
+ function configCap(runner, args, cwd, cpus) {
1175
+ const files = [];
1176
+ const explicit = flagValue(args, ["--config", "-c"]);
1177
+ if (explicit) files.push(path.resolve(cwd, expandHome(explicit)));
1178
+ else files.push(...DEFAULT_CONFIGS[runner].map((f) => path.join(cwd, f)));
1179
+ for (const file of files) {
1180
+ const text = readText(file);
1181
+ if (text === null) continue;
1182
+ const m = text.match(CONFIG_CAP_RE[runner]);
1183
+ if (!m) continue;
1184
+ if (runner === "vitest") {
1185
+ if (/^single/.test(m[1])) return { workers: /false/.test(m[2] || "") ? null : 1 };
1186
+ if (m[1] === "fileParallelism") return /false/.test(m[2] || "") ? { workers: 1 } : null;
1187
+ return { workers: m[2] ? workersFrom(m[2], cpus) : null };
1188
+ }
1189
+ return { workers: m[1] ? workersFrom(m[1], cpus) : null };
1190
+ }
1191
+ if (runner === "jest" && !explicit) {
1192
+ const pkg = readPackageJson(cwd);
1193
+ if (pkg && pkg.jest && typeof pkg.jest === "object" && pkg.jest.maxWorkers !== undefined) {
1194
+ return { workers: workersFrom(pkg.jest.maxWorkers, cpus) };
1195
+ }
1196
+ }
1197
+ return null;
1198
+ }
1199
+
1200
+ // How many workers a run will start: { notARun } | { workers, source }.
1201
+ // workers null = capped by project code we cannot evaluate (trusted).
1202
+ function testRunWorkers(run, cpus) {
1203
+ const { runner, args } = run;
1204
+ if (args.some((a) => NOT_A_RUN_FLAGS.has(a)) || (runner === "vitest" && ["list", "init"].includes(args[0]))) {
1205
+ return { notARun: true };
1206
+ }
1207
+ if (runner === "jest") {
1208
+ if (args.includes("--runInBand") || args.includes("-i") || args.includes("--run-in-band")) return { workers: 1, source: "--runInBand" };
1209
+ const value = flagValue(args, ["--maxWorkers", "--max-workers", "-w"]);
1210
+ if (value !== null) return { workers: workersFrom(value, cpus) ?? cpus, source: "--maxWorkers" };
1211
+ } else {
1212
+ if (args.includes("--no-file-parallelism") || args.includes("--fileParallelism=false")
1213
+ || args.includes("--poolOptions.threads.singleThread") || args.includes("--poolOptions.forks.singleFork")) {
1214
+ return { workers: 1, source: "single worker" };
1215
+ }
1216
+ const value = flagValue(args, ["--maxWorkers", "--poolOptions.threads.maxThreads", "--poolOptions.forks.maxForks", "--poolOptions.vmThreads.maxThreads"]);
1217
+ if (value !== null) return { workers: workersFrom(value, cpus) ?? cpus, source: "--maxWorkers" };
1218
+ }
1219
+ const cap = configCap(runner, args, run.cwd, cpus);
1220
+ if (cap) return { workers: cap.workers, source: "config" };
1221
+ return { workers: Math.max(1, cpus - 1), source: "default" };
1222
+ }
1223
+
1224
+ // Free memory as a percentage: macOS memory_pressure; Linux the cgroup's
1225
+ // limit (a pod) or the host's MemAvailable. null when unknown (no check).
1226
+ function linuxFreeMemoryPct({ root = "/" } = {}) {
1227
+ const read = (rel) => readText(path.join(root, rel));
1228
+ const max2 = read("sys/fs/cgroup/memory.max");
1229
+ if (max2 !== null && /^\d+\s*$/.test(max2)) {
1230
+ const max = Number(max2);
1231
+ const current = Number(read("sys/fs/cgroup/memory.current"));
1232
+ const inactive = Number(((read("sys/fs/cgroup/memory.stat") || "").match(/^inactive_file (\d+)$/m) || [])[1] || 0);
1233
+ if (max > 0 && Number.isFinite(current)) return Math.round(((max - Math.max(0, current - inactive)) / max) * 100);
1234
+ }
1235
+ const max1 = Number(read("sys/fs/cgroup/memory/memory.limit_in_bytes"));
1236
+ if (max1 > 0 && max1 < 2 ** 60) {
1237
+ const usage = Number(read("sys/fs/cgroup/memory/memory.usage_in_bytes"));
1238
+ const inactive = Number(((read("sys/fs/cgroup/memory/memory.stat") || "").match(/^total_inactive_file (\d+)$/m) || [])[1] || 0);
1239
+ if (Number.isFinite(usage)) return Math.round(((max1 - Math.max(0, usage - inactive)) / max1) * 100);
1240
+ }
1241
+ const info = read("proc/meminfo") || "";
1242
+ const total = Number((info.match(/^MemTotal:\s+(\d+)/m) || [])[1]);
1243
+ const avail = Number((info.match(/^MemAvailable:\s+(\d+)/m) || [])[1]);
1244
+ return total > 0 && Number.isFinite(avail) ? Math.round((avail / total) * 100) : null;
1245
+ }
1246
+
1247
+ const systemProbe = {
1248
+ cpus: () => (os.availableParallelism ? os.availableParallelism() : os.cpus().length) || 1,
1249
+ freePct() {
1250
+ if (process.platform === "darwin") {
1251
+ const out = require("child_process").spawnSync("memory_pressure", ["-Q"], { encoding: "utf8", timeout: 3000 });
1252
+ const m = String(out.stdout || "").match(/free percentage:\s*(\d+)%/);
1253
+ return m ? Number(m[1]) : null;
1254
+ }
1255
+ return process.platform === "linux" ? linuxFreeMemoryPct() : null;
1256
+ },
1257
+ processes() {
1258
+ const out = require("child_process").spawnSync("ps", ["-axo", "pid=,etime=,command="], { encoding: "utf8", timeout: 3000 });
1259
+ return String(out.stdout || "").split("\n").map((line) => {
1260
+ const m = line.match(/^\s*(\d+)\s+(\S+)\s+(.*)$/);
1261
+ return m && { pid: Number(m[1]), etime: m[2], command: m[3] };
1262
+ }).filter(Boolean);
1263
+ },
1264
+ cwdOf(pid) {
1265
+ if (process.platform === "linux") { try { return fs.readlinkSync(`/proc/${pid}/cwd`); } catch (e) { return ""; } }
1266
+ const out = require("child_process").spawnSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], { encoding: "utf8", timeout: 3000 });
1267
+ const line = String(out.stdout || "").split("\n").find((l) => l.startsWith("n"));
1268
+ return line ? line.slice(1) : "";
1269
+ },
1270
+ now: () => Date.now(),
1271
+ };
1272
+
1273
+ // A process that is a test run: the runner or one of its workers, not a shell,
1274
+ // grep or `node -e` script whose text mentions jest (and never claude, whose
1275
+ // command line carries the whole system prompt).
1276
+ function isTestProcess(command) {
1277
+ const words = String(command).split(/\s+/);
1278
+ const bin = baseName(words[0] || "");
1279
+ if (!TEST_PROCESS_BINS.has(bin)) return false;
1280
+ if (bin === "node" || bin === "nodejs" || bin === "bun") {
1281
+ let j = 1;
1282
+ while (j < words.length && words[j].startsWith("-")) {
1283
+ if (["-e", "--eval", "-p", "--print"].includes(words[j])) return false;
1284
+ j += NODE_VALUE_FLAGS.has(words[j]) ? 2 : 1;
1285
+ }
1286
+ return TEST_PROCESS_RE.test(words[j] || "");
1287
+ }
1288
+ return TEST_PROCESS_RE.test(command);
1289
+ }
1290
+
1291
+ function runningTestProcess(probe) {
1292
+ return probe.processes().find((p) => p.pid !== process.pid && isTestProcess(p.command)) || null;
1293
+ }
1294
+
1295
+ function readClaim(file) {
1296
+ const text = readText(file, 64 * 1024);
1297
+ if (text === null) return null;
1298
+ try { return JSON.parse(text); } catch (e) { return null; }
1299
+ }
1300
+
1301
+ const home = (p) => String(p || "").replace(os.homedir(), "~");
1302
+
1303
+ // null (allow) or { reason, message } for a Bash command. `session` is the
1304
+ // hook payload's session_id: a conversation retrying its own just-started
1305
+ // run is not blocked by its own claim.
1306
+ function testRunDenial(command, { cwd, session = "", probe = systemProbe, limits = testRunLimits() } = {}) {
1307
+ try {
1308
+ if (process.env.OC_TEST_RUN_GUARD === "off") return null;
1309
+ const runs = testRunInvocations(command, cwd);
1310
+ if (!runs.length) return null;
1311
+ const cpus = probe.cpus();
1312
+ let heavy = null;
1313
+ for (const run of runs) {
1314
+ const verdict = testRunWorkers(run, cpus);
1315
+ if (verdict.notARun) continue;
1316
+ heavy = heavy || run;
1317
+ if (verdict.workers !== null && verdict.workers > limits.maxWorkers) {
1318
+ const capped = `--maxWorkers=${limits.maxWorkers}`;
1319
+ const how = run.via ? `${run.via} ${capped}` : `npx ${run.runner} ${capped} <files>`;
1320
+ return {
1321
+ reason: "workers",
1322
+ message:
1323
+ `⛔ Test-run guard: this ${run.runner} run would start ${verdict.workers} workers (${verdict.source === "default" ? "no worker cap: one per CPU core" : `${verdict.source} allows ${verdict.workers}`}), about 1 GB each. ` +
1324
+ `On 19 Oct uncapped test runs from parallel conversations froze the Mac twice.\n` +
1325
+ `Run it with at most ${limits.maxWorkers} workers: add ${capped} (or --runInBand for a few files), e.g. \`${how}\`.`,
1326
+ };
1327
+ }
1328
+ }
1329
+ if (!heavy) return null;
1330
+ const busy = runningTestProcess(probe);
1331
+ if (busy) {
1332
+ const where = home(probe.cwdOf(busy.pid));
1333
+ return {
1334
+ reason: "busy",
1335
+ message:
1336
+ `⛔ Test-run guard: another test run is already going on this machine (pid ${busy.pid}, running ${busy.etime}${where ? `, in ${where}` : ""}): ${String(busy.command).slice(0, 140)}\n` +
1337
+ `Only one test run at a time, so parallel conversations cannot run the machine out of memory. Wait for it to finish, then run yours. If it is your own leftover run, stop it first.`,
1338
+ };
1339
+ }
1340
+ const claim = readClaim(limits.claimFile);
1341
+ if (claim && claim.session !== session && probe.now() - Number(claim.at || 0) < TEST_RUN_CLAIM_MS) {
1342
+ return {
1343
+ reason: "busy",
1344
+ message:
1345
+ `⛔ Test-run guard: another conversation started a test run ${Math.round((probe.now() - claim.at) / 1000)} s ago${claim.cwd ? ` in ${home(claim.cwd)}` : ""}. Only one runs at a time; wait for it to finish, then run yours.`,
1346
+ };
1347
+ }
1348
+ const free = probe.freePct();
1349
+ if (free !== null && free < limits.minFreePct) {
1350
+ return {
1351
+ reason: "memory",
1352
+ message:
1353
+ `⛔ Test-run guard: free memory is ${free}%, under the ${limits.minFreePct}% a test run needs. Wait a few minutes and try again; do not raise the worker count.` +
1354
+ (process.platform === "darwin" ? " See what is using memory: open-claudia tool run mac-health memory-guard-status" : ""),
1355
+ };
1356
+ }
1357
+ try {
1358
+ fs.writeFileSync(limits.claimFile, JSON.stringify({ at: probe.now(), session, cwd: heavy.cwd, runner: heavy.runner }));
1359
+ } catch (e) { /* best effort */ }
1360
+ return null;
1361
+ } catch (e) {
1362
+ return null;
1363
+ }
1364
+ }
1365
+
997
1366
  // Claude sends the shell command as a string; accept an argv array too.
998
1367
  function shellCommandOf(input) {
999
1368
  if (!input || typeof input !== "object") return null;
@@ -1072,6 +1441,17 @@ function runHook(payload) {
1072
1441
  logGuardEvent({ kind: "deny", rule: "browser-driver", subcommand: driving.subcommand });
1073
1442
  return { exit: 2, stderr: driving.message };
1074
1443
  }
1444
+ // Test runs (worker cap, one at a time, free memory): ahead of the
1445
+ // tooling-mode switch too. Logged as its own kind so the tool-first KPI
1446
+ // (denies/bypasses) does not count it.
1447
+ const testRun = testRunDenial(command, {
1448
+ cwd: typeof data.cwd === "string" ? data.cwd : undefined,
1449
+ session: typeof data.session_id === "string" ? data.session_id : "",
1450
+ });
1451
+ if (testRun) {
1452
+ logGuardEvent({ kind: "test-run", decision: "deny", reason: testRun.reason, command: String(command).slice(0, 300) });
1453
+ return { exit: 2, stderr: testRun.message };
1454
+ }
1075
1455
  // Relaxed tooling mode: the gate observes but never blocks. The event is
1076
1456
  // still logged (as relaxed-pass, not deny) so the dream KPI can report
1077
1457
  // what WOULD have been denied — visibility survives the mode switch.
@@ -1107,4 +1487,5 @@ module.exports = {
1107
1487
  protectedReadRoots, isProtectedReadPath, isProtectedSearchRoot, secretReadRule,
1108
1488
  matchRawOp, noteRawOp, readGuardEvents,
1109
1489
  agentBrowserInvocations, browserDriverDenial,
1490
+ testRunInvocations, testRunWorkers, testRunDenial, linuxFreeMemoryPct,
1110
1491
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.5.66",
3
+ "version": "3.5.68",
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-owner-cards-dm.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-stop-queue.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-quiet-defaults.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-cost-by-part.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-speech-voices.js && node test-tts-voice.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-kazee-catchup.js && node test-usage-limits.js && node test-limit-waits.js && node test-signin-race-retry.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-browser-address.js && node test-browser-control.js && node test-browser-drive.js && node test-browser-teach.js && node test-kazee-bot-apps.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-app.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 && node test-call-quick-voice.js && node test-call-run-stop.js && node test-call-stopped-transcript.js && node test-call-greeting-context.js && node test-call-camera.js && node test-switchboard-calls.js && node test-switchboard-supervisor.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-test-run-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-owner-cards-dm.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-stop-queue.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-quiet-defaults.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-cost-by-part.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-card-blocks.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-speech-voices.js && node test-tts-voice.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-kazee-catchup.js && node test-usage-limits.js && node test-limit-waits.js && node test-signin-race-retry.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-browser-address.js && node test-browser-control.js && node test-browser-drive.js && node test-browser-teach.js && node test-kazee-bot-apps.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-app.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 && node test-call-quick-voice.js && node test-call-run-stop.js && node test-call-stopped-transcript.js && node test-call-greeting-context.js && node test-call-camera.js && node test-switchboard-calls.js && node test-switchboard-supervisor.js"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -647,7 +647,13 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
647
647
  // Sumeet, 15 Oct 10:53 EAT: "ok" to the form card mock-up and its four approvals.
648
648
  // 3.5.66 = 3.5.65 + a form's choice question answered in own words wakes as
649
649
  // their words, not "[object Object]" (APP-30; Sumeet, 15 Oct 18:09 EAT).
650
- assert.strictEqual(pkg.version, "3.5.66", "release version must remain unchanged without explicit approval");
650
+ // 3.5.67 = 3.5.66 + card building blocks (APP-31): examples, wake wording for
651
+ // tick/date/slots/rows/board, `card pieces`, the card coach nudge.
652
+ // Sumeet, 16 Oct 08:28 EAT: "ok build and push, ensure you also update open claudia".
653
+ // 3.5.68 = 3.5.67 + test-run guard (jest/vitest: 2 workers, one run at a time,
654
+ // 40% free memory) in the tool hook, after the Mac froze twice on 19 Oct.
655
+ // Sumeet, 19 Oct 11:47 EAT card: "Both: checker rule and safety net".
656
+ assert.strictEqual(pkg.version, "3.5.68", "release version must remain unchanged without explicit approval");
651
657
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
652
658
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
653
659
  }