@inetafrica/open-claudia 3.5.12 → 3.5.14
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/.env.example +2 -0
- package/CHANGELOG.md +19 -0
- package/README.md +3 -2
- package/channels/kazee/adapter.js +11 -1
- package/core/cards.js +296 -18
- package/core/loopback.js +6 -2
- package/core/runner.js +19 -3
- package/core/system-prompt.js +9 -0
- package/core/usage-log.js +10 -1
- package/package.json +4 -3
- package/test-group-status-notes.js +140 -0
- package/test-provider-language.js +14 -1
package/.env.example
CHANGED
|
@@ -63,6 +63,8 @@ OC_THREAD_TITLES=on
|
|
|
63
63
|
THREAD_TITLE_PROVIDER=
|
|
64
64
|
THREAD_TITLE_MODEL_CLAUDE=
|
|
65
65
|
THREAD_TITLE_MODEL_CODEX=
|
|
66
|
+
# Token usage alerts to the owner: on | off (default off). Never posted in group chats.
|
|
67
|
+
USAGE_ALERTS=off
|
|
66
68
|
USAGE_ALERT_CONTEXT_TOKENS=120000
|
|
67
69
|
USAGE_ALERT_RATE_MULTIPLIER=1.75
|
|
68
70
|
USAGE_ALERT_BASELINE_TURNS=20
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.5.14
|
|
4
|
+
|
|
5
|
+
Decisions stop sinking and going stale (Sumeet, 5 Oct: "they tend to fall quite far back in the chat ... after a while they might be invalid"). Bot side of "decisions inline"; works with today's chat-central, and the "waiting on you" strip comes with the server and app changes. Canary first, on the sales bot.
|
|
6
|
+
|
|
7
|
+
- **The bot closes decisions that no longer apply.** Each turn now lists the decision cards this bot asked in that conversation (the same thread, or the main timeline) that are still open, oldest first, with how long ago they were asked. If the latest message answers one, or it no longer applies, the bot closes it with `open-claudia card update <id> --close --note "why"`. Nothing is added when none are open.
|
|
8
|
+
- **One reminder for a decision left overnight.** At the first 09:00 (`OC_TIMEZONE`, default Africa/Nairobi) at least 8 hours after it was asked, the bot checks the card on the server and, only if the server shows it still open, replies to it in its own chat and thread: "⏳ Still waiting on you: <title>". Reminders go out between 09:00 and 18:00 local, so a restart at night never posts a batch. Never a new card, never twice (a "⏰ Later" reply counts as the card's one reminder), nothing for cards over 7 days old.
|
|
9
|
+
- **A late answer is checked first.** An answer to a card asked more than 6 hours earlier wakes the conversation with "check it still applies before acting".
|
|
10
|
+
- **"⏰ Later" keeps the decision where it is.** The card stays open with a note ("⏰ Later: I'll remind you at 14:30."); when the time comes the bot replies to that card instead of closing it and posting a new one. The next queued decision is still asked meanwhile. A decision snoozed before this release comes back as a new card, as before.
|
|
11
|
+
- **The card ledger keeps each card's state current** (closed or changed by the bot, answered, or read from the server), and the Kazee adapter can read one card's current state (`readCard`).
|
|
12
|
+
- **Tests:** `test-decision-upkeep.js` (new); `test-kazee-cards.js` now expects Later to keep the card open.
|
|
13
|
+
|
|
14
|
+
## 3.5.13
|
|
15
|
+
|
|
16
|
+
Group chats stop seeing notes meant only for the owner, and token usage alerts are off by default. Canary first, on the sales bot.
|
|
17
|
+
|
|
18
|
+
- **Fix: no more junk "📌 standing rule" posts.** After every owner turn the rule detector scanned the whole prompt, including the group or thread messages the bot was shown, so it saved the bot's own lines as the owner's rules. Its own "📌 Got it — I'll keep this loaded as a standing rule from now on" note then set off the next one, one more after every owner message in a staff group (sales bot, 4 Oct; same cause as the 1 Oct thread report). Rules now come only from the owner's own words (the typed message, voice transcript or caption), and only in a direct chat with the bot: group chats never capture rules or see 📌 notes.
|
|
19
|
+
- **Token usage alerts are off by default** (Sumeet, 4 Oct: "turn off this token usage thing by default on all the bots"). `USAGE_ALERTS=on` brings them back, with the same `USAGE_ALERT_*` thresholds, and even then they never go to a group chat.
|
|
20
|
+
- **Tests:** `test-group-status-notes.js` (new).
|
|
21
|
+
|
|
3
22
|
## 3.5.12
|
|
4
23
|
|
|
5
24
|
Two fixes: "No response requested." no longer reaches the chat, and voice notes wait out a busy speech server. Canary first, on the sales bot. (3.5.11 was tagged but never published: its release run failed on the new test, which assumed an agent CLI is installed; CI has none.)
|
package/README.md
CHANGED
|
@@ -448,8 +448,9 @@ All stored in `~/.open-claudia/`:
|
|
|
448
448
|
| `AGENT_ENV_PASSTHROUGH` | No | Comma-separated project environment keys to pass to provider children; reserved provider/control/keyring keys are always filtered and re-added only by the selected provider |
|
|
449
449
|
| `CLAUDE_MODEL` / `CODEX_MODEL` | No | Optional default model for the matching provider |
|
|
450
450
|
| `AUTO_COMPACT_TOKENS` | No | Auto-compact threshold in tokens (also settable via `/compactwindow`) |
|
|
451
|
-
| `
|
|
452
|
-
| `
|
|
451
|
+
| `USAGE_ALERTS` | No | Token usage alerts to the owner: `on` or `off` (default `off`). Never posted in group chats. The thresholds below apply only when on |
|
|
452
|
+
| `USAGE_ALERT_CONTEXT_TOKENS` | No | With alerts on: alert when one completed turn's context tokens exceed this ceiling (default `120000`, `off` disables) |
|
|
453
|
+
| `USAGE_ALERT_RATE_MULTIPLIER` | No | With alerts on: alert when the latest context-token rate exceeds the recent baseline by this multiple (default `1.75`, `off` disables) |
|
|
453
454
|
| `USAGE_ALERT_BASELINE_TURNS` / `USAGE_ALERT_MIN_BASELINE_TURNS` / `USAGE_ALERT_COOLDOWN_MS` | No | Tune token-rate baseline size, minimum sample size, and alert cooldown |
|
|
454
455
|
| `MEMORY_RECALL_MAX_CHARS` | No | Hard cap for auto-injected pack/entity memory per turn (default `9000`, `off` disables auto recall injection) |
|
|
455
456
|
| `RECALL_ENGINE` | No | Default recall engine when a chat hasn't set one via `/engine` (`classic` or `discoverer`, default `discoverer`) |
|
|
@@ -1132,10 +1132,20 @@ class KazeeAdapter {
|
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
1134
|
// Change a card this bot sent: { status: "closed"|"open", note, card }.
|
|
1135
|
+
// The card ledger keeps the new state, so open decisions stay current.
|
|
1135
1136
|
async updateCard(messageId, patch = {}) {
|
|
1136
1137
|
const res = await this._request("PUT", `/message/${encodeURIComponent(messageId)}/card`, patch);
|
|
1137
1138
|
if (res && res.success === false) throw new Error(res.error || "card update refused");
|
|
1138
|
-
|
|
1139
|
+
const view = res?.card || null;
|
|
1140
|
+
try { require("../../core/cards").noteCardUpdate(messageId, patch, view); } catch (e) { console.error("Kazee updateCard: ledger not updated:", e.message); }
|
|
1141
|
+
return view;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// A card as the server shows it now (its state: open, closed, decided),
|
|
1145
|
+
// read through the one-message lookup. Null when the message is gone.
|
|
1146
|
+
async readCard(chatId, messageId) {
|
|
1147
|
+
const m = await this._messageInChat(String(chatId), String(messageId));
|
|
1148
|
+
return m?.card || null;
|
|
1139
1149
|
}
|
|
1140
1150
|
|
|
1141
1151
|
// Get-or-create the 1:1 chat between the bot and a user, returning its
|
package/core/cards.js
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
// rest wait; Later snoozes, Tell me more asks the origin
|
|
13
13
|
// conversation for a voice explainer
|
|
14
14
|
//
|
|
15
|
+
// Open decisions must not sink and go stale (Sumeet, 5 Oct 2026): the ledger
|
|
16
|
+
// keeps each card's state current, the turn's prompt lists the decisions
|
|
17
|
+
// still open in that conversation so the bot closes the ones that no longer
|
|
18
|
+
// apply, a decision left overnight gets one reminder in its own thread, and a
|
|
19
|
+
// late answer wakes with "check this still applies".
|
|
20
|
+
//
|
|
15
21
|
// Safety: an answer wakes the conversation that sent the card, which runs as
|
|
16
22
|
// that card's sender. Anything a person typed (a rating comment) is quoted as
|
|
17
23
|
// their words, never as instructions. Cards that anyone in a group may answer
|
|
@@ -29,6 +35,19 @@ const CARD_KEEP_MS = 45 * 24 * 60 * 60 * 1000;
|
|
|
29
35
|
const CARD_KEEP_MAX = 2000;
|
|
30
36
|
const SNOOZE_MS = Number(process.env.OC_DECISION_SNOOZE_MS || 3 * 60 * 60 * 1000);
|
|
31
37
|
const COMMENT_MAX = 500;
|
|
38
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
39
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
40
|
+
const OPEN_DECISION_MAX_AGE_MS = 14 * DAY_MS;
|
|
41
|
+
const REMIND_MAX_AGE_MS = 7 * DAY_MS;
|
|
42
|
+
const REMIND_MIN_WAIT_MS = 8 * HOUR_MS;
|
|
43
|
+
const REMIND_HOUR = 9;
|
|
44
|
+
const REMIND_LAST_HOUR = 18;
|
|
45
|
+
const LATE_ANSWER_MS = 6 * HOUR_MS;
|
|
46
|
+
const PROMPT_DECISIONS_MAX = 5;
|
|
47
|
+
const PROMPT_TITLE_MAX = 80;
|
|
48
|
+
const PROMPT_TITLE_MIN = 20;
|
|
49
|
+
const PROMPT_SECTION_MAX = 600;
|
|
50
|
+
const timeZone = () => process.env.OC_TIMEZONE || "Africa/Nairobi";
|
|
32
51
|
|
|
33
52
|
function readJson(file, fallback) {
|
|
34
53
|
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch (_) { return fallback; }
|
|
@@ -77,6 +96,97 @@ function updateCardRecord(messageId, patch) {
|
|
|
77
96
|
return cards[String(messageId)];
|
|
78
97
|
}
|
|
79
98
|
|
|
99
|
+
/** A card is open unless the server (or the bot) closed or decided it. */
|
|
100
|
+
function cardIsOpen(state) {
|
|
101
|
+
if (!state) return true;
|
|
102
|
+
if (state.decided) return false;
|
|
103
|
+
return !["closed", "decided", "expired"].includes(state.status);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The bot changed one of its own cards (PUT /message/:id/card): keep the
|
|
108
|
+
* ledger's lastState current. `view` is the server's answer when there is one.
|
|
109
|
+
*/
|
|
110
|
+
function noteCardUpdate(messageId, patch = {}, view = null) {
|
|
111
|
+
const cur = getCard(messageId);
|
|
112
|
+
if (!cur) return null;
|
|
113
|
+
const at = new Date().toISOString();
|
|
114
|
+
let lastState;
|
|
115
|
+
if (view && view.state) {
|
|
116
|
+
lastState = { ...view.state, at };
|
|
117
|
+
} else {
|
|
118
|
+
lastState = { ...(cur.lastState || {}), status: patch.status || cur.lastState?.status || "open", at };
|
|
119
|
+
if (patch.note !== undefined && patch.note !== null) {
|
|
120
|
+
const note = String(patch.note).trim();
|
|
121
|
+
if (note) lastState.note = note;
|
|
122
|
+
else delete lastState.note;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return updateCardRecord(messageId, { lastState, ...(patch.card?.title ? { title: patch.card.title } : {}) });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Where a card was posted: a queued decision's home, else the conversation that sent it. */
|
|
129
|
+
function placeOf(rec) {
|
|
130
|
+
return rec.home || rec.origin || {};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The bot's own decision cards still waiting for an answer, oldest first.
|
|
135
|
+
* With channelId: only those asked in that conversation (its thread, or the
|
|
136
|
+
* main timeline when threadId is empty).
|
|
137
|
+
*/
|
|
138
|
+
function openDecisions({ adapterId = null, channelId, threadId = null, now = Date.now() } = {}) {
|
|
139
|
+
const queue = new Map(loadDecisions().map((d) => [d.id, d]));
|
|
140
|
+
return Object.values(loadCards())
|
|
141
|
+
.filter((c) => c && c.kind === "decision" && !c.answeredAt && cardIsOpen(c.lastState))
|
|
142
|
+
.filter((c) => now - Date.parse(c.sentAt || 0) <= OPEN_DECISION_MAX_AGE_MS)
|
|
143
|
+
.filter((c) => !c.decisionId || !queue.has(c.decisionId) || ["queued", "asked", "snoozed"].includes(queue.get(c.decisionId).status))
|
|
144
|
+
.filter((c) => {
|
|
145
|
+
if (channelId === undefined || channelId === null) return true;
|
|
146
|
+
const p = placeOf(c);
|
|
147
|
+
return String(p.channelId || "") === String(channelId)
|
|
148
|
+
&& String(p.threadId || "") === String(threadId || "")
|
|
149
|
+
&& (!adapterId || !p.adapter || p.adapter === adapterId);
|
|
150
|
+
})
|
|
151
|
+
.sort((a, b) => Date.parse(a.sentAt) - Date.parse(b.sentAt));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function plural(n, word) {
|
|
155
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** "5 minutes", "3 hours", "2 days". */
|
|
159
|
+
function ageWords(ms) {
|
|
160
|
+
if (ms < HOUR_MS) return plural(Math.max(1, Math.floor(ms / 60000)), "minute");
|
|
161
|
+
if (ms < 2 * DAY_MS) return plural(Math.floor(ms / HOUR_MS), "hour");
|
|
162
|
+
return plural(Math.floor(ms / DAY_MS), "day");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function shortTitle(title, max) {
|
|
166
|
+
const t = String(title || "a decision").replace(/\s+/g, " ").trim().replace(/"/g, "'");
|
|
167
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The per-turn prompt section listing the decisions this bot asked in this
|
|
172
|
+
* conversation that are still open, so it closes the ones answered in words
|
|
173
|
+
* or no longer needed. "" when there are none. Titles are cut to keep the
|
|
174
|
+
* whole section under PROMPT_SECTION_MAX characters.
|
|
175
|
+
*/
|
|
176
|
+
function openDecisionsPromptSection({ adapterId = null, channelId, threadId = null, now = Date.now() } = {}) {
|
|
177
|
+
if (!channelId) return "";
|
|
178
|
+
const open = openDecisions({ adapterId, channelId, threadId, now });
|
|
179
|
+
if (!open.length) return "";
|
|
180
|
+
const shown = open.slice(0, PROMPT_DECISIONS_MAX);
|
|
181
|
+
const head = "## Open decisions you asked here";
|
|
182
|
+
const more = open.length > shown.length ? [`- and ${open.length - shown.length} more`] : [];
|
|
183
|
+
const tail = "If the latest message answers one, or it no longer applies, close it: `open-claudia card update <messageId> --close --note \"<why, in plain words>\"`. Never leave an answered or moot decision open.";
|
|
184
|
+
const line = (c, title) => `- [${c.messageId}] "${title}" — asked ${ageWords(now - Date.parse(c.sentAt))} ago`;
|
|
185
|
+
const fixed = [head, ...shown.map((c) => line(c, "")), ...more, tail].join("\n").length;
|
|
186
|
+
const titleMax = Math.max(PROMPT_TITLE_MIN, Math.min(PROMPT_TITLE_MAX, Math.floor((PROMPT_SECTION_MAX - fixed) / shown.length)));
|
|
187
|
+
return [head, ...shown.map((c) => line(c, shortTitle(c.title, titleMax))), ...more, tail].join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
80
190
|
/** Default wake policy: one person's card wakes on each answer; open polls don't. */
|
|
81
191
|
function defaultWake(card) {
|
|
82
192
|
const responders = card?.policy?.responders;
|
|
@@ -117,11 +227,20 @@ function totalsLine(card) {
|
|
|
117
227
|
return parts.join("; ");
|
|
118
228
|
}
|
|
119
229
|
|
|
120
|
-
|
|
230
|
+
/** For a wake-up: a card answered long after it was asked may be out of date. */
|
|
231
|
+
function lateAnswerLine(sentAt, now = Date.now()) {
|
|
232
|
+
const age = now - Date.parse(sentAt || 0);
|
|
233
|
+
if (!sentAt || !Number.isFinite(age) || age <= LATE_ANSWER_MS) return "";
|
|
234
|
+
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.`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function describeResponse(rec, payload, now = Date.now()) {
|
|
121
238
|
const who = payload.userName || (payload.userId ? `user ${payload.userId}` : "Someone (anonymous card)");
|
|
122
239
|
const title = rec.title || payload.card?.title || "your card";
|
|
240
|
+
const late = lateAnswerLine(rec.sentAt, now);
|
|
123
241
|
if (payload.action) {
|
|
124
|
-
|
|
242
|
+
const tapped = `[Card ${payload.messageId}] ${who} tapped "${payload.label || payload.action}" on "${title}".`;
|
|
243
|
+
return late ? `${tapped}\n${late}` : tapped;
|
|
125
244
|
}
|
|
126
245
|
const comment = payload.answer && payload.answer.comment ? quote(payload.answer.comment) : "";
|
|
127
246
|
const lines = [`[Card ${payload.messageId}] ${who} answered "${title}": ${answerLabel(payload)}.`];
|
|
@@ -129,6 +248,7 @@ function describeResponse(rec, payload) {
|
|
|
129
248
|
const totals = totalsLine(payload.card);
|
|
130
249
|
if (totals) lines.push(`Totals: ${totals}.`);
|
|
131
250
|
lines.push("This is their reply to the card you sent. Act on it if your work was waiting for it; otherwise just note it.");
|
|
251
|
+
if (late) lines.push(late);
|
|
132
252
|
return lines.join("\n");
|
|
133
253
|
}
|
|
134
254
|
|
|
@@ -179,7 +299,12 @@ async function handleCardResponse(envelope, deps = {}) {
|
|
|
179
299
|
console.log(`[cards] answer on unknown card ${messageId} (sent before this bot kept a ledger, or by another bot)`);
|
|
180
300
|
return { handled: false, reason: "unknown card" };
|
|
181
301
|
}
|
|
182
|
-
|
|
302
|
+
const at = new Date().toISOString();
|
|
303
|
+
updateCardRecord(messageId, {
|
|
304
|
+
lastState: payload.card?.state ? { ...payload.card.state, at } : rec.lastState || null,
|
|
305
|
+
lastResponseAt: at,
|
|
306
|
+
...(payload.answer ? { answeredAt: rec.answeredAt || at } : {}),
|
|
307
|
+
});
|
|
183
308
|
|
|
184
309
|
if (rec.decisionId) return handleDecisionResponse(rec, payload, envelope, deps);
|
|
185
310
|
|
|
@@ -248,11 +373,19 @@ function decisionCard(d, position, total) {
|
|
|
248
373
|
return card;
|
|
249
374
|
}
|
|
250
375
|
|
|
376
|
+
/** The one reply that brings an open decision back to the top of the chat. */
|
|
377
|
+
function reminderText(title) {
|
|
378
|
+
return `⏳ Still waiting on you: ${title || "a decision"}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
251
381
|
/**
|
|
252
382
|
* Ask the next decision in a home chat when none is out. `send` posts a card
|
|
253
|
-
* and returns its message id: (home, card) => Promise<string>.
|
|
383
|
+
* and returns its message id: (home, card) => Promise<string>. `remind`
|
|
384
|
+
* posts a plain reply: (home, text, { threadId, replyTo }) => Promise. A
|
|
385
|
+
* snoozed decision keeps its card open; when its time is up it gets one
|
|
386
|
+
* reply on that card (not a new card) and counts as asked again.
|
|
254
387
|
*/
|
|
255
|
-
async function pumpDecisions({ send, now = Date.now(), homeKey = null } = {}) {
|
|
388
|
+
async function pumpDecisions({ send, remind = null, updateCard = null, now = Date.now(), homeKey = null } = {}) {
|
|
256
389
|
const list = loadDecisions();
|
|
257
390
|
const homes = new Map();
|
|
258
391
|
for (const d of list) {
|
|
@@ -264,8 +397,37 @@ async function pumpDecisions({ send, now = Date.now(), homeKey = null } = {}) {
|
|
|
264
397
|
}
|
|
265
398
|
const asked = [];
|
|
266
399
|
for (const [key, ds] of homes) {
|
|
267
|
-
|
|
268
|
-
|
|
400
|
+
const isDue = (d) => d.status === "snoozed" && Date.parse(d.notBefore || 0) <= now;
|
|
401
|
+
let remindedHere = false;
|
|
402
|
+
for (const d of ds.filter((x) => isDue(x) && x.keptCard && x.askedMessageId)) {
|
|
403
|
+
if (!remind) continue;
|
|
404
|
+
try {
|
|
405
|
+
await remind(d.home, reminderText(d.title), { threadId: d.home.threadId || null, replyTo: d.askedMessageId });
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.error(`[cards] snoozed decision ${d.id} reminder failed:`, e.message);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const fresh = loadDecisions();
|
|
411
|
+
const target = fresh.find((x) => x.id === d.id);
|
|
412
|
+
if (!target || target.status !== "snoozed") continue;
|
|
413
|
+
target.status = "asked";
|
|
414
|
+
target.remindedAt = new Date(now).toISOString();
|
|
415
|
+
saveDecisions(fresh);
|
|
416
|
+
// This reply is the card's one reminder: the overnight pass skips it.
|
|
417
|
+
updateCardRecord(d.askedMessageId, { remindedAt: target.remindedAt });
|
|
418
|
+
if (updateCard) {
|
|
419
|
+
try {
|
|
420
|
+
const view = await updateCard(d.askedMessageId, { note: "" });
|
|
421
|
+
noteCardUpdate(d.askedMessageId, { note: "" }, view);
|
|
422
|
+
} catch (_) { /* the reminder is out; a stale note is harmless */ }
|
|
423
|
+
}
|
|
424
|
+
d.status = "asked";
|
|
425
|
+
remindedHere = true;
|
|
426
|
+
asked.push({ key, decisionId: d.id, messageId: d.askedMessageId, reminded: true });
|
|
427
|
+
}
|
|
428
|
+
if (remindedHere || ds.some((d) => d.status === "asked")) continue;
|
|
429
|
+
// A decision snoozed before cards stayed open had its card closed: ask it again as a new card.
|
|
430
|
+
const due = ds.filter((d) => d.status === "queued" || (isDue(d) && !d.keptCard));
|
|
269
431
|
if (!due.length) continue;
|
|
270
432
|
const next = due.sort((a, b) => (a.status === "queued") - (b.status === "queued") || a.seq - b.seq)[0];
|
|
271
433
|
const open = ds.filter((d) => d.status !== "snoozed" || d === next).length;
|
|
@@ -299,15 +461,22 @@ async function handleDecisionResponse(rec, payload, envelope, deps = {}) {
|
|
|
299
461
|
if (!d) return { handled: false, reason: "unknown decision" };
|
|
300
462
|
const updateCard = deps.updateCard || ((messageId, patch) => envelope.adapter?.updateCard?.(messageId, patch));
|
|
301
463
|
const send = deps.sendCard || ((home, card) => envelope.adapter?.sendCard?.(home.channelId, card, { threadId: home.threadId || null }));
|
|
464
|
+
const remind = deps.remind || ((home, text, opts) => envelope.adapter?.send?.(home.channelId, text, opts));
|
|
465
|
+
const pump = () => pumpDecisions({ send, remind, updateCard, homeKey: `${d.home.adapter}|${d.home.channelId}` });
|
|
302
466
|
|
|
303
467
|
if (payload.action === "later") {
|
|
468
|
+
// The card stays open where it is (no closing it and posting a new one
|
|
469
|
+
// later); a note on it says when it comes back. A note-only change just
|
|
470
|
+
// redraws the card: no new message, no notification.
|
|
304
471
|
d.status = "snoozed";
|
|
472
|
+
d.keptCard = true;
|
|
305
473
|
d.notBefore = new Date(Date.now() + SNOOZE_MS).toISOString();
|
|
306
474
|
d.snoozes = (d.snoozes || 0) + 1;
|
|
307
475
|
saveDecisions(list);
|
|
308
|
-
const until = new Date(d.notBefore).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone:
|
|
309
|
-
|
|
310
|
-
|
|
476
|
+
const until = new Date(d.notBefore).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: timeZone() });
|
|
477
|
+
const patch = { note: `⏰ Later: I'll remind you at ${until}.` };
|
|
478
|
+
try { noteCardUpdate(rec.messageId, patch, await updateCard(rec.messageId, patch)); } catch (e) { console.error("[cards] snooze note failed:", e.message); }
|
|
479
|
+
await pump();
|
|
311
480
|
return { handled: true, snoozed: true };
|
|
312
481
|
}
|
|
313
482
|
if (payload.action === "more") {
|
|
@@ -320,18 +489,23 @@ async function handleDecisionResponse(rec, payload, envelope, deps = {}) {
|
|
|
320
489
|
return { handled: true, explained: true };
|
|
321
490
|
}
|
|
322
491
|
if (payload.answer) {
|
|
492
|
+
const wasSnoozed = d.status === "snoozed" && d.keptCard;
|
|
323
493
|
d.status = "decided";
|
|
324
494
|
d.answer = payload.answer;
|
|
325
495
|
d.answerLabel = answerLabel(payload);
|
|
326
496
|
d.decidedAt = new Date().toISOString();
|
|
327
497
|
saveDecisions(list);
|
|
498
|
+
if (wasSnoozed) {
|
|
499
|
+
try { noteCardUpdate(rec.messageId, { note: "" }, await updateCard(rec.messageId, { note: "" })); } catch (_) { /* only clears the "remind you at" note */ }
|
|
500
|
+
}
|
|
501
|
+
const late = lateAnswerLine(rec.sentAt);
|
|
328
502
|
wakeOrigin(
|
|
329
503
|
d.origin,
|
|
330
|
-
`[Decision ${d.id}] Sumeet decided "${d.title}": ${d.answerLabel}.\nThis was queued from this conversation. Carry on with the work that was waiting for it, and tell him briefly what happens next
|
|
504
|
+
`[Decision ${d.id}] Sumeet decided "${d.title}": ${d.answerLabel}.\nThis was queued from this conversation. Carry on with the work that was waiting for it, and tell him briefly what happens next.${late ? `\n${late}` : ""}`,
|
|
331
505
|
`Decided: ${d.title}`,
|
|
332
506
|
deps,
|
|
333
507
|
);
|
|
334
|
-
await
|
|
508
|
+
await pump();
|
|
335
509
|
return { handled: true, decided: true };
|
|
336
510
|
}
|
|
337
511
|
return { handled: false, reason: "nothing to do" };
|
|
@@ -342,18 +516,94 @@ async function cancelDecision(id, { updateCard } = {}) {
|
|
|
342
516
|
const d = list.find((x) => x.id === id);
|
|
343
517
|
if (!d) return null;
|
|
344
518
|
if (!["queued", "asked", "snoozed"].includes(d.status)) return d;
|
|
345
|
-
const
|
|
519
|
+
const cardOpen = d.status === "asked" || (d.status === "snoozed" && d.keptCard);
|
|
346
520
|
d.status = "cancelled";
|
|
347
521
|
d.cancelledAt = new Date().toISOString();
|
|
348
522
|
saveDecisions(list);
|
|
349
|
-
if (
|
|
350
|
-
|
|
523
|
+
if (cardOpen && d.askedMessageId && updateCard) {
|
|
524
|
+
const patch = { status: "closed", note: "Withdrawn: no longer needed." };
|
|
525
|
+
try { noteCardUpdate(d.askedMessageId, patch, await updateCard(d.askedMessageId, patch)); } catch (_) {}
|
|
351
526
|
}
|
|
352
527
|
return d;
|
|
353
528
|
}
|
|
354
529
|
|
|
355
|
-
//
|
|
356
|
-
//
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// One reminder for a decision left waiting overnight
|
|
532
|
+
|
|
533
|
+
function zonedParts(t, tz) {
|
|
534
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
535
|
+
timeZone: tz, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
|
|
536
|
+
}).formatToParts(new Date(t));
|
|
537
|
+
const p = Object.fromEntries(parts.map((x) => [x.type, Number(x.value)]));
|
|
538
|
+
return { year: p.year, month: p.month, day: p.day, hour: p.hour % 24, minute: p.minute, second: p.second };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The instant of hour:00 on a local calendar day (day may overflow; Date.UTC
|
|
543
|
+
* rolls it). One offset lookup is enough for a morning hour: clocks change
|
|
544
|
+
* in the small hours, never within a few hours of 09:00.
|
|
545
|
+
*/
|
|
546
|
+
function zonedTime(year, month, day, hour, tz) {
|
|
547
|
+
const guess = Date.UTC(year, month - 1, day, hour);
|
|
548
|
+
const p = zonedParts(guess, tz);
|
|
549
|
+
const offset = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) - guess;
|
|
550
|
+
return guess - offset;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** The first 09:00 local at least 8 hours after the decision was asked. */
|
|
554
|
+
function reminderDueAt(sentAt, tz = timeZone()) {
|
|
555
|
+
const earliest = Date.parse(sentAt) + REMIND_MIN_WAIT_MS;
|
|
556
|
+
const p = zonedParts(earliest, tz);
|
|
557
|
+
const sameDay = zonedTime(p.year, p.month, p.day, REMIND_HOUR, tz);
|
|
558
|
+
return sameDay >= earliest ? sameDay : zonedTime(p.year, p.month, p.day + 1, REMIND_HOUR, tz);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Post the one reminder each open decision card is due, as a plain reply to
|
|
563
|
+
* the card in its own chat and thread. `readState(rec)` asks the server for
|
|
564
|
+
* the card's current state; only a card the server shows as open gets the
|
|
565
|
+
* reminder (the ledger misses answers the bot never received), and a failed
|
|
566
|
+
* or empty read is tried again on a later tick. Without `readState` the
|
|
567
|
+
* ledger decides. Reminders go out between 09:00 and 18:00 local only, so a
|
|
568
|
+
* restart at night never posts a batch of them.
|
|
569
|
+
* `send(place, text, { threadId, replyTo })` posts the reply.
|
|
570
|
+
*/
|
|
571
|
+
async function remindOpenDecisions({ send, readState = null, now = Date.now(), tz = timeZone() } = {}) {
|
|
572
|
+
const localHour = zonedParts(now, tz).hour;
|
|
573
|
+
if (localHour < REMIND_HOUR || localHour >= REMIND_LAST_HOUR) return [];
|
|
574
|
+
const queue = new Map(loadDecisions().map((d) => [d.id, d]));
|
|
575
|
+
const due = openDecisions({ now }).filter((c) => !c.remindedAt
|
|
576
|
+
&& now - Date.parse(c.sentAt) <= REMIND_MAX_AGE_MS
|
|
577
|
+
&& now >= reminderDueAt(c.sentAt, tz)
|
|
578
|
+
// a snoozed decision comes back through the queue at its own time
|
|
579
|
+
&& !(c.decisionId && queue.get(c.decisionId)?.status === "snoozed"));
|
|
580
|
+
const reminded = [];
|
|
581
|
+
for (const c of due) {
|
|
582
|
+
if (readState) {
|
|
583
|
+
let state = null;
|
|
584
|
+
try { state = await readState(c); } catch (e) { console.error(`[cards] reading card ${c.messageId} failed, trying again later:`, e.message); }
|
|
585
|
+
if (!state) continue;
|
|
586
|
+
updateCardRecord(c.messageId, { lastState: { ...state, at: new Date(now).toISOString() } });
|
|
587
|
+
if (!cardIsOpen(state)) continue;
|
|
588
|
+
}
|
|
589
|
+
// Claimed before sending: a reminder is never posted twice, even if the
|
|
590
|
+
// send fails half way.
|
|
591
|
+
updateCardRecord(c.messageId, { remindedAt: new Date(now).toISOString() });
|
|
592
|
+
const place = placeOf(c);
|
|
593
|
+
try {
|
|
594
|
+
await send(place, reminderText(c.title), { threadId: place.threadId || null, replyTo: c.messageId });
|
|
595
|
+
reminded.push(c.messageId);
|
|
596
|
+
} catch (e) {
|
|
597
|
+
updateCardRecord(c.messageId, { remindFailed: String(e.message || e).slice(0, 200) });
|
|
598
|
+
console.error(`[cards] reminder for card ${c.messageId} failed:`, e.message);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return reminded;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Snoozed decisions come back when their time is up, and decisions left
|
|
605
|
+
// overnight get their one reminder; nothing else would, so the bot looks
|
|
606
|
+
// every few minutes.
|
|
357
607
|
let pumpTimer = null;
|
|
358
608
|
function startDecisionPump(registry, intervalMs = 5 * 60 * 1000) {
|
|
359
609
|
if (pumpTimer) return pumpTimer;
|
|
@@ -362,7 +612,27 @@ function startDecisionPump(registry, intervalMs = 5 * 60 * 1000) {
|
|
|
362
612
|
if (!adapter || typeof adapter.sendCard !== "function") return null;
|
|
363
613
|
return adapter.sendCard(home.channelId, card, { threadId: home.threadId || null });
|
|
364
614
|
};
|
|
365
|
-
const
|
|
615
|
+
const remind = (place, text, opts) => {
|
|
616
|
+
const adapter = registry.findAdapter(place.adapter);
|
|
617
|
+
if (!adapter || typeof adapter.send !== "function") throw new Error(`no adapter ${place.adapter}`);
|
|
618
|
+
return adapter.send(place.channelId, text, opts);
|
|
619
|
+
};
|
|
620
|
+
const updateCard = (messageId, patch) => {
|
|
621
|
+
const rec = getCard(messageId);
|
|
622
|
+
const adapter = rec && registry.findAdapter(placeOf(rec).adapter);
|
|
623
|
+
return adapter && typeof adapter.updateCard === "function" ? adapter.updateCard(messageId, patch) : null;
|
|
624
|
+
};
|
|
625
|
+
const readState = async (rec) => {
|
|
626
|
+
const place = placeOf(rec);
|
|
627
|
+
const adapter = registry.findAdapter(place.adapter);
|
|
628
|
+
if (!adapter || typeof adapter.readCard !== "function") return null;
|
|
629
|
+
const card = await adapter.readCard(place.channelId, rec.messageId);
|
|
630
|
+
return card?.state || null;
|
|
631
|
+
};
|
|
632
|
+
const tick = async () => {
|
|
633
|
+
await pumpDecisions({ send, remind, updateCard }).catch((e) => console.error("[cards] decision pump failed:", e.message));
|
|
634
|
+
await remindOpenDecisions({ send: remind, readState }).catch((e) => console.error("[cards] decision reminders failed:", e.message));
|
|
635
|
+
};
|
|
366
636
|
pumpTimer = setInterval(tick, intervalMs);
|
|
367
637
|
if (pumpTimer.unref) pumpTimer.unref();
|
|
368
638
|
const first = setTimeout(tick, 30 * 1000);
|
|
@@ -377,6 +647,14 @@ module.exports = {
|
|
|
377
647
|
recordSentCard,
|
|
378
648
|
getCard,
|
|
379
649
|
updateCardRecord,
|
|
650
|
+
noteCardUpdate,
|
|
651
|
+
cardIsOpen,
|
|
652
|
+
openDecisions,
|
|
653
|
+
openDecisionsPromptSection,
|
|
654
|
+
ageWords,
|
|
655
|
+
lateAnswerLine,
|
|
656
|
+
reminderDueAt,
|
|
657
|
+
remindOpenDecisions,
|
|
380
658
|
defaultWake,
|
|
381
659
|
describeResponse,
|
|
382
660
|
handleCardResponse,
|
package/core/loopback.js
CHANGED
|
@@ -234,7 +234,7 @@ async function handleCardKind(res, kind, payload, { channelId, adapterId, adapte
|
|
|
234
234
|
}
|
|
235
235
|
try {
|
|
236
236
|
const view = await target.updateCard(String(payload.messageId), patch);
|
|
237
|
-
cards.
|
|
237
|
+
cards.noteCardUpdate(payload.messageId, patch, view);
|
|
238
238
|
return reply(res, 200, { ok: true, card: view });
|
|
239
239
|
} catch (e) {
|
|
240
240
|
return reply(res, 400, { error: e.message });
|
|
@@ -282,7 +282,11 @@ async function handleCardKind(res, kind, payload, { channelId, adapterId, adapte
|
|
|
282
282
|
});
|
|
283
283
|
let asked = [];
|
|
284
284
|
try {
|
|
285
|
-
asked = await cards.pumpDecisions({
|
|
285
|
+
asked = await cards.pumpDecisions({
|
|
286
|
+
send: (home, card) => ((registry.findAdapter(home.adapter) || own).sendCard(home.channelId, card, { threadId: home.threadId || null })),
|
|
287
|
+
remind: (home, text, opts) => ((registry.findAdapter(home.adapter) || own).send(home.channelId, text, opts)),
|
|
288
|
+
updateCard: (messageId, patch) => (typeof own.updateCard === "function" ? own.updateCard(messageId, patch) : null),
|
|
289
|
+
});
|
|
286
290
|
} catch (e) {
|
|
287
291
|
const waiting = cards.loadDecisions().filter((x) => ["queued", "asked", "snoozed"].includes(x.status)).length;
|
|
288
292
|
return reply(res, 200, { ok: true, decision: d, askedNow: false, waiting, warning: `queued, but asking failed: ${e.message}` });
|
package/core/runner.js
CHANGED
|
@@ -234,10 +234,13 @@ function logTurnUsage(state, usage, costUsd, announce, opts = {}) {
|
|
|
234
234
|
if (result.alert) {
|
|
235
235
|
state.usageAlertLastAt = Date.now();
|
|
236
236
|
// Owner-only: version/model/context internals must never surface to an
|
|
237
|
-
// external speaker, regardless of the usage-alert policy.
|
|
237
|
+
// external speaker, regardless of the usage-alert policy. Never in a group
|
|
238
|
+
// chat either: everyone else in the room sees only noise.
|
|
238
239
|
let usageOwnerOnly = true;
|
|
239
240
|
try { usageOwnerOnly = !require("./relationship").isCurrentSpeakerGuarded(); } catch (e) {}
|
|
240
|
-
|
|
241
|
+
let inGroup = false;
|
|
242
|
+
try { inGroup = require("./context").currentIsGroup(); } catch (e) {}
|
|
243
|
+
if (announce && usageOwnerOnly && !inGroup) {
|
|
241
244
|
const baseline = result.trend?.baselineAvgContextTokens || 0;
|
|
242
245
|
const rate = result.trend?.latestRate || 0;
|
|
243
246
|
const contextLabel = usageScope === "session_total" && !hasPeak ? "Session context total" : "Context this turn";
|
|
@@ -2231,16 +2234,27 @@ function nameThreadAfterTurn(result, runContext, store) {
|
|
|
2231
2234
|
// detector spots durable-rule statements ("from now on / never / stop") in the
|
|
2232
2235
|
// owner's message and captures them into lessons.md (or asks first). Owner-only
|
|
2233
2236
|
// and best-effort: it never blocks or breaks a turn.
|
|
2237
|
+
//
|
|
2238
|
+
// Only the owner's own words count: store.inboundText (the typed message,
|
|
2239
|
+
// voice transcript or caption, set by admitGuardedTurn), never the prompt,
|
|
2240
|
+
// which also carries injected group/thread messages, quoted replies and the
|
|
2241
|
+
// bot's own earlier lines. Reading the prompt saved the bot's own "📌 ... from
|
|
2242
|
+
// now on" notes as the owner's rules, one more after every owner turn in a
|
|
2243
|
+
// group (sales bot, 4 Oct 2026). Group chats never capture: a rule note there
|
|
2244
|
+
// is noise for everyone else, so rules are set in a direct chat with the bot.
|
|
2234
2245
|
async function considerGuardrail(result, runContext, store) {
|
|
2235
2246
|
try {
|
|
2236
2247
|
if (!result.ok || runContext.purpose !== "foreground") return;
|
|
2248
|
+
if (store?.isGroup) return;
|
|
2249
|
+
const ownWords = String(store?.inboundText || "").trim();
|
|
2250
|
+
if (!ownWords) return;
|
|
2237
2251
|
const guardrail = require("./guardrail");
|
|
2238
2252
|
if (!guardrail.enabled()) return;
|
|
2239
2253
|
const relationship = require("./relationship");
|
|
2240
2254
|
const speaker = relationship.speakerFor(store?.adapter?.type, store?.channelId, store?.userId);
|
|
2241
2255
|
if (relationship.guardActive(speaker)) return; // external speakers never capture
|
|
2242
2256
|
const announce = (text, opts) => chatContext.run(store, () => (statusChatterMuted() ? true : send(text, opts)));
|
|
2243
|
-
await guardrail.consider({ text:
|
|
2257
|
+
await guardrail.consider({ text: ownWords, speaker, announce });
|
|
2244
2258
|
} catch (e) { /* best-effort — a guardrail miss must never break a turn */ }
|
|
2245
2259
|
}
|
|
2246
2260
|
|
|
@@ -2718,4 +2732,6 @@ module.exports = {
|
|
|
2718
2732
|
archiveCompactionBrief,
|
|
2719
2733
|
collectRepoStateFacts,
|
|
2720
2734
|
collectRecentTurns,
|
|
2735
|
+
considerGuardrail,
|
|
2736
|
+
logTurnUsage,
|
|
2721
2737
|
};
|
package/core/system-prompt.js
CHANGED
|
@@ -540,6 +540,15 @@ function buildDynamicContextBlock(opts = {}) {
|
|
|
540
540
|
...parallelConversationLines(opts.state || currentState(), opts.runContext),
|
|
541
541
|
];
|
|
542
542
|
if (adapter && channelId) {
|
|
543
|
+
// Decision cards this bot asked in this conversation that still wait: it
|
|
544
|
+
// closes the ones answered in words or no longer needed. Never breaks the
|
|
545
|
+
// prompt (a missing or broken cards.json just means none).
|
|
546
|
+
try {
|
|
547
|
+
const section = require("./cards").openDecisionsPromptSection({
|
|
548
|
+
adapterId: adapter.id, channelId, threadId: opts.runContext?.threadId || currentThreadId() || null,
|
|
549
|
+
});
|
|
550
|
+
if (section) lines.push("", section);
|
|
551
|
+
} catch (_) { /* no open decisions shown */ }
|
|
543
552
|
const pending = tasksStore.pendingSummary(adapter.id, channelId);
|
|
544
553
|
if (pending.length > 0) {
|
|
545
554
|
const key = dedupeScope(adapter, channelId, opts);
|
package/core/usage-log.js
CHANGED
|
@@ -56,11 +56,20 @@ function floatEnv(env, key, fallback) {
|
|
|
56
56
|
return Number.isFinite(n) ? n : fallback;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Token usage alerts are an operator's tuning aid, so they are off unless
|
|
60
|
+
// USAGE_ALERTS=on (Sumeet, 4 Oct 2026: "turn off this token usage thing by
|
|
61
|
+
// default on all the bots", after one landed in a staff group). The
|
|
62
|
+
// thresholds below only apply once they are switched on.
|
|
63
|
+
function usageAlertsOn(env = {}) {
|
|
64
|
+
const raw = env.USAGE_ALERTS ?? process.env.USAGE_ALERTS;
|
|
65
|
+
return /^(on|true|yes|1)$/i.test(String(raw ?? "").trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
function usageAlertPolicy(env = {}) {
|
|
60
69
|
const absoluteContextTokens = intEnv(env, "USAGE_ALERT_CONTEXT_TOKENS", 120000);
|
|
61
70
|
const rateMultiplier = floatEnv(env, "USAGE_ALERT_RATE_MULTIPLIER", 1.75);
|
|
62
71
|
return {
|
|
63
|
-
enabled: absoluteContextTokens > 0 || rateMultiplier > 0,
|
|
72
|
+
enabled: usageAlertsOn(env) && (absoluteContextTokens > 0 || rateMultiplier > 0),
|
|
64
73
|
absoluteContextTokens,
|
|
65
74
|
rateMultiplier,
|
|
66
75
|
baselineTurns: intEnv(env, "USAGE_ALERT_BASELINE_TURNS", 20),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.14",
|
|
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": {
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"test:kazee-call": "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",
|
|
11
11
|
"setup": "node setup.js",
|
|
12
12
|
"start": "node bot.js",
|
|
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-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-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-call-speech.js && node test-kazee-call-session.js",
|
|
14
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-room-scope.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-thread-lanes.js && node test-thread-titles.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-thread-stop.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-stream-preview-purpose.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-attest.js && node test-sandbox-probe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-bot-endpoint.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cli-owner-gates.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-verify-contact.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-owner-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-watch-check-env.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-intro-adoption.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js && node test-kazee-cards.js && node test-passive-replies.js && node test-kazee-bot-loops.js && node test-placeholder-reply.js && node test-media-speech.js && node test-passive-replies-router.js && node test-loopback-cards.js"
|
|
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-device-login.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-upgrade-path.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-kazee-context-media.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-group-status-notes.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-call-speech.js && node test-kazee-call-session.js",
|
|
14
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-room-scope.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-thread-lanes.js && node test-thread-titles.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-thread-stop.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-stream-preview-purpose.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-attest.js && node test-sandbox-probe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-bot-endpoint.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cli-owner-gates.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-verify-contact.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-owner-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-watch-check-env.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-intro-adoption.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js && node test-kazee-cards.js && node test-decision-upkeep.js && node test-passive-replies.js && node test-kazee-bot-loops.js && node test-placeholder-reply.js && node test-media-speech.js && node test-passive-replies-router.js && node test-loopback-cards.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bot.js",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"test-agency-ledger.js",
|
|
86
86
|
"test-agency-deliberate.js",
|
|
87
87
|
"test-guardrail.js",
|
|
88
|
+
"test-group-status-notes.js",
|
|
88
89
|
"test-delivery-contract.js",
|
|
89
90
|
"test-run-lock.js",
|
|
90
91
|
"test-thread-lanes.js",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Notes meant only for the owner stay out of group chats, and a standing rule
|
|
2
|
+
// comes only from the owner's own words. Sales bot, 4 Oct 2026: after each of
|
|
3
|
+
// Sumeet's turns in a staff group it posted "📌 Got it — I'll keep this loaded
|
|
4
|
+
// as a standing rule from now on" and saved its OWN previous 📌 line (shown to
|
|
5
|
+
// it as group context) as his rule, one more per turn; a "Token usage alert"
|
|
6
|
+
// landed in the same group. Token alerts are now off unless USAGE_ALERTS=on.
|
|
7
|
+
// No network, redirected config dir.
|
|
8
|
+
|
|
9
|
+
const assert = require("assert");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const os = require("os");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
|
|
14
|
+
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "oc-group-notes-"));
|
|
15
|
+
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
16
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = TMP;
|
|
17
|
+
process.env.WORKSPACE = TMP;
|
|
18
|
+
process.env.GUARDRAIL_CAPTURE = "on";
|
|
19
|
+
delete process.env.USAGE_ALERTS;
|
|
20
|
+
|
|
21
|
+
const lessons = require("./core/lessons");
|
|
22
|
+
const guardrail = require("./core/guardrail");
|
|
23
|
+
const relationship = require("./core/relationship");
|
|
24
|
+
const { runInChat } = require("./core/context");
|
|
25
|
+
const { usageAlertPolicy, evaluateUsageAlert } = require("./core/usage-log");
|
|
26
|
+
const { considerGuardrail, logTurnUsage } = require("./core/runner");
|
|
27
|
+
|
|
28
|
+
// The speaker is the owner throughout: who may capture at all is
|
|
29
|
+
// test-guardrail.js's job, this file is about which words and which rooms.
|
|
30
|
+
relationship.speakerFor = () => ({ isOwner: true, channelId: "kazee:room" });
|
|
31
|
+
relationship.guardActive = () => false;
|
|
32
|
+
relationship.isCurrentSpeakerGuarded = () => false;
|
|
33
|
+
|
|
34
|
+
function resetLessons() {
|
|
35
|
+
try { fs.rmSync(lessons.LESSONS_FILE, { force: true }); } catch (e) {}
|
|
36
|
+
try { fs.rmSync(lessons.LESSONS_META_FILE, { force: true }); } catch (e) {}
|
|
37
|
+
try { fs.rmSync(guardrail.PENDING_FILE, { force: true }); } catch (e) {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A chat store as the router builds it: inboundText is what the person said
|
|
41
|
+
// (typed text, voice transcript or caption), isGroup marks a group room.
|
|
42
|
+
function chat({ isGroup, inboundText }) {
|
|
43
|
+
const sent = [];
|
|
44
|
+
const adapter = {
|
|
45
|
+
type: "kazee",
|
|
46
|
+
send: async (channelId, text) => { sent.push(text); return { ok: true, messageId: `m${sent.length}` }; },
|
|
47
|
+
};
|
|
48
|
+
return { sent, store: { adapter, channelId: "kazee:room", userId: "owner", isGroup, inboundText } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const OK = { ok: true };
|
|
52
|
+
const turn = (userPrompt) => ({ purpose: "foreground", userPrompt });
|
|
53
|
+
const RULE = "From now on, always send the deep link after posting a Spaces comment.";
|
|
54
|
+
const PIN = "📌 Got it — I'll keep this loaded as a standing rule from now on";
|
|
55
|
+
|
|
56
|
+
(async () => {
|
|
57
|
+
// 1. The live case: a question in a group whose context starts with the
|
|
58
|
+
// bot's own last 📌 note. Nothing is saved and nothing is posted.
|
|
59
|
+
resetLessons();
|
|
60
|
+
let c = chat({ isGroup: true, inboundText: "How many leads did we get last month?" });
|
|
61
|
+
await considerGuardrail(OK, turn([
|
|
62
|
+
"[Group context] Messages since your last reply:",
|
|
63
|
+
` [1d ago] You: ${PIN}`,
|
|
64
|
+
" [2m ago] Sumeet Rajpal: How many leads did we get last month?",
|
|
65
|
+
"",
|
|
66
|
+
"How many leads did we get last month?",
|
|
67
|
+
].join("\n")), c.store);
|
|
68
|
+
assert.deepStrictEqual(lessons.readLessons(), [], "the bot's own 📌 line in the context is not the owner's rule");
|
|
69
|
+
assert.deepStrictEqual(c.sent, [], "no 📌 note in the group");
|
|
70
|
+
|
|
71
|
+
// 2. A direct thread with the bot quoting its earlier 📌 note: the owner's
|
|
72
|
+
// own words hold no rule, so nothing is saved.
|
|
73
|
+
resetLessons();
|
|
74
|
+
c = chat({ isGroup: false, inboundText: "And deals?" });
|
|
75
|
+
await considerGuardrail(OK, turn(`[Kazee thread context]\n [36s ago] You: ${PIN}\n\nAnd deals?`), c.store);
|
|
76
|
+
assert.deepStrictEqual(lessons.readLessons(), [], "a quoted bot line never becomes a rule");
|
|
77
|
+
assert.deepStrictEqual(c.sent, []);
|
|
78
|
+
|
|
79
|
+
// 3. The owner states a rule in a direct chat: saved, and the 📌 note goes
|
|
80
|
+
// to that chat with its Undo.
|
|
81
|
+
resetLessons();
|
|
82
|
+
c = chat({ isGroup: false, inboundText: RULE });
|
|
83
|
+
await considerGuardrail(OK, turn(`[Kazee thread context]\n [5m ago] You: Done.\n\n${RULE}`), c.store);
|
|
84
|
+
const saved = lessons.readLessons();
|
|
85
|
+
assert.strictEqual(saved.length, 1, "the owner's own rule is saved");
|
|
86
|
+
assert.ok(/deep link/i.test(saved[0].text), saved[0].text);
|
|
87
|
+
assert.strictEqual(c.sent.length, 1);
|
|
88
|
+
assert.ok(c.sent[0].startsWith("📌"), c.sent[0]);
|
|
89
|
+
|
|
90
|
+
// 4. The same words in a group: rules are set in a direct chat, so the
|
|
91
|
+
// group gets no note and nothing is saved.
|
|
92
|
+
resetLessons();
|
|
93
|
+
c = chat({ isGroup: true, inboundText: RULE });
|
|
94
|
+
await considerGuardrail(OK, turn(RULE), c.store);
|
|
95
|
+
assert.deepStrictEqual(lessons.readLessons(), [], "groups never capture");
|
|
96
|
+
assert.deepStrictEqual(c.sent, []);
|
|
97
|
+
|
|
98
|
+
// 5. A turn with none of the owner's words (wake-up, cron): the prompt alone
|
|
99
|
+
// never captures, even when it reads like a rule.
|
|
100
|
+
resetLessons();
|
|
101
|
+
c = chat({ isGroup: false, inboundText: "" });
|
|
102
|
+
await considerGuardrail(OK, turn(`[Scheduled wakeup "x" fired] ${RULE}`), c.store);
|
|
103
|
+
assert.deepStrictEqual(lessons.readLessons(), [], "no own words, no capture");
|
|
104
|
+
|
|
105
|
+
// Token usage alerts: off by default; thresholds alone do not switch them
|
|
106
|
+
// on; USAGE_ALERTS=on does, unless both thresholds are off.
|
|
107
|
+
const big = { contextTokens: 127758 };
|
|
108
|
+
assert.strictEqual(usageAlertPolicy({}).enabled, false, "alerts are off by default");
|
|
109
|
+
assert.strictEqual(evaluateUsageAlert([], big, usageAlertPolicy({})).alert, false);
|
|
110
|
+
assert.strictEqual(usageAlertPolicy({ USAGE_ALERT_CONTEXT_TOKENS: "120000" }).enabled, false);
|
|
111
|
+
assert.strictEqual(usageAlertPolicy({ USAGE_ALERTS: "on" }).enabled, true);
|
|
112
|
+
assert.strictEqual(evaluateUsageAlert([], big, usageAlertPolicy({ USAGE_ALERTS: "on" })).alert, true);
|
|
113
|
+
assert.strictEqual(usageAlertPolicy({
|
|
114
|
+
USAGE_ALERTS: "on", USAGE_ALERT_CONTEXT_TOKENS: "off", USAGE_ALERT_RATE_MULTIPLIER: "off",
|
|
115
|
+
}).enabled, false);
|
|
116
|
+
|
|
117
|
+
// The turn's usage log: a 127.8k-context turn announces nothing by default,
|
|
118
|
+
// announces once in a direct chat with USAGE_ALERTS=on, and never in a group.
|
|
119
|
+
const usage = { input_tokens: 1000, output_tokens: 200, cache_read_input_tokens: 126558 };
|
|
120
|
+
async function alertsFor({ isGroup }) {
|
|
121
|
+
const alerts = [];
|
|
122
|
+
const announce = async (text) => { alerts.push(text); };
|
|
123
|
+
const state = { userId: "owner", sessionUsage: {} };
|
|
124
|
+
const { store } = chat({ isGroup, inboundText: "And deals?" });
|
|
125
|
+
await runInChat({ ...store }, async () => logTurnUsage(state, usage, 0, announce, {
|
|
126
|
+
backend: "claude", model: "claude-test", sessionId: "s1", liveContextTokens: 127758,
|
|
127
|
+
}));
|
|
128
|
+
return alerts;
|
|
129
|
+
}
|
|
130
|
+
assert.deepStrictEqual(await alertsFor({ isGroup: false }), [], "off by default in a direct chat too");
|
|
131
|
+
process.env.USAGE_ALERTS = "on";
|
|
132
|
+
const direct = await alertsFor({ isGroup: false });
|
|
133
|
+
assert.strictEqual(direct.length, 1, "USAGE_ALERTS=on announces in a direct chat");
|
|
134
|
+
assert.ok(direct[0].startsWith("Token usage alert:"), direct[0]);
|
|
135
|
+
assert.deepStrictEqual(await alertsFor({ isGroup: true }), [], "never in a group");
|
|
136
|
+
delete process.env.USAGE_ALERTS;
|
|
137
|
+
|
|
138
|
+
console.log("group status notes: ok");
|
|
139
|
+
process.exit(0);
|
|
140
|
+
})().catch((error) => { console.error(error); process.exit(1); });
|
|
@@ -546,7 +546,20 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
546
546
|
// the speech queue and say why they fail. 3.5.11 was tagged but never
|
|
547
547
|
// published (its release run failed on a test), so the same release ships as
|
|
548
548
|
// 3.5.12. Canary on the sales bot only.
|
|
549
|
-
|
|
549
|
+
// 3.5.13 approved by Sumeet 2026-10-04 12:27 EAT ("Yes, fix and ship" on the card
|
|
550
|
+
// "Switch token alerts off on every bot, fix the junk 📌 posts, and ship 3.5.13
|
|
551
|
+
// to the sales bot?"), after asking at 11:44 EAT ("Can we turn of[f] this token
|
|
552
|
+
// usage thing by default on all the bots"): rules come only from the owner's own
|
|
553
|
+
// words in a direct chat, so group chats never get junk "standing rule" posts,
|
|
554
|
+
// and token usage alerts are off by default and never go to a group chat.
|
|
555
|
+
// Canary on the sales bot only.
|
|
556
|
+
// 3.5.14 approved by Sumeet 2026-10-05 14:56 EAT ("yep" to "Ship decisions
|
|
557
|
+
// inline? ... Open Claudia: release 3.5.14 to the bots ... Reply yes to ship
|
|
558
|
+
// all of it"): open decisions stay current (listed per conversation, closed
|
|
559
|
+
// when they no longer apply), one reminder in working hours only when the
|
|
560
|
+
// server shows the card open, and "Later" keeps the card open with a note.
|
|
561
|
+
// Canary on the sales bot, then the fleet.
|
|
562
|
+
assert.strictEqual(pkg.version, "3.5.14", "release version must remain unchanged without explicit approval");
|
|
550
563
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
551
564
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
552
565
|
}
|