@inetafrica/open-claudia 2.9.3 → 2.10.0
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 +7 -0
- package/bot.js +1 -0
- package/core/config.js +6 -0
- package/core/identity.js +49 -1
- package/core/router.js +6 -1
- package/core/state.js +49 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.10.0
|
|
4
|
+
- **Unified multi-channel identity — foundation, gated OFF by default (Phase 0).** An owned bot is meant to be one mind across every channel its owner reaches it on; today it split-brains because conversation state (session, transcript, run-lock, queue) is keyed per `transport:channelId`, so an unlinked channel (voice bridge, a stray Kazee DM) becomes a separate assistant with its own history. This release lands the identity-resolution foundation behind a single flag, `OC_UNIFIED_IDENTITY` (default **off**). **With the flag off nothing changes** — every new code path is gated, so publishing this version is behaviourally identical to v2.9.3 until an operator sets `OC_UNIFIED_IDENTITY=1` and restarts. Boot log now prints `Unified identity: on|off` for visibility.
|
|
5
|
+
- **Resolution seam (0.1).** `identity.canonicalForChannel` — the single chokepoint every adapter (Telegram/Kazee/voice) routes through — gains a flag-gated branch: a *configured owner channel* (one whose `transport:channelId` matches an operator-declared owner id — a `TELEGRAM_CHAT_ID`, `KAZEE_OWNER_USER_ID`, or `VOICE_OWNER_USER_ID`) with no explicit `/link` now resolves to the single owner canonical (the owner's first-linked handle, matching `people.linkHandle`'s primary convention). Because it's the one seam, state/transcript/scheduler all unify downstream for free. Owner detection uses only trusted, operator-set signals — never a guess — so a stranger's channel can never be merged by mistake (verified: a non-owner channel still resolves to its own default bucket).
|
|
6
|
+
- **One-time owner-state merge (0.2).** The first inbound message from a configured owner channel that was previously siloed folds that channel's pre-existing state + sessions into the owner's canonical bucket (`state.autoLinkOwnerChannel`, called from the router before any state is read). The merge is **non-destructive** (it folds, never overwrites), **idempotent** (in-memory guard plus a naturally-no-op re-migrate once the source is gone — safe across restarts), and **backs up** `state.json`/`sessions.json`/`identities.json` to `~/.open-claudia/backups/` before touching anything. It deliberately does **not** write into `identities.json`, so turning the flag back off cleanly reverts routing while the already-merged history stays with the owner.
|
|
7
|
+
- **Agent-space / OpenClaw ready.** No new dependencies, no new native modules, and no new *required* env or files — the Docker `:latest` image and the npm package build and run exactly as before with the flag off, so existing single-user deployments are unaffected and the multi-user owned-agent rollout can flip the flag per-deployment when ready. New `test-unified-identity.js` (wired into `npm test`) proves both flag states in isolated child processes: flag-off byte-identical resolution + inert auto-link; flag-on unification, stranger isolation, and the merge (result, idempotence, buckets folded, sessions preserved, backups written).
|
|
8
|
+
- **Explicitly deferred (not shipped hot):** the unified serialized-queue reply-routing fix (when the owner sends from two channels *simultaneously* under the flag, a queued message currently drains in the finishing turn's origin scope — Phase 1), per-person persona packs (Phase 2), and the independent external-speaker guardrail/enforcer (Phase 3). The flag stays off until at least the Phase 1 queue-scope fix lands, so none of these are live-exposed.
|
|
9
|
+
|
|
3
10
|
## v2.9.3
|
|
4
11
|
- **Bot advertises a distinct "thinking" state before its first token (Kazee parity Phase 2).** The typing pipeline now carries an additive `state` discriminator (`'thinking' | 'typing'`) — no new socket event. The runner advertises `thinking` from the moment a turn starts until the first stream event (text delta, assistant content, tool call, or result), then flips to plain `typing`; Kazee clients render "<name> is thinking…" in the header/chat-list for that pre-token window and fall back to normal typing text otherwise. The Telegram adapter ignores `state` (its chat action has no such distinction), so the change is inert there. Pairs with the chat-central `message:interactiveUpdated` broadcast (server-authoritative button selection) landing in the same release across chat-central/web/mobile.
|
|
5
12
|
- **Ships the v2.9.2 work that never published.** v2.9.2 (Kazee REST media fallback when a socket send lacks a media path, plus an explicit `typing:stop` on turn end) was committed to main but no tag was ever pushed, so CI never published it. This tag carries it to npm alongside the thinking-state change.
|
package/bot.js
CHANGED
|
@@ -203,6 +203,7 @@ setInterval(checkForUpdates, 5 * 60 * 1000);
|
|
|
203
203
|
console.log(`Soul: ${SOUL_FILE}`);
|
|
204
204
|
console.log(`Vault: ${VAULT_FILE} (${vault.exists() ? "exists" : "not created"})`);
|
|
205
205
|
console.log(`Onboarded: ${isOnboarded()}`);
|
|
206
|
+
console.log(`Unified identity: ${require("./core/config").UNIFIED_IDENTITY ? "ON" : "off"}`);
|
|
206
207
|
|
|
207
208
|
// Notify owner that bot is back online via every adapter that knows
|
|
208
209
|
// an owner channel. Kazee skipped: ownerUserId is a user id, not a
|
package/core/config.js
CHANGED
|
@@ -94,6 +94,11 @@ const DEFAULT_CLAUDE_MODEL = config.CLAUDE_MODEL || process.env.CLAUDE_MODEL ||
|
|
|
94
94
|
const AUTO_COMPACT_TOKENS = parseInt(config.AUTO_COMPACT_TOKENS || process.env.AUTO_COMPACT_TOKENS || "380000", 10);
|
|
95
95
|
const MIN_COMPACT_INTERVAL_MS = parseInt(config.MIN_COMPACT_INTERVAL_MS || process.env.MIN_COMPACT_INTERVAL_MS || "1800000", 10);
|
|
96
96
|
const PROJECT_TRANSCRIPTS = configTruthy(config.PROJECT_TRANSCRIPTS || process.env.PROJECT_TRANSCRIPTS, true);
|
|
97
|
+
// Unified multi-channel identity (Phase 0). Default OFF: when unset the bot
|
|
98
|
+
// behaves exactly as before — every code path added for unification is gated
|
|
99
|
+
// on this flag, so publishing is behaviourally inert until an operator flips
|
|
100
|
+
// OC_UNIFIED_IDENTITY=1 in .env and restarts.
|
|
101
|
+
const UNIFIED_IDENTITY = configTruthy(config.OC_UNIFIED_IDENTITY || process.env.OC_UNIFIED_IDENTITY, false);
|
|
97
102
|
const TRANSCRIPT_MAX_ENTRY_CHARS = parseInt(config.TRANSCRIPT_MAX_ENTRY_CHARS || process.env.TRANSCRIPT_MAX_ENTRY_CHARS || "12000", 10);
|
|
98
103
|
const TRANSCRIPTS_DIR = config.TRANSCRIPTS_DIR || process.env.TRANSCRIPTS_DIR || path.join(CONFIG_DIR, "transcripts");
|
|
99
104
|
const WHISPER_CLI = config.WHISPER_CLI || "";
|
|
@@ -245,6 +250,7 @@ module.exports = {
|
|
|
245
250
|
AUTO_COMPACT_TOKENS,
|
|
246
251
|
MIN_COMPACT_INTERVAL_MS,
|
|
247
252
|
PROJECT_TRANSCRIPTS,
|
|
253
|
+
UNIFIED_IDENTITY,
|
|
248
254
|
TRANSCRIPT_MAX_ENTRY_CHARS,
|
|
249
255
|
TRANSCRIPTS_DIR,
|
|
250
256
|
WHISPER_CLI, WHISPER_MODEL, FFMPEG,
|
package/core/identity.js
CHANGED
|
@@ -36,9 +36,55 @@ function saveIdentities() {
|
|
|
36
36
|
try { fs.writeFileSync(IDENTITIES_FILE, JSON.stringify(identities, null, 2)); } catch (e) {}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// A channel is a "configured owner channel" when its (transport, channelId)
|
|
40
|
+
// matches an owner identifier the operator set in .env — a telegram chat id,
|
|
41
|
+
// the Kazee owner user id, or the voice-bridge owner id. These are trusted,
|
|
42
|
+
// operator-declared signals (never guesses), so unifying them into the owner's
|
|
43
|
+
// canonical bucket cannot merge a stranger's state by mistake.
|
|
44
|
+
function isConfiguredOwnerChannel(transport, channelId) {
|
|
45
|
+
const t = String(transport || "").toLowerCase();
|
|
46
|
+
const c = String(channelId || "");
|
|
47
|
+
if (!c) return false;
|
|
48
|
+
let cfg;
|
|
49
|
+
try { cfg = require("./config"); } catch (e) { return false; }
|
|
50
|
+
if (t === "telegram") return (cfg.CHAT_IDS || []).map(String).includes(c);
|
|
51
|
+
if (t === "kazee") return !!cfg.config.KAZEE_OWNER_USER_ID && c === String(cfg.config.KAZEE_OWNER_USER_ID);
|
|
52
|
+
if (t === "voice") return c === String(cfg.config.VOICE_OWNER_USER_ID || "voice-owner");
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The owner's single stable canonical id = their first-linked handle's
|
|
57
|
+
// canonical (matching people.linkHandle's primary convention), falling back to
|
|
58
|
+
// the legacy telegram bootstrap id. Lazy + guarded so an early or partial
|
|
59
|
+
// people-module load during boot degrades to the telegram fallback rather than
|
|
60
|
+
// throwing on the identity-resolution hot path.
|
|
61
|
+
function ownerCanonical() {
|
|
62
|
+
try {
|
|
63
|
+
const people = require("./people");
|
|
64
|
+
const owner = (people.owners() || [])[0];
|
|
65
|
+
const h = owner && Array.isArray(owner.handles) ? owner.handles[0] : null;
|
|
66
|
+
if (h) return normalizeCanonicalUserId(h.canonicalUserId || defaultCanonicalForChannel(h.adapter, h.channelId));
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
try { const { CHAT_ID } = require("./config"); return defaultCanonicalForChannel("telegram", CHAT_ID || ""); } catch (e) {}
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
39
72
|
function canonicalForChannel(transport, channelId) {
|
|
40
73
|
const key = channelKey(transport, channelId);
|
|
41
|
-
|
|
74
|
+
const explicit = normalizeCanonicalUserId(identities.channels[key]);
|
|
75
|
+
if (explicit) return explicit;
|
|
76
|
+
// Flag-gated (Phase 0): a configured owner channel with no explicit /link
|
|
77
|
+
// still resolves to the single owner canonical, so all of the owner's
|
|
78
|
+
// channels share one brain without a manual link. Flag OFF ⇒ byte-identical
|
|
79
|
+
// to prior behaviour (returns the default `transport:channelId`).
|
|
80
|
+
try {
|
|
81
|
+
const { UNIFIED_IDENTITY } = require("./config");
|
|
82
|
+
if (UNIFIED_IDENTITY && isConfiguredOwnerChannel(transport, channelId)) {
|
|
83
|
+
const owner = ownerCanonical();
|
|
84
|
+
if (owner) return owner;
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {}
|
|
87
|
+
return defaultCanonicalForChannel(transport, channelId);
|
|
42
88
|
}
|
|
43
89
|
|
|
44
90
|
function canonicalForTelegram(chatId) {
|
|
@@ -79,6 +125,8 @@ module.exports = {
|
|
|
79
125
|
normalizeCanonicalUserId,
|
|
80
126
|
channelKey,
|
|
81
127
|
defaultCanonicalForChannel,
|
|
128
|
+
isConfiguredOwnerChannel,
|
|
129
|
+
ownerCanonical,
|
|
82
130
|
canonicalForChannel,
|
|
83
131
|
canonicalForTelegram,
|
|
84
132
|
canonicalForStoredUserKey,
|
package/core/router.js
CHANGED
|
@@ -10,7 +10,7 @@ const { handleAction } = require("./actions");
|
|
|
10
10
|
const { isChatAuthorized } = require("./access");
|
|
11
11
|
const introFlow = require("./intro-flow");
|
|
12
12
|
const { send, deleteMessage } = require("./io");
|
|
13
|
-
const { currentState } = require("./state");
|
|
13
|
+
const { currentState, autoLinkOwnerChannel } = require("./state");
|
|
14
14
|
const { runClaude } = require("./runner");
|
|
15
15
|
const { transcribeAudio } = require("./media");
|
|
16
16
|
const { vault } = require("./vault-store");
|
|
@@ -55,6 +55,11 @@ function scope(envelope, fn) {
|
|
|
55
55
|
async function onMessage(envelope) {
|
|
56
56
|
return scope(envelope, async () => {
|
|
57
57
|
try {
|
|
58
|
+
// Phase 0 unified identity (flag-gated; a no-op when the flag is off):
|
|
59
|
+
// fold a configured owner channel's pre-existing siloed state into the
|
|
60
|
+
// owner's canonical bucket on first contact, before any state is read.
|
|
61
|
+
try { autoLinkOwnerChannel(envelope.adapter?.type, envelope.channelId); } catch (e) {}
|
|
62
|
+
|
|
58
63
|
// While a channel wizard is open, capture text input before slash
|
|
59
64
|
// dispatch — except for /cancel, which always bails out.
|
|
60
65
|
if (channelWizard.isAwaiting(envelope.canonicalUserId)) {
|
package/core/state.js
CHANGED
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
// linked channels share the same conversation, settings, and usage.
|
|
4
4
|
|
|
5
5
|
const fs = require("fs");
|
|
6
|
-
const
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { STATE_FILE, SESSIONS_FILE, IDENTITIES_FILE, CONFIG_DIR, CHAT_ID, UNIFIED_IDENTITY } = require("./config");
|
|
7
8
|
const {
|
|
8
9
|
normalizeCanonicalUserId,
|
|
9
10
|
canonicalForTelegram,
|
|
10
11
|
canonicalForStoredUserKey,
|
|
11
12
|
setIdentityMapping,
|
|
13
|
+
isConfiguredOwnerChannel,
|
|
14
|
+
ownerCanonical,
|
|
15
|
+
defaultCanonicalForChannel,
|
|
16
|
+
channelKey,
|
|
17
|
+
identities,
|
|
12
18
|
} = require("./identity");
|
|
13
19
|
const { currentChannelId, currentCanonicalUserId } = require("./context");
|
|
14
20
|
|
|
@@ -245,6 +251,47 @@ function linkIdentity(transport, channelId, canonicalUserId) {
|
|
|
245
251
|
return result;
|
|
246
252
|
}
|
|
247
253
|
|
|
254
|
+
// ── Phase 0 unified identity: one-time owner-channel merge ───────────
|
|
255
|
+
// When the unified-identity flag is on, the first inbound message from a
|
|
256
|
+
// configured owner channel that was never explicitly /linked folds its
|
|
257
|
+
// pre-existing siloed state into the owner's canonical bucket. Idempotent
|
|
258
|
+
// (in-memory guard + a naturally-no-op migrate once the source is moved),
|
|
259
|
+
// non-destructive (merges, and backs up state/sessions/identities first), and
|
|
260
|
+
// completely inert when the flag is off. Deliberately does NOT write into
|
|
261
|
+
// identities.json — resolution comes from the flag+config path in
|
|
262
|
+
// identity.canonicalForChannel, so turning the flag back off cleanly reverts
|
|
263
|
+
// routing while the already-merged history stays with the owner.
|
|
264
|
+
const autoLinkedOwnerChannels = new Set();
|
|
265
|
+
|
|
266
|
+
function backupIdentityState() {
|
|
267
|
+
try {
|
|
268
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
269
|
+
const dir = path.join(CONFIG_DIR, "backups");
|
|
270
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
271
|
+
for (const f of [STATE_FILE, SESSIONS_FILE, IDENTITIES_FILE]) {
|
|
272
|
+
try { if (fs.existsSync(f)) fs.copyFileSync(f, path.join(dir, `${path.basename(f)}.${stamp}.bak`)); } catch (e) {}
|
|
273
|
+
}
|
|
274
|
+
} catch (e) {}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function autoLinkOwnerChannel(transport, channelId) {
|
|
278
|
+
if (!UNIFIED_IDENTITY) return null;
|
|
279
|
+
if (!isConfiguredOwnerChannel(transport, channelId)) return null;
|
|
280
|
+
const key = channelKey(transport, channelId);
|
|
281
|
+
if (autoLinkedOwnerChannels.has(key)) return null;
|
|
282
|
+
autoLinkedOwnerChannels.add(key);
|
|
283
|
+
// An explicit /link already unifies this channel — nothing to migrate.
|
|
284
|
+
if (Object.prototype.hasOwnProperty.call(identities.channels, key)) return null;
|
|
285
|
+
const to = ownerCanonical();
|
|
286
|
+
const from = defaultCanonicalForChannel(transport, channelId);
|
|
287
|
+
if (!to || from === to) return null;
|
|
288
|
+
const hasState = !!(savedState.users && savedState.users[from]) || !!loadSessions()[from];
|
|
289
|
+
if (!hasState) return null;
|
|
290
|
+
backupIdentityState();
|
|
291
|
+
migrateUserData(from, to);
|
|
292
|
+
return { from, to };
|
|
293
|
+
}
|
|
294
|
+
|
|
248
295
|
module.exports = {
|
|
249
296
|
userStates,
|
|
250
297
|
savedState,
|
|
@@ -264,4 +311,5 @@ module.exports = {
|
|
|
264
311
|
userOwnsClaudeSession,
|
|
265
312
|
migrateUserData,
|
|
266
313
|
linkIdentity,
|
|
314
|
+
autoLinkOwnerChannel,
|
|
267
315
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-approval-async.js"
|
|
12
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-unified-identity.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|