@inetafrica/open-claudia 3.4.20 → 3.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.22
4
+
5
+ Supersedes 3.4.21, which was tagged but never published: its build failed because `test-upgrade-path.js` was wired into the test chain without being committed. Nothing shipped from that tag.
6
+
7
+
8
+ - Fix `/upgrade` reporting success while upgrading a copy of the bot that nothing runs. It ran `npm install -g`, then read the resulting version back from `npm root -g` — the one directory guaranteed to agree with itself. Found on a host where the bot executes from `/usr/local/lib/node_modules` while `npm config get prefix` is `~/.local`: three releases (3.4.18, 3.4.19, 3.4.20) were each announced as installed while the running process stayed on 3.4.17.
9
+ - The layout check no longer string-matches the docker `/app` path. It compares the directory the running process executes from against the directory npm would write to, so any prefix drift routes to the in-place overlay, not just the container case. An unqueryable `npm root -g` is not treated as a mismatch — the post-install check is the backstop there.
10
+ - `/upgrade` and `/downgrade` now read the resulting version back from the directory the process actually executes from. A mismatch reports "Upgrade did NOT take", names both directories, and restarts nothing; the previous behaviour was to restart and claim success.
11
+ - Drop the superseded `claude-opus-4-8`, `claude-opus-4-7` and `claude-opus-4-6` buttons from the Claude model picker, which had grown long enough to bury the current models.
12
+ - Removal is cosmetic by design. `/model` takes a typed name verbatim without consulting the list, so `/model claude-opus-4-8` still works as a rollback; only the buttons go. The list is still checked on tapped callbacks, whose job is to reject a stale button from an old message.
13
+ - `claude-sonnet-4-6` and `claude-haiku-4-5-20251001` deliberately stay: they are the tier models for medium and low utility work, not picker garnish, and dropping them would push recall, pack review and the enforcer onto opus.
14
+
3
15
  ## 3.4.20
4
16
 
5
17
  - Make `claude-opus-5-5` the default Claude model. 3.4.19 only added it to the picker; the default stayed on `claude-opus-4-8`, so a room that had never run `/model` kept the old model.
@@ -261,7 +273,7 @@ Three approved plans land together — memory adoption A1-A4 (from the TencentDB
261
273
  ## v3.1.21 — Kazee public participation: owner seeding, group tag-gating, guarded external turns
262
274
 
263
275
  - **Owner seeding for kazee-only pods.** A pod whose only owner signal is `KAZEE_OWNER_USER_ID` (empty `auth.json`) never got a people-store owner record, so every unknown inbound — group or DM — was refused with a false "no owner configured" (observed on network-mon, 2026-07-23). `seedOwnerFromLegacy` now falls back to the kazee env owner; legacy `auth.json` owners keep precedence and seeding stays idempotent.
264
- - **Group chats engage only when addressed.** The kazee adapter fetches chat meta (5-min cache) and, in group chats, engages only on an @-mention of the bot or a reply to the bot's own message — matching chat-central's own `assistantAction` default `assistant_tagged_reply`. Chats set to `assistant_free_reply` engage on everything. Untagged group chatter is dropped silently (no roster prompts, no refusals — owner included). 1:1 chats are unchanged; a meta-fetch failure fails open to pre-gate behavior and is not cached.
276
+ - **Group chats engage only when addressed.** The kazee adapter fetches chat meta (5-min cache) and, in group chats, engages only on an @-mention of the bot or a reply to the bot's own message — matching chat-central's own `assistantAction` default `assistant_tagged_reply`. Chats set to `assistant_free_reply` engage on everything. Untagged group chatter is dropped silently (no roster prompts, no refusals — owner included). 1:1 chats are unchanged; an unresolved metadata fetch is not cached and fails closed because the room cannot safely be classified as a DM.
265
277
  - **Tagged externals get the guarded turn.** An engaged group message authorizes the turn via adapter-attested `envelope.engaged` (never derived from message content), but the speaker still classifies external: replies are vetted by the enforcer and escalate to the owner's approve-once / deny / make-a-rule card — the same pattern as Spaces. Slash commands stay closed to externals.
266
278
  - **Escalation cards reach the kazee owner.** `ownerTarget` used to return env-seeded kazee owner handles as the raw user id, but kazee sends need a chat-document id, so approval cards would 404. User-id handles now resolve through `openDirectChat`, falling back to the configured owner target.
267
279
  - New release-chain tests: `test-people-owner-seed.js` (env seed, legacy precedence, no-signal refusal) and `test-kazee-group-gate.js` (mention/reply engagement, free-reply, 1:1, meta failure, meta cache, engaged access pass-through).
package/core/handlers.js CHANGED
@@ -643,21 +643,70 @@ async function requestAgentSpaceUpgrade() {
643
643
  });
644
644
  }
645
645
 
646
- // True when the running bot's source lives at /app (docker image layout from
647
- // our Dockerfile). In that case `npm install -g` can't actually upgrade us:
648
- // the process reads code from /app, not from the global npm root, and uid
649
- // 1001 can't write to /usr/local/lib/node_modules anyway. Detect this and
650
- // run an in-place tarball overlay instead.
651
- function isDockerAppLayout() {
646
+ // The directory the RUNNING bot's source actually lives in. Everything about
647
+ // upgrading has to be judged against this, never against `npm root -g`.
648
+ function runningInstallRoot() {
649
+ return path.resolve(path.join(__dirname, ".."));
650
+ }
651
+
652
+ // Where `npm install -g` would place our package, or null when npm cannot be
653
+ // asked. Null means "cannot tell" and callers fall back to the npm path.
654
+ function globalInstallRoot() {
652
655
  try {
653
- const pkgPath = path.resolve(path.join(__dirname, "..", "package.json"));
654
- return pkgPath === "/app/package.json";
656
+ const root = execSync("npm root -g", {
657
+ encoding: "utf-8", timeout: 15000,
658
+ cwd: process.env.HOME || require("os").homedir(),
659
+ env: { ...process.env, PATH: FULL_PATH },
660
+ }).trim();
661
+ if (!root) return null;
662
+ return path.resolve(path.join(root, "@inetafrica", "open-claudia"));
663
+ } catch (e) {
664
+ return null;
665
+ }
666
+ }
667
+
668
+ // True when `npm install -g` cannot upgrade THIS process, because the code it
669
+ // executes does not live where npm would write. Two shapes seen in the wild:
670
+ //
671
+ // 1. the docker /app layout from our Dockerfile - the process reads code
672
+ // from /app and uid 1001 cannot write the global root anyway;
673
+ // 2. a host whose npm prefix has moved away from where the bot was
674
+ // installed. Observed 2026-09-23 on Sumeet's Mac: launchd runs
675
+ // /usr/local/lib/node_modules/@inetafrica/open-claudia/bot.js while
676
+ // `npm config get prefix` is ~/.local, so `npm install -g` faithfully
677
+ // installed a copy nothing executes. Three releases (3.4.18, 3.4.19,
678
+ // 3.4.20) landed in that copy and /upgrade reported success each time
679
+ // while the running bot stayed on 3.4.17 from 7 Sep.
680
+ //
681
+ // Case 2 is why this is a directory comparison and not a string match on
682
+ // "/app": the failure is general, and it is silent, which is the worst part.
683
+ // Version recorded in a given install directory, or null if unreadable. The
684
+ // ONLY honest way to answer "did the upgrade take": read it back from the
685
+ // directory the process executes from.
686
+ function installedVersionAt(dir) {
687
+ try {
688
+ return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8")).version || null;
689
+ } catch (e) {
690
+ return null;
691
+ }
692
+ }
693
+
694
+ function npmGlobalUpgradeWouldMiss(probe = {}) {
695
+ try {
696
+ const running = probe.running !== undefined ? probe.running : runningInstallRoot();
697
+ if (running === "/app") return true;
698
+ const global = probe.global !== undefined ? probe.global : globalInstallRoot();
699
+ // Cannot tell where npm would write - keep the historical behaviour and
700
+ // let `npm install -g` run. The post-install version check below is what
701
+ // catches it if that guess was wrong.
702
+ if (!global) return false;
703
+ return global !== running;
655
704
  } catch (e) {
656
705
  return false;
657
706
  }
658
707
  }
659
708
 
660
- function inPlaceTarballUpgrade(targetVersion) {
709
+ function inPlaceTarballUpgrade(targetVersion, targetDir = "/app") {
661
710
  const tmpDir = `/tmp/oc-upgrade-${process.pid}-${Date.now()}`;
662
711
  fs.mkdirSync(tmpDir, { recursive: true });
663
712
  const env = { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() };
@@ -670,10 +719,10 @@ function inPlaceTarballUpgrade(targetVersion) {
670
719
  execSync(`tar -xzf "${tgzPath}" -C "${tmpDir}"`, { encoding: "utf-8", timeout: 60000 });
671
720
  // npm pack tarballs always extract into a top-level "package/" directory.
672
721
  // cp -a preserves perms and symlinks; the trailing /. copies contents only.
673
- execSync(`cp -a "${tmpDir}/package/." /app/`, { encoding: "utf-8", timeout: 60000 });
674
- try { fs.chmodSync("/app/bin/cli.js", 0o755); } catch (e) {}
722
+ execSync(`cp -a "${tmpDir}/package/." "${targetDir}/"`, { encoding: "utf-8", timeout: 60000 });
723
+ try { fs.chmodSync(path.join(targetDir, "bin", "cli.js"), 0o755); } catch (e) {}
675
724
  execSync(`npm install --omit=dev --no-audit --no-fund`, {
676
- encoding: "utf-8", cwd: "/app", timeout: 240000, env,
725
+ encoding: "utf-8", cwd: targetDir, timeout: 240000, env,
677
726
  });
678
727
  try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) {}
679
728
  }
@@ -731,11 +780,12 @@ register({
731
780
  return;
732
781
  }
733
782
  }
734
- // Docker /app layout: bot runs from baked-in source, not from the global
735
- // npm root, and as a non-root user. Do an in-place tarball overlay and
736
- // exit; whatever orchestrates the container (k8s, docker restart=always)
737
- // brings us back on the new source.
738
- if (isDockerAppLayout()) {
783
+ // The running source does not live where `npm install -g` writes - docker
784
+ // /app, or a host whose npm prefix has drifted. Overlay the tarball onto
785
+ // the directory we actually execute from and exit; whatever supervises us
786
+ // (k8s, docker restart=always, launchd) brings us back on the new source.
787
+ if (npmGlobalUpgradeWouldMiss()) {
788
+ const overlayDir = runningInstallRoot();
739
789
  try { process.chdir("/tmp"); } catch (e) {}
740
790
  let latest = null;
741
791
  try {
@@ -751,19 +801,27 @@ register({
751
801
  await send(`Already on the latest version (v${CURRENT_VERSION}).`);
752
802
  return;
753
803
  }
754
- await send(`Upgrading v${CURRENT_VERSION} → v${latest} (in-place tarball overlay into /app)...`);
804
+ await send(`Upgrading v${CURRENT_VERSION} → v${latest} (in-place overlay into ${overlayDir})...`);
755
805
  try {
756
- inPlaceTarballUpgrade(latest);
806
+ inPlaceTarballUpgrade(latest, overlayDir);
757
807
  } catch (e) {
758
808
  const errOutput = (e.stdout || e.stderr || e.message || String(e)).slice(-700);
759
809
  await send(`Upgrade failed:\n${errOutput}`);
760
810
  return;
761
811
  }
812
+ // Verify against the directory we RUN from. Reporting a version read
813
+ // from anywhere else is how three releases were announced as installed
814
+ // while the running bot never moved.
815
+ const landed = installedVersionAt(overlayDir);
816
+ if (landed !== latest) {
817
+ await send(`Upgrade did NOT take: ${overlayDir} still reports v${landed || "unknown"}, expected v${latest}. Nothing was restarted.`);
818
+ return;
819
+ }
762
820
  let newVersion = latest;
763
821
  let whatsNew = "";
764
822
  try {
765
- newVersion = JSON.parse(fs.readFileSync("/app/package.json", "utf-8")).version;
766
- const changelog = fs.readFileSync("/app/CHANGELOG.md", "utf-8");
823
+ newVersion = JSON.parse(fs.readFileSync(path.join(overlayDir, "package.json"), "utf-8")).version;
824
+ const changelog = fs.readFileSync(path.join(overlayDir, "CHANGELOG.md"), "utf-8");
767
825
  let versionHeader = `## v${newVersion}`;
768
826
  let start = changelog.indexOf(versionHeader);
769
827
  if (start < 0) { versionHeader = `## ${newVersion}`; start = changelog.indexOf(versionHeader); }
@@ -809,11 +867,20 @@ register({
809
867
  const { ensureSetup: postUpgradeSetup } = require("../health");
810
868
  postUpgradeSetup();
811
869
  } catch (e) {}
812
- const root = execSync("npm root -g", { encoding: "utf-8", cwd: process.env.HOME || require("os").homedir(), env: { ...process.env, PATH: FULL_PATH } }).trim();
813
- const newPkg = JSON.parse(fs.readFileSync(path.join(root, "@inetafrica", "open-claudia", "package.json"), "utf-8"));
870
+ // Read the result back from the directory we EXECUTE from. Reading it
871
+ // from `npm root -g` only proves npm wrote somewhere, which is exactly
872
+ // the bug that let 3.4.18/19/20 be announced as installed on a host
873
+ // whose prefix had drifted while the running bot stayed on 3.4.17.
874
+ const runRoot = runningInstallRoot();
875
+ const landed = installedVersionAt(runRoot);
876
+ if (installTarget !== "latest" && landed !== installTarget) {
877
+ await send(`Upgrade did NOT take: ${runRoot} still reports v${landed || "unknown"}, expected v${installTarget}. npm installed elsewhere (prefix ${globalInstallRoot() || "unknown"}). Nothing was restarted.`);
878
+ return;
879
+ }
880
+ const newPkg = { version: landed || installTarget };
814
881
  let whatsNew = "";
815
882
  try {
816
- const changelog = fs.readFileSync(path.join(root, "@inetafrica", "open-claudia", "CHANGELOG.md"), "utf-8");
883
+ const changelog = fs.readFileSync(path.join(runRoot, "CHANGELOG.md"), "utf-8");
817
884
  let versionHeader = `## v${newPkg.version}`;
818
885
  let start = changelog.indexOf(versionHeader);
819
886
  if (start < 0) { versionHeader = `## ${newPkg.version}`; start = changelog.indexOf(versionHeader); }
@@ -882,14 +949,14 @@ register({
882
949
  return;
883
950
  }
884
951
  const direction = isNewerVersion(resolved, CURRENT_VERSION) ? "Moving" : "Rolling back";
885
- if (isDockerAppLayout()) {
952
+ if (npmGlobalUpgradeWouldMiss()) {
886
953
  try { process.chdir("/tmp"); } catch (e) {}
887
954
  const podNote = process.env.AGENTSPACE_POD_TOKEN
888
955
  ? "\nNote: this is an in-place overlay — a pod recreate returns to the deployment image version. Roll the image for a permanent downgrade."
889
956
  : "";
890
957
  await send(`${direction} v${CURRENT_VERSION} → v${resolved} (in-place tarball overlay into /app)...${podNote}`);
891
958
  try {
892
- inPlaceTarballUpgrade(resolved);
959
+ inPlaceTarballUpgrade(resolved, runningInstallRoot());
893
960
  } catch (e) {
894
961
  const errOutput = (e.stdout || e.stderr || e.message || String(e)).slice(-700);
895
962
  await send(`Downgrade failed:\n${errOutput}`);
@@ -2687,3 +2754,8 @@ register({
2687
2754
  send("Usage: /intros | /intros approve <id> | /intros reject <id> [reason]");
2688
2755
  },
2689
2756
  });
2757
+
2758
+ module.exports.runningInstallRoot = runningInstallRoot;
2759
+ module.exports.globalInstallRoot = globalInstallRoot;
2760
+ module.exports.npmGlobalUpgradeWouldMiss = npmGlobalUpgradeWouldMiss;
2761
+ module.exports.installedVersionAt = installedVersionAt;
@@ -12,14 +12,19 @@ const { createClaudeHookTransport } = require("./claude-hook");
12
12
  // them. Verified 2026-09-23 on 2.1.280: the API reported claude-opus-5-5 as the
13
13
  // serving model (canonicalModel claude-opus-5-5, 1M context), while 2.1.258
14
14
  // returned "does not support this model; version 2.1.280 or newer is required".
15
+ // The opus 4.x entries (4-8, 4-7, 4-6) were dropped from the picker in 3.4.21 —
16
+ // superseded, and the menu had grown long enough to bury the current models.
17
+ // This removes the BUTTONS only: /model takes a typed name verbatim without
18
+ // consulting this list, so `/model claude-opus-4-8` still works as a rollback.
19
+ // The list is consulted on tapped mb: callbacks (core/actions.js), whose job is
20
+ // to reject a stale button from an old message — that is the intended effect.
21
+ // claude-sonnet-4-6 and claude-haiku-4-5 STAY: they are the tier models for
22
+ // utility work (see tierModel below), not just picker entries.
15
23
  const CLAUDE_MODELS = Object.freeze([
16
24
  "claude-fable-5-1",
17
25
  "claude-fable-5",
18
26
  "claude-opus-5-5",
19
27
  "claude-opus-5",
20
- "claude-opus-4-8",
21
- "claude-opus-4-7",
22
- "claude-opus-4-6",
23
28
  "claude-sonnet-4-6",
24
29
  "claude-haiku-4-5-20251001",
25
30
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.4.20",
3
+ "version": "3.4.22",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "test:kazee-call": "node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-call-speech.js && node test-kazee-call-session.js",
11
11
  "setup": "node setup.js",
12
12
  "start": "node bot.js",
13
- "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-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-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-kazee-context-media.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-call-speech.js && node test-kazee-call-session.js",
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-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-upgrade-path.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-kazee-context-media.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-call-speech.js && node test-kazee-call-session.js",
14
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-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-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-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-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-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-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-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-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-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"
15
15
  },
16
16
  "files": [
@@ -49,6 +49,15 @@ assert.ok(provider.modelChoices().includes("claude-fable-5-1"));
49
49
  assert.ok(provider.modelChoices().includes("claude-opus-5-5"), "opus 5.5 is offered in the model picker");
50
50
  assert.ok(provider.modelChoices().includes("claude-fable-5"));
51
51
  assert.ok(provider.modelChoices().includes("claude-sonnet-4-6"));
52
+ // 3.4.21: the superseded opus 4.x entries are gone from the picker. They are
53
+ // BUTTONS only - /model takes a typed name verbatim (core/handlers.js reads
54
+ // tail without consulting this list), so the rollback path survives removal.
55
+ for (const retired of ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"]) {
56
+ assert.ok(!provider.modelChoices().includes(retired), `retired model still offered: ${retired}`);
57
+ }
58
+ // These two are NOT picker garnish - they are what tierModel returns for
59
+ // utility work, so they must survive any picker trim.
60
+ assert.ok(provider.modelChoices().includes("claude-haiku-4-5-20251001"), "haiku stays: it is the low tier");
52
61
 
53
62
  // The SHIPPED default, asserted on the real module rather than the fixture above,
54
63
  // so changing the constant cannot pass unnoticed. Read from source because the
@@ -65,8 +74,15 @@ assert.match(
65
74
  // stop a default bump from silently repricing recall, pack review and the enforcer.
66
75
  assert.strictEqual(claudeProvider.tierModel("low"), "claude-haiku-4-5-20251001", "low-tier utility work stays on haiku");
67
76
  assert.strictEqual(claudeProvider.tierModel("medium"), "claude-sonnet-4-6", "medium-tier utility work stays on sonnet");
68
- if (!process.env.CLAUDE_MODEL && !claudeSource.includes("__fixture__")) {
69
- assert.ok(CLAUDE_MODELS.includes(claudeProvider.defaultModel()), "the resolved default is a model the picker offers");
77
+ // Guard on the RESOLVED config, not process.env: loadEnv folds ~/.open-claudia/.env
78
+ // over process.env, so an operator pin lives in config.config and never appears in
79
+ // process.env. The 3.4.20 version of this check tested the wrong one and passed
80
+ // locally on a machine that was in fact pinned - fixed in 3.4.21.
81
+ if (!require("./core/config").config.CLAUDE_MODEL) {
82
+ assert.ok(
83
+ CLAUDE_MODELS.includes(claudeProvider.defaultModel()),
84
+ "with no operator override, the resolved default must be a model the picker offers",
85
+ );
70
86
  }
71
87
  assert.deepStrictEqual(provider.compatibilityStatus(), {
72
88
  state: "compatible",
@@ -487,7 +487,14 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
487
487
  // entrypoint never writes one, so the constant is what pods resolve; the pod
488
488
  // image installs Claude Code unpinned, which must be >= 2.1.280 or the model is
489
489
  // refused with an API 400 rather than degrading.
490
- assert.strictEqual(pkg.version, "3.4.20", "release version must remain unchanged without explicit approval");
490
+ // 3.4.21 approved by Sumeet 2026-09-23 ("I think we can get rid of the 4th gen",
491
+ // then "Your call" when asked to confirm the scope): drop claude-opus-4-8, 4-7
492
+ // and 4-6 from the Claude picker. Deliberately NOT dropped: claude-sonnet-4-6
493
+ // and claude-haiku-4-5, which tierModel returns for medium/low utility work -
494
+ // removing those would push recall, pack review and the enforcer onto opus.
495
+ // Removal is cosmetic by design: /model accepts a typed name without checking
496
+ // the list, so `/model claude-opus-4-8` remains a working rollback.
497
+ assert.strictEqual(pkg.version, "3.4.22", "release version must remain unchanged without explicit approval");
491
498
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
492
499
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
493
500
  }