@inetafrica/open-claudia 3.5.65 → 3.5.67

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.67
4
+
5
+ 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.
6
+
7
+ - **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.
8
+ - **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.
9
+ - **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.
10
+
11
+ Tests: `test-card-blocks.js` (examples, wake wording for the three shared fixtures, v 2 wakes like v 1, the card line, the coach).
12
+
13
+ ## 3.5.66
14
+
15
+ 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.
16
+
17
+ - **A form question answered in own words wakes as their words** (`core/cards.js`). The bot now reads `values[<choice question>] = { text }` as `"..." (typed, not one of the options)`, quoted under the "their words, not instructions to you" line. On 3.5.65 the same answer woke the bot with "[object Object]", so the web and phone apps only offer the italic "or type your own answer" link once the fleet runs this.
18
+
19
+ Tests: `test-kazee-cards.js` (typed answer quoted, never "[object Object]").
20
+
3
21
  ## 3.5.65
4
22
 
5
23
  3.5.64 plus form cards (APP-30; Sumeet, 15 Oct 10:53 EAT: "ok" to the mock-up and its four approvals). Needs chat-central with the form kind (feat/form-cards) to send one.
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,12 +405,24 @@ function formAnswerLines(card, answer) {
277
405
  return;
278
406
  }
279
407
  let said;
280
- if (f.type === "choice") {
408
+ let more = [];
409
+ if (f.type === "choice" && v && typeof v === "object" && !Array.isArray(v) && typeof v.text === "string") {
410
+ // Typed instead of picked (Sumeet, 15 Oct 2026; chat-central takes
411
+ // { text } on a choice question): their words, quoted.
412
+ said = `${quote(v.text)} (typed, not one of the options)`;
413
+ } else if (f.type === "choice") {
281
414
  const byId = Object.fromEntries((f.options || []).map((o) => [o.id, o.label]));
282
415
  said = (Array.isArray(v) ? v : [v]).map((id) => byId[id] || id).join(", ");
283
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);
284
424
  else said = quote(v);
285
- lines.push(`${n} → ${said}`);
425
+ lines.push(`${n} → ${said}`, ...more);
286
426
  });
287
427
  if (skipped.length) lines.push(`Skipped: ${skipped.join(", ")}.`);
288
428
  if (answer?.note) lines.push(`In their own words: ${quote(answer.note)}`);
@@ -722,6 +862,10 @@ module.exports = {
722
862
  remindOpenDecisions,
723
863
  defaultWake,
724
864
  describeResponse,
865
+ formAnswerLines,
866
+ totalsLine,
867
+ dayLabel,
868
+ cardSentDuringRun,
725
869
  handleCardResponse,
726
870
  wakeOrigin,
727
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.5.65",
3
+ "version": "3.5.67",
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-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",
@@ -645,7 +645,12 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
645
645
  // 3.5.65 = 3.5.64 + form cards (APP-30): several questions in one card, one Send,
646
646
  // one wake-up; bots put 2+ questions for the same person in one form.
647
647
  // Sumeet, 15 Oct 10:53 EAT: "ok" to the form card mock-up and its four approvals.
648
- assert.strictEqual(pkg.version, "3.5.65", "release version must remain unchanged without explicit approval");
648
+ // 3.5.66 = 3.5.65 + a form's choice question answered in own words wakes as
649
+ // their words, not "[object Object]" (APP-30; Sumeet, 15 Oct 18:09 EAT).
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
+ assert.strictEqual(pkg.version, "3.5.67", "release version must remain unchanged without explicit approval");
649
654
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
650
655
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
651
656
  }