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