johns-harness 2026.9.11 → 2026.9.13

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.
Files changed (49) hide show
  1. package/README.md +7 -1
  2. package/dist/auth-profiles-5CHn7vq1.js +26 -3
  3. package/dist/codex-oauth-device.js +39 -9
  4. package/dist/codex-oauth-nudge.js +3 -2
  5. package/dist/codex-oauth-owner.js +1 -0
  6. package/dist/commands-registry-C78-tPS3.js +1 -0
  7. package/dist/commands-registry-D0Jkz-Sn.js +1 -0
  8. package/dist/commands-registry-DJWLO-6B.js +1 -0
  9. package/dist/commands-registry-l51VuG0m.js +1 -0
  10. package/dist/compact-B247y5Qt.js +10 -1
  11. package/dist/config-koj5M_EO.js +64 -4
  12. package/dist/daemon-cli.js +64 -4
  13. package/dist/manifest-registry-BMEqbkWA.js +48 -1
  14. package/dist/manifest-registry-Ba187z7Z.js +48 -1
  15. package/dist/model-selection-BGlGpPgM.js +74 -4
  16. package/dist/model-selection-BU6wl1le.js +74 -4
  17. package/dist/model-selection-L7RMwsG-.js +26 -3
  18. package/dist/models-CxHdes0w.js +1 -0
  19. package/dist/models-cli-DZnoM8Ln.js +1 -0
  20. package/dist/models-cli-U89juIRK.js +1 -0
  21. package/dist/pi-embedded-C6ITuRXf.js +10 -1
  22. package/dist/pi-embedded-DoQsYfIY.js +10 -1
  23. package/dist/plugin-sdk/commands-registry-B4ZP4Y1B.js +1 -0
  24. package/dist/plugin-sdk/commands-registry-B8Og-fWU.js +1 -0
  25. package/dist/plugin-sdk/commands-registry-BoAXxFpH.js +1 -0
  26. package/dist/plugin-sdk/commands-registry-Ca1h6XvV.js +1 -0
  27. package/dist/plugin-sdk/commands-registry-Do-Y2EhN.js +1 -0
  28. package/dist/plugin-sdk/commands-registry-DpRs-E_M.js +1 -0
  29. package/dist/plugin-sdk/config-4Fgm-yEH.js +74 -4
  30. package/dist/plugin-sdk/config-Di-lyCdh.js +74 -4
  31. package/dist/plugin-sdk/discord.js +64 -4
  32. package/dist/plugin-sdk/dispatch-BP0viZiL.js +10 -1
  33. package/dist/plugin-sdk/dispatch-CQsjmw7g.js +10 -1
  34. package/dist/plugin-sdk/dispatch-Cerq29sy.js +10 -1
  35. package/dist/plugin-sdk/dispatch-Cndjtt0g.js +10 -1
  36. package/dist/plugin-sdk/dispatch-UogiJYul.js +10 -1
  37. package/dist/plugin-sdk/mattermost.js +1 -0
  38. package/dist/plugin-sdk/model-auth-CX9cPHdC.js +74 -4
  39. package/dist/plugin-sdk/model-auth-Ciehz0x5.js +74 -4
  40. package/dist/plugin-sdk/model-auth-DVyo6JSX.js +74 -4
  41. package/dist/plugin-sdk/model-auth-kiHYHGrs.js +74 -4
  42. package/dist/plugin-sdk/model-selection-Dbj4jDYM.js +74 -4
  43. package/dist/plugin-sdk/reply-DbZnH8-h.js +10 -1
  44. package/dist/reply-C5LKjXcC.js +10 -1
  45. package/docs/PATCHES.md +21 -1
  46. package/extensions/mr-memory/johnness.plugin.json +1 -0
  47. package/package.json +1 -1
  48. package/vendor/patched-deps/@mariozechner/pi-ai/dist/models.generated.js +242 -0
  49. package/vendor/patched-deps/@mariozechner/pi-ai/dist/models.js +5 -0
package/README.md CHANGED
@@ -45,12 +45,18 @@ The wizard sets up credentials, workspace, gateway, and channels. It asks for yo
45
45
  | Connect | `johnness channels add --channel telegram --token "<bot token>"` | Connects a chat channel. Discord, Slack, WhatsApp, Signal, iMessage, Matrix, and more are supported. |
46
46
  | Check | `johnness doctor` | Health checks and quick fixes for the gateway and channels. |
47
47
 
48
+ <!-- JOHNNESS_PATCH_DEVICE_LOGIN_60B -->
48
49
  ### Signing in from chat
49
50
  Owners and allowlisted senders can send `/login` (or `/relogin`) in Telegram, Discord, or another chat channel.
50
- Open the link, enter the device code, and sign in to ChatGPT; the harness reconnects automatically.
51
+ Open the link, sign in to ChatGPT if asked, then enter the device code on the same page and tap Approve. In Telegram, tap the code to copy it.
52
+ Enable "Device code authorization for Codex" once in chatgpt.com > Settings > Security; if it was disabled, send `/login` again for a fresh code.
51
53
  For headless installs, run `johnness models auth login --device`; no browser or localhost callback needed.
52
54
  Expired ChatGPT credentials trigger an owner notice with the `/login` recovery command.
53
55
 
56
+ <!-- JOHNNESS_PATCH_DEFAULTS_61 -->
57
+ ### Batteries included
58
+ The harness ships the current model catalog and the short aliases `opus`, `sonnet`, `fable`, `astra`, `sol`, `luna`, `terra`, and `gpt`, so `/model opus` works on a fresh install with no `models.providers` block. Channels you configure load without a `plugins.allow` list; only an explicit `enabled: false` or `plugins.deny` turns one off. Updating the harness updates the catalog and aliases for every agent.
59
+
54
60
  ### From source
55
61
 
56
62
  ```bash
@@ -3536,6 +3536,9 @@ async function resolveImplicitBedrockProvider(params) {
3536
3536
  //#region src/agents/model-selection.ts
3537
3537
  const log$1 = createSubsystemLogger("model-selection");
3538
3538
  const ANTHROPIC_MODEL_ALIASES = {
3539
+ "opus-5": "claude-opus-5", // JOHNNESS_PATCH_DEFAULTS_61
3540
+ "sonnet-5": "claude-sonnet-5",
3541
+ "fable-5.1": "claude-fable-5-1",
3539
3542
  "opus-4.6": "claude-opus-4-6",
3540
3543
  "opus-4.5": "claude-opus-4-5",
3541
3544
  "sonnet-4.6": "claude-sonnet-4-6",
@@ -3683,6 +3686,16 @@ function resolveModelRefFromString(params) {
3683
3686
  ref: aliasMatch.ref,
3684
3687
  alias: aliasMatch.alias
3685
3688
  };
3689
+ // JOHNNESS_PATCH_DEFAULTS_61: fall back to the shipped aliases (opus, sonnet, fable,
3690
+ // astra, sol, luna, terra, gpt) when the user config defines none.
3691
+ const builtinTarget = DEFAULT_MODEL_ALIASES[aliasKey];
3692
+ if (builtinTarget) {
3693
+ const builtinRef = parseModelRef(builtinTarget, params.defaultProvider);
3694
+ if (builtinRef) return {
3695
+ ref: builtinRef,
3696
+ alias: aliasKey
3697
+ };
3698
+ }
3686
3699
  }
3687
3700
  const parsed = parseModelRef(model, params.defaultProvider);
3688
3701
  if (!parsed) return null;
@@ -5081,9 +5094,18 @@ let defaultWarnState = { warned: false };
5081
5094
  const DEFAULT_MODEL_ALIASES = {
5082
5095
  muse: "meta/muse-spark-1.3", // JOHNNESS_PATCH_MUSE_SPARK
5083
5096
  "muse-spark": "meta/muse-spark-1.3",
5084
- opus: "anthropic/claude-opus-4-6",
5085
- sonnet: "anthropic/claude-sonnet-4-6",
5086
- gpt: "openai/gpt-5.4",
5097
+ // JOHNNESS_PATCH_DEFAULTS_61: current model aliases ship with the harness; user config
5098
+ // aliases (agents.defaults.models[*].alias) still take precedence.
5099
+ opus: "anthropic/claude-opus-5",
5100
+ sonnet: "anthropic/claude-sonnet-5",
5101
+ opus46: "anthropic/claude-opus-4-6",
5102
+ sonnet46: "anthropic/claude-sonnet-4-6",
5103
+ fable: "anthropic/claude-fable-5-1",
5104
+ astra: "openai-codex/gpt-6-astra",
5105
+ sol: "openai-codex/gpt-5.6-sol",
5106
+ luna: "openai-codex/gpt-5.6-luna",
5107
+ terra: "openai-codex/gpt-5.6-terra",
5108
+ gpt: "openai-codex/gpt-5.5",
5087
5109
  "gpt-mini": "openai/gpt-5-mini",
5088
5110
  gemini: "google/gemini-3.1-pro-preview",
5089
5111
  "gemini-flash": "google/gemini-3-flash-preview",
@@ -13762,6 +13784,7 @@ function validateConfigObjectWithPluginsBase(raw, opts) {
13762
13784
  workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)) ?? void 0
13763
13785
  });
13764
13786
  for (const diag of registry.diagnostics) {
13787
+ if (diag.level === "info") continue; // JOHNNESS_PATCH_DEFAULTS_61
13765
13788
  let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
13766
13789
  if (!diag.pluginId && diag.message.includes("plugin path not found")) path = "plugins.load.paths";
13767
13790
  const message = `${diag.pluginId ? `plugin ${diag.pluginId}` : "plugin"}: ${diag.message}`;
@@ -1,4 +1,5 @@
1
1
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
2
3
  // Native OpenAI device flow, verified against Codex 0.154.0 device_code_auth.rs.
3
4
  // OpenAI uses deviceauth + an authorization-code/PKCE exchange, NOT RFC 8628.
4
5
  import path from 'node:path';
@@ -23,6 +24,23 @@ export function redactLoginEmail(email) {
23
24
  const [local, domain] = email.split('@');
24
25
  return `${local.slice(0, 1)}***@${domain.slice(0, 1)}***`;
25
26
  }
27
+ export const DEVICE_LOGIN_SECURITY_FIX = "Go to chatgpt.com > Settings > Security, turn on 'Device code authorization for Codex'";
28
+ export function formatDeviceLoginInstructions(login, { email, chat = false } = {}) {
29
+ const account = typeof email === 'string' && email.includes('@') ? redactLoginEmail(email) : 'the same account as before';
30
+ const code = chat ? `\`${login.userCode}\`` : login.userCode;
31
+ const retry = chat ? 'send /login again' : 'run johnness models auth login --device again';
32
+ const minutes = Math.max(1, Math.ceil((login.expiresAt - Date.now()) / 60000));
33
+ return `Let's sign you back into ChatGPT.
34
+ 1. Open ${login.verificationUri} on your phone or computer.
35
+ 2. Sign in with your ChatGPT account (${account}) if asked.
36
+ 3. On the same page, enter this code${chat ? ' (tap the code to copy it)' : ''}:
37
+ ${code}
38
+ 4. Tap Approve.
39
+ Code is good for ${minutes} minute${minutes === 1 ? '' : 's'}. If you see 'Device code authorization is disabled', ${DEVICE_LOGIN_SECURITY_FIX.charAt(0).toLowerCase() + DEVICE_LOGIN_SECURITY_FIX.slice(1)}, then ${retry} for a fresh code.`;
40
+ }
41
+ function hasDeviceLoginSecurityFix(error) {
42
+ return ['device_login_unavailable', 'device_code_authorization_disabled'].includes(error?.reason);
43
+ }
26
44
  export async function startDeviceLogin({ provider = PROVIDER, fetch: fetchImpl = globalThis.fetch, now = Date.now, sleep: pause = sleep, signal } = {}) {
27
45
  if (provider !== PROVIDER) fail('unsupported_provider');
28
46
  async function request(url, body, form = false, remaining = 30000) {
@@ -36,7 +54,12 @@ export async function startDeviceLogin({ provider = PROVIDER, fetch: fetchImpl =
36
54
  }
37
55
  const started = now();
38
56
  const { response, data } = await request(`${ISSUER}/api/accounts/deviceauth/usercode`, { client_id: CLIENT_ID });
39
- if (!response.ok) fail(response.status === 404 ? 'device_login_unavailable' : 'request_failed', response.status);
57
+ if (!response.ok) {
58
+ // Only classify the initial request, never reinterpret polling's bare 403/404.
59
+ const messages = [data?.message, data?.error_description, data?.error?.message, data?.error];
60
+ if (response.status === 403 && messages.some(message => typeof message === 'string' && /device code authorization is disabled|security settings do not allow device code authorization for codex/i.test(message))) fail('device_code_authorization_disabled', response.status);
61
+ fail(response.status === 404 ? 'device_login_unavailable' : 'request_failed', response.status);
62
+ }
40
63
  const userCode = data.user_code ?? data.usercode;
41
64
  if (typeof data.device_auth_id !== 'string' || !data.device_auth_id || typeof userCode !== 'string' || !/^[A-Za-z0-9-]{4,32}$/.test(userCode)) fail('invalid_response');
42
65
  const expiresAt = started + TTL;
@@ -166,11 +189,16 @@ export async function getOrStartDeviceLogin(params = {}) {
166
189
  }
167
190
  export async function runDeviceLoginCli(opts, runtime, params = {}) {
168
191
  if (opts.provider && opts.provider !== PROVIDER) fail('unsupported_provider');
169
- const login = await getOrStartDeviceLogin(params);
170
- runtime.log(`Open ${login.verificationUri} and enter code ${login.userCode} (expires in ${Math.max(1, Math.ceil((login.expiresAt - Date.now()) / 60000))} min)`);
171
- const result = await login.complete();
172
- runtime.log(`Signed in as ${redactLoginEmail(result.credential.email)}`);
173
- return result;
192
+ try {
193
+ const login = await getOrStartDeviceLogin(params);
194
+ runtime.log(formatDeviceLoginInstructions(login, { email: selectDeviceLoginProfile(params).previous?.email }));
195
+ const result = await login.complete();
196
+ runtime.log(`Signed in as ${redactLoginEmail(result.credential.email)}`);
197
+ return result;
198
+ } catch (error) {
199
+ if (hasDeviceLoginSecurityFix(error)) runtime.log(`${DEVICE_LOGIN_SECURITY_FIX}, then run johnness models auth login --device again for a fresh code.`);
200
+ throw error;
201
+ }
174
202
  }
175
203
  export async function handleDeviceLoginCommand(params, allowTextCommands, { routeReply, callGateway, log = console.info, ...options } = {}) {
176
204
  if (!allowTextCommands || !/^\/(login|relogin)(?:\s|$)/i.test(params.command.commandBodyNormalized)) return null;
@@ -185,7 +213,7 @@ export async function handleDeviceLoginCommand(params, allowTextCommands, { rout
185
213
  if (!login.notifiedRoutes.has(routeKey)) {
186
214
  login.notifiedRoutes.add(routeKey);
187
215
  // Never keep the command lane occupied for the 15-minute authorization window.
188
- void login.complete().then(() => 'Signed in. I\'m back.', error => error.reason === 'expired' ? 'That code expired. Send /login again.' : error.reason === 'denied' ? 'Login was denied. Send /login to try again.' : 'Login failed. Send /login to try again.').then(async text => {
216
+ void login.complete().then(() => 'Signed in. I\'m back.', error => error.reason === 'expired' ? "That code expired (they last 15 minutes). Send /login and I'll give you a fresh one." : error.reason === 'denied' ? 'That sign-in was declined. Send /login to try again.' : 'Login failed. Send /login to try again.').then(async text => {
189
217
  const result = ["webchat", "internal"].includes(route.channel) && callGateway
190
218
  ? await callGateway({ method: "chat.inject", params: { sessionKey: route.sessionKey, message: text } })
191
219
  : await routeReply({ ...route, payload: { text } });
@@ -194,6 +222,8 @@ export async function handleDeviceLoginCommand(params, allowTextCommands, { rout
194
222
  }).catch(() => log('[codex-device-login] completion delivery failed; send /login again.'));
195
223
  }
196
224
  log('[codex-device-login] /login recognized; code issued (code and identity redacted)');
197
- return reply(`Open ${login.verificationUri} and enter code ${login.userCode}. Good for ${Math.max(1, Math.ceil((login.expiresAt - Date.now()) / 60000))} minutes.`);
198
- } catch { return reply('Could not start ChatGPT login. Send /login to try again.'); }
225
+ return reply(formatDeviceLoginInstructions(login, { email: selectDeviceLoginProfile({ ...options, agentDir: params.agentDir }).previous?.email, chat: true }));
226
+ } catch (error) {
227
+ return reply(hasDeviceLoginSecurityFix(error) ? `${DEVICE_LOGIN_SECURITY_FIX}, then send /login again for a fresh code.` : 'Could not start ChatGPT login. Send /login to try again.');
228
+ }
199
229
  }
@@ -1,8 +1,9 @@
1
1
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
  import { transactAuthStore, sameCredential } from './auth-store-transactions.js';
5
- export const LOGIN_NUDGE_TEXT = 'My ChatGPT login expired. Send /login to fix it.';
6
+ export const LOGIN_NUDGE_TEXT = "My ChatGPT login expired. Send /login and I'll walk you through signing back in (takes about a minute).";
6
7
  const DEAD = new Set(['refresh_token_reused', 'invalid_grant', 'refresh_token_expired', 'refresh_token_invalidated', 'expired_no_refresh']);
7
8
  export function resolveLoginOwnerRoute(cfg = {}) {
8
9
  const owners = cfg.commands?.ownerAllowFrom ?? [];
@@ -48,7 +49,7 @@ export async function notifyDeadCodexChain(params = {}) {
48
49
  }
49
50
  }
50
51
  const route = resolveLoginOwnerRoute(cfg);
51
- if (!route) { log('[codex-device-login] My ChatGPT login expired. Send /login to fix it. (No owner route resolved.)'); return true; }
52
+ if (!route) { log(`[codex-device-login] ${LOGIN_NUDGE_TEXT} (No owner route resolved.)`); return true; }
52
53
  if (params.send) await params.send({ ...route, text: LOGIN_NUDGE_TEXT, cfg });
53
54
  else {
54
55
  const { deliverOutboundPayloads } = await import('./deliver-runtime-JZdz0coW.js');
@@ -1,4 +1,5 @@
1
1
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
2
3
  import { notifyDeadCodexChain } from './codex-oauth-nudge.js';
3
4
  // JOHNNESS_PATCH_CODEX_OAUTH_OWNERSHIP_58
4
5
  import fs from 'node:fs';
@@ -1,4 +1,5 @@
1
1
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
2
3
  import { l as escapeRegExp } from "./utils-DC4zYvW0.js";
3
4
  import { i as listThinkingLevels } from "./thinking-BYwvlJ3S.js";
4
5
  import { Gi as DEFAULT_PROVIDER, Wi as DEFAULT_MODEL, h as resolveConfiguredModelRef } from "./model-selection-L7RMwsG-.js";
@@ -1,4 +1,5 @@
1
1
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
2
3
  import { D as isPlainObject, u as escapeRegExp } from "./logger-Bj0Xl6pn.js";
3
4
  import { Fr as DEFAULT_MODEL, Ir as DEFAULT_PROVIDER, l as resolveConfiguredModelRef, nn as getActivePluginRegistry } from "./model-selection-BGlGpPgM.js";
4
5
  import { f as listChannelDocks, r as listThinkingLevels } from "./thinking-CAzdgmNV.js";
@@ -1,4 +1,5 @@
1
1
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
2
3
  import { D as isPlainObject, u as escapeRegExp } from "./logger-BW8uLq6f.js";
3
4
  import { Mr as DEFAULT_PROVIDER, jr as DEFAULT_MODEL, tn as getActivePluginRegistry, u as resolveConfiguredModelRef } from "./model-selection-BU6wl1le.js";
4
5
  import { f as listChannelDocks, r as listThinkingLevels } from "./thinking-B5B36ffe.js";
@@ -1,4 +1,5 @@
1
1
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
2
3
  import { Bi as DEFAULT_PROVIDER, Or as resolveConfiguredModelRef, zi as DEFAULT_MODEL } from "./auth-profiles-5CHn7vq1.js";
3
4
  import { c as escapeRegExp } from "./utils-C5WN6czr.js";
4
5
  import { u as getActivePluginRegistry } from "./registry-DoLLbW4m.js";
@@ -1,4 +1,5 @@
1
1
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
2
3
  import { handleDeviceLoginCommand } from "./codex-oauth-device.js";
3
4
  import { E as normalizeLogLevel, a as logVerbose, c as shouldLogVerbose, k as resolvePreferredJohnnessTmpDir, m as getChildLogger, p as theme, t as danger, u as warn } from "./globals-Bv4ZcVWM.js";
4
5
  import { g as resolveStateDir, r as STATE_DIR, u as resolveGatewayPort, y as resolveRequiredHomeDir } from "./paths-BfR2LXbA.js";
@@ -92631,7 +92632,10 @@ function isTrackedByProvenance(params) {
92631
92632
  function warnWhenAllowlistIsOpen(params) {
92632
92633
  if (!params.pluginsEnabled) return;
92633
92634
  if (params.allow.length > 0) return;
92634
- const nonBundled = params.discoverablePlugins.filter((entry) => entry.origin !== "bundled");
92635
+ // JOHNNESS_PATCH_DEFAULTS_61: plugins the config already implies (memory slot, enabled
92636
+ // entries, install records) are trusted by construction and never warned about.
92637
+ const implied = params.impliedIds ?? new Set();
92638
+ const nonBundled = params.discoverablePlugins.filter((entry) => entry.origin !== "bundled" && !implied.has(entry.id));
92635
92639
  if (nonBundled.length === 0) return;
92636
92640
  const preview = nonBundled.slice(0, 6).map((entry) => `${entry.id} (${entry.source})`).join(", ");
92637
92641
  const extra = nonBundled.length > 6 ? ` (+${nonBundled.length - 6} more)` : "";
@@ -92729,6 +92733,11 @@ function loadJohnnessPlugins(options = {}) {
92729
92733
  logger,
92730
92734
  pluginsEnabled: normalized.enabled,
92731
92735
  allow: normalized.allow,
92736
+ impliedIds: new Set([
92737
+ ...typeof normalized.slots.memory === "string" ? [normalized.slots.memory] : [],
92738
+ ...Object.entries(normalized.entries).filter(([, entry]) => entry?.enabled === true).map(([id]) => id),
92739
+ ...Object.keys(cfg.plugins?.installs ?? {})
92740
+ ]), // JOHNNESS_PATCH_DEFAULTS_61
92732
92741
  discoverablePlugins: manifestRegistry.plugins.map((plugin) => ({
92733
92742
  id: plugin.id,
92734
92743
  source: plugin.source,
@@ -1573,6 +1573,9 @@ function normalizeGoogleModelId(id) {
1573
1573
  }
1574
1574
  createSubsystemLogger("model-selection");
1575
1575
  const ANTHROPIC_MODEL_ALIASES = {
1576
+ "opus-5": "claude-opus-5", // JOHNNESS_PATCH_DEFAULTS_61
1577
+ "sonnet-5": "claude-sonnet-5",
1578
+ "fable-5.1": "claude-fable-5-1",
1576
1579
  "opus-4.6": "claude-opus-4-6",
1577
1580
  "opus-4.5": "claude-opus-4-5",
1578
1581
  "sonnet-4.6": "claude-sonnet-4-6",
@@ -1788,9 +1791,18 @@ let defaultWarnState = { warned: false };
1788
1791
  const DEFAULT_MODEL_ALIASES = {
1789
1792
  muse: "meta/muse-spark-1.3", // JOHNNESS_PATCH_MUSE_SPARK
1790
1793
  "muse-spark": "meta/muse-spark-1.3",
1791
- opus: "anthropic/claude-opus-4-6",
1792
- sonnet: "anthropic/claude-sonnet-4-6",
1793
- gpt: "openai/gpt-5.4",
1794
+ // JOHNNESS_PATCH_DEFAULTS_61: current model aliases ship with the harness; user config
1795
+ // aliases (agents.defaults.models[*].alias) still take precedence.
1796
+ opus: "anthropic/claude-opus-5",
1797
+ sonnet: "anthropic/claude-sonnet-5",
1798
+ opus46: "anthropic/claude-opus-4-6",
1799
+ sonnet46: "anthropic/claude-sonnet-4-6",
1800
+ fable: "anthropic/claude-fable-5-1",
1801
+ astra: "openai-codex/gpt-6-astra",
1802
+ sol: "openai-codex/gpt-5.6-sol",
1803
+ luna: "openai-codex/gpt-5.6-luna",
1804
+ terra: "openai-codex/gpt-5.6-terra",
1805
+ gpt: "openai-codex/gpt-5.5",
1794
1806
  "gpt-mini": "openai/gpt-5-mini",
1795
1807
  gemini: "google/gemini-3.1-pro-preview",
1796
1808
  "gemini-flash": "google/gemini-3-flash-preview",
@@ -4407,7 +4419,9 @@ function isBundledChannelEnabledByChannelConfig(cfg, pluginId) {
4407
4419
  if (!channelId) return false;
4408
4420
  const entry = cfg.channels?.[channelId];
4409
4421
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
4410
- return entry.enabled === true;
4422
+ // JOHNNESS_PATCH_DEFAULTS_61: a configured channel block implies the bundled channel
4423
+ // plugin. Only an explicit enabled:false opts out; no plugins.allow needed.
4424
+ return entry.enabled !== false;
4411
4425
  }
4412
4426
  function resolveEffectiveEnableState(params) {
4413
4427
  const base = resolveEnableState(params.id, params.origin, params.config);
@@ -5091,6 +5105,18 @@ function safeStatMtimeMs(filePath) {
5091
5105
  return null;
5092
5106
  }
5093
5107
  }
5108
+ // JOHNNESS_PATCH_DEFAULTS_61: loose semver compare for bundled-vs-installed plugin dedupe.
5109
+ function compareJohnnessPluginVersions(a, b) {
5110
+ const parse = (value) => String(value ?? "").trim().replace(/^v/i, "").split(/[-+]/)[0].split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isFinite(part) ? part : 0);
5111
+ const left = parse(a);
5112
+ const right = parse(b);
5113
+ const len = Math.max(left.length, right.length, 1);
5114
+ for (let i = 0; i < len; i += 1) {
5115
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
5116
+ if (diff !== 0) return diff > 0 ? 1 : -1;
5117
+ }
5118
+ return 0;
5119
+ }
5094
5120
  function normalizeManifestLabel(raw) {
5095
5121
  const trimmed = raw?.trim();
5096
5122
  return trimmed ? trimmed : void 0;
@@ -5187,6 +5213,39 @@ function loadPluginManifestRegistry(params) {
5187
5213
  }
5188
5214
  continue;
5189
5215
  }
5216
+ // JOHNNESS_PATCH_DEFAULTS_61: the same plugin id bundled with the harness and installed
5217
+ // under the state dir is the normal shape (mr-memory). The installed copy stays
5218
+ // authoritative (family 59) with no warning; when the bundled copy is newer by
5219
+ // package version, one info line suggests updating. Two non-bundled copies
5220
+ // still warn as before.
5221
+ const existingBundled = existing.candidate.origin === "bundled";
5222
+ const candidateBundled = candidate.origin === "bundled";
5223
+ if (existingBundled !== candidateBundled) {
5224
+ const existingRecord = records[existing.recordIndex];
5225
+ const candidateVersion = normalizeManifestLabel(manifest.version) ?? candidate.packageVersion;
5226
+ const bundledVersion = existingBundled ? existingRecord?.version : candidateVersion;
5227
+ const installedVersion = existingBundled ? candidateVersion : existingRecord?.version;
5228
+ if (existingBundled) {
5229
+ records[existing.recordIndex] = buildRecord({
5230
+ manifest,
5231
+ candidate,
5232
+ manifestPath: manifestRes.manifestPath,
5233
+ schemaCacheKey,
5234
+ configSchema
5235
+ });
5236
+ seenIds.set(manifest.id, {
5237
+ candidate,
5238
+ recordIndex: existing.recordIndex
5239
+ });
5240
+ }
5241
+ if (compareJohnnessPluginVersions(bundledVersion, installedVersion) > 0) diagnostics.push({
5242
+ level: "info",
5243
+ pluginId: manifest.id,
5244
+ source: candidate.source,
5245
+ message: `installed copy ${installedVersion ?? "?"} is older than the bundled ${bundledVersion ?? "?"}; run "johnness plugins update ${manifest.id}" to upgrade`
5246
+ });
5247
+ continue;
5248
+ }
5190
5249
  diagnostics.push({
5191
5250
  level: "warn",
5192
5251
  pluginId: manifest.id,
@@ -9576,6 +9635,7 @@ function validateConfigObjectWithPluginsBase(raw, opts) {
9576
9635
  workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)) ?? void 0
9577
9636
  });
9578
9637
  for (const diag of registry.diagnostics) {
9638
+ if (diag.level === "info") continue; // JOHNNESS_PATCH_DEFAULTS_61
9579
9639
  let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
9580
9640
  if (!diag.pluginId && diag.message.includes("plugin path not found")) path = "plugins.load.paths";
9581
9641
  const message = `${diag.pluginId ? `plugin ${diag.pluginId}` : "plugin"}: ${diag.message}`;
@@ -4724,6 +4724,9 @@ function normalizeGoogleModelId(id) {
4724
4724
  }
4725
4725
  createSubsystemLogger("model-selection");
4726
4726
  const ANTHROPIC_MODEL_ALIASES = {
4727
+ "opus-5": "claude-opus-5", // JOHNNESS_PATCH_DEFAULTS_61
4728
+ "sonnet-5": "claude-sonnet-5",
4729
+ "fable-5.1": "claude-fable-5-1",
4727
4730
  "opus-4.6": "claude-opus-4-6",
4728
4731
  "opus-4.5": "claude-opus-4-5",
4729
4732
  "sonnet-4.6": "claude-sonnet-4-6",
@@ -4939,9 +4942,18 @@ let defaultWarnState = { warned: false };
4939
4942
  const DEFAULT_MODEL_ALIASES = {
4940
4943
  muse: "meta/muse-spark-1.3", // JOHNNESS_PATCH_MUSE_SPARK
4941
4944
  "muse-spark": "meta/muse-spark-1.3",
4942
- opus: "anthropic/claude-opus-4-6",
4943
- sonnet: "anthropic/claude-sonnet-4-6",
4944
- gpt: "openai/gpt-5.4",
4945
+ // JOHNNESS_PATCH_DEFAULTS_61: current model aliases ship with the harness; user config
4946
+ // aliases (agents.defaults.models[*].alias) still take precedence.
4947
+ opus: "anthropic/claude-opus-5",
4948
+ sonnet: "anthropic/claude-sonnet-5",
4949
+ opus46: "anthropic/claude-opus-4-6",
4950
+ sonnet46: "anthropic/claude-sonnet-4-6",
4951
+ fable: "anthropic/claude-fable-5-1",
4952
+ astra: "openai-codex/gpt-6-astra",
4953
+ sol: "openai-codex/gpt-5.6-sol",
4954
+ luna: "openai-codex/gpt-5.6-luna",
4955
+ terra: "openai-codex/gpt-5.6-terra",
4956
+ gpt: "openai-codex/gpt-5.5",
4945
4957
  "gpt-mini": "openai/gpt-5-mini",
4946
4958
  gemini: "google/gemini-3.1-pro-preview",
4947
4959
  "gemini-flash": "google/gemini-3-flash-preview",
@@ -7371,7 +7383,9 @@ function isBundledChannelEnabledByChannelConfig(cfg, pluginId) {
7371
7383
  if (!channelId) return false;
7372
7384
  const entry = cfg.channels?.[channelId];
7373
7385
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
7374
- return entry.enabled === true;
7386
+ // JOHNNESS_PATCH_DEFAULTS_61: a configured channel block implies the bundled channel
7387
+ // plugin. Only an explicit enabled:false opts out; no plugins.allow needed.
7388
+ return entry.enabled !== false;
7375
7389
  }
7376
7390
  function resolveEffectiveEnableState(params) {
7377
7391
  const base = resolveEnableState(params.id, params.origin, params.config);
@@ -8055,6 +8069,18 @@ function safeStatMtimeMs(filePath) {
8055
8069
  return null;
8056
8070
  }
8057
8071
  }
8072
+ // JOHNNESS_PATCH_DEFAULTS_61: loose semver compare for bundled-vs-installed plugin dedupe.
8073
+ function compareJohnnessPluginVersions(a, b) {
8074
+ const parse = (value) => String(value ?? "").trim().replace(/^v/i, "").split(/[-+]/)[0].split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isFinite(part) ? part : 0);
8075
+ const left = parse(a);
8076
+ const right = parse(b);
8077
+ const len = Math.max(left.length, right.length, 1);
8078
+ for (let i = 0; i < len; i += 1) {
8079
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
8080
+ if (diff !== 0) return diff > 0 ? 1 : -1;
8081
+ }
8082
+ return 0;
8083
+ }
8058
8084
  function normalizeManifestLabel(raw) {
8059
8085
  const trimmed = raw?.trim();
8060
8086
  return trimmed ? trimmed : void 0;
@@ -8151,6 +8177,39 @@ function loadPluginManifestRegistry(params) {
8151
8177
  }
8152
8178
  continue;
8153
8179
  }
8180
+ // JOHNNESS_PATCH_DEFAULTS_61: the same plugin id bundled with the harness and installed
8181
+ // under the state dir is the normal shape (mr-memory). The installed copy stays
8182
+ // authoritative (family 59) with no warning; when the bundled copy is newer by
8183
+ // package version, one info line suggests updating. Two non-bundled copies
8184
+ // still warn as before.
8185
+ const existingBundled = existing.candidate.origin === "bundled";
8186
+ const candidateBundled = candidate.origin === "bundled";
8187
+ if (existingBundled !== candidateBundled) {
8188
+ const existingRecord = records[existing.recordIndex];
8189
+ const candidateVersion = normalizeManifestLabel(manifest.version) ?? candidate.packageVersion;
8190
+ const bundledVersion = existingBundled ? existingRecord?.version : candidateVersion;
8191
+ const installedVersion = existingBundled ? candidateVersion : existingRecord?.version;
8192
+ if (existingBundled) {
8193
+ records[existing.recordIndex] = buildRecord({
8194
+ manifest,
8195
+ candidate,
8196
+ manifestPath: manifestRes.manifestPath,
8197
+ schemaCacheKey,
8198
+ configSchema
8199
+ });
8200
+ seenIds.set(manifest.id, {
8201
+ candidate,
8202
+ recordIndex: existing.recordIndex
8203
+ });
8204
+ }
8205
+ if (compareJohnnessPluginVersions(bundledVersion, installedVersion) > 0) diagnostics.push({
8206
+ level: "info",
8207
+ pluginId: manifest.id,
8208
+ source: candidate.source,
8209
+ message: `installed copy ${installedVersion ?? "?"} is older than the bundled ${bundledVersion ?? "?"}; run "johnness plugins update ${manifest.id}" to upgrade`
8210
+ });
8211
+ continue;
8212
+ }
8154
8213
  diagnostics.push({
8155
8214
  level: "warn",
8156
8215
  pluginId: manifest.id,
@@ -12481,6 +12540,7 @@ function validateConfigObjectWithPluginsBase(raw, opts) {
12481
12540
  workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)) ?? void 0
12482
12541
  });
12483
12542
  for (const diag of registry.diagnostics) {
12543
+ if (diag.level === "info") continue; // JOHNNESS_PATCH_DEFAULTS_61
12484
12544
  let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
12485
12545
  if (!diag.pluginId && diag.message.includes("plugin path not found")) path = "plugins.load.paths";
12486
12546
  const message = `${diag.pluginId ? `plugin ${diag.pluginId}` : "plugin"}: ${diag.message}`;
@@ -134,7 +134,9 @@ function isBundledChannelEnabledByChannelConfig(cfg, pluginId) {
134
134
  if (!channelId) return false;
135
135
  const entry = cfg.channels?.[channelId];
136
136
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
137
- return entry.enabled === true;
137
+ // JOHNNESS_PATCH_DEFAULTS_61: a configured channel block implies the bundled channel
138
+ // plugin. Only an explicit enabled:false opts out; no plugins.allow needed.
139
+ return entry.enabled !== false;
138
140
  }
139
141
  function resolveEffectiveEnableState(params) {
140
142
  const base = resolveEnableState(params.id, params.origin, params.config);
@@ -820,6 +822,18 @@ function safeStatMtimeMs(filePath) {
820
822
  return null;
821
823
  }
822
824
  }
825
+ // JOHNNESS_PATCH_DEFAULTS_61: loose semver compare for bundled-vs-installed plugin dedupe.
826
+ function compareJohnnessPluginVersions(a, b) {
827
+ const parse = (value) => String(value ?? "").trim().replace(/^v/i, "").split(/[-+]/)[0].split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isFinite(part) ? part : 0);
828
+ const left = parse(a);
829
+ const right = parse(b);
830
+ const len = Math.max(left.length, right.length, 1);
831
+ for (let i = 0; i < len; i += 1) {
832
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
833
+ if (diff !== 0) return diff > 0 ? 1 : -1;
834
+ }
835
+ return 0;
836
+ }
823
837
  function normalizeManifestLabel(raw) {
824
838
  const trimmed = raw?.trim();
825
839
  return trimmed ? trimmed : void 0;
@@ -916,6 +930,39 @@ function loadPluginManifestRegistry(params) {
916
930
  }
917
931
  continue;
918
932
  }
933
+ // JOHNNESS_PATCH_DEFAULTS_61: the same plugin id bundled with the harness and installed
934
+ // under the state dir is the normal shape (mr-memory). The installed copy stays
935
+ // authoritative (family 59) with no warning; when the bundled copy is newer by
936
+ // package version, one info line suggests updating. Two non-bundled copies
937
+ // still warn as before.
938
+ const existingBundled = existing.candidate.origin === "bundled";
939
+ const candidateBundled = candidate.origin === "bundled";
940
+ if (existingBundled !== candidateBundled) {
941
+ const existingRecord = records[existing.recordIndex];
942
+ const candidateVersion = normalizeManifestLabel(manifest.version) ?? candidate.packageVersion;
943
+ const bundledVersion = existingBundled ? existingRecord?.version : candidateVersion;
944
+ const installedVersion = existingBundled ? candidateVersion : existingRecord?.version;
945
+ if (existingBundled) {
946
+ records[existing.recordIndex] = buildRecord({
947
+ manifest,
948
+ candidate,
949
+ manifestPath: manifestRes.manifestPath,
950
+ schemaCacheKey,
951
+ configSchema
952
+ });
953
+ seenIds.set(manifest.id, {
954
+ candidate,
955
+ recordIndex: existing.recordIndex
956
+ });
957
+ }
958
+ if (compareJohnnessPluginVersions(bundledVersion, installedVersion) > 0) diagnostics.push({
959
+ level: "info",
960
+ pluginId: manifest.id,
961
+ source: candidate.source,
962
+ message: `installed copy ${installedVersion ?? "?"} is older than the bundled ${bundledVersion ?? "?"}; run "johnness plugins update ${manifest.id}" to upgrade`
963
+ });
964
+ continue;
965
+ }
919
966
  diagnostics.push({
920
967
  level: "warn",
921
968
  pluginId: manifest.id,
@@ -134,7 +134,9 @@ function isBundledChannelEnabledByChannelConfig(cfg, pluginId) {
134
134
  if (!channelId) return false;
135
135
  const entry = cfg.channels?.[channelId];
136
136
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
137
- return entry.enabled === true;
137
+ // JOHNNESS_PATCH_DEFAULTS_61: a configured channel block implies the bundled channel
138
+ // plugin. Only an explicit enabled:false opts out; no plugins.allow needed.
139
+ return entry.enabled !== false;
138
140
  }
139
141
  function resolveEffectiveEnableState(params) {
140
142
  const base = resolveEnableState(params.id, params.origin, params.config);
@@ -820,6 +822,18 @@ function safeStatMtimeMs(filePath) {
820
822
  return null;
821
823
  }
822
824
  }
825
+ // JOHNNESS_PATCH_DEFAULTS_61: loose semver compare for bundled-vs-installed plugin dedupe.
826
+ function compareJohnnessPluginVersions(a, b) {
827
+ const parse = (value) => String(value ?? "").trim().replace(/^v/i, "").split(/[-+]/)[0].split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isFinite(part) ? part : 0);
828
+ const left = parse(a);
829
+ const right = parse(b);
830
+ const len = Math.max(left.length, right.length, 1);
831
+ for (let i = 0; i < len; i += 1) {
832
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
833
+ if (diff !== 0) return diff > 0 ? 1 : -1;
834
+ }
835
+ return 0;
836
+ }
823
837
  function normalizeManifestLabel(raw) {
824
838
  const trimmed = raw?.trim();
825
839
  return trimmed ? trimmed : void 0;
@@ -916,6 +930,39 @@ function loadPluginManifestRegistry(params) {
916
930
  }
917
931
  continue;
918
932
  }
933
+ // JOHNNESS_PATCH_DEFAULTS_61: the same plugin id bundled with the harness and installed
934
+ // under the state dir is the normal shape (mr-memory). The installed copy stays
935
+ // authoritative (family 59) with no warning; when the bundled copy is newer by
936
+ // package version, one info line suggests updating. Two non-bundled copies
937
+ // still warn as before.
938
+ const existingBundled = existing.candidate.origin === "bundled";
939
+ const candidateBundled = candidate.origin === "bundled";
940
+ if (existingBundled !== candidateBundled) {
941
+ const existingRecord = records[existing.recordIndex];
942
+ const candidateVersion = normalizeManifestLabel(manifest.version) ?? candidate.packageVersion;
943
+ const bundledVersion = existingBundled ? existingRecord?.version : candidateVersion;
944
+ const installedVersion = existingBundled ? candidateVersion : existingRecord?.version;
945
+ if (existingBundled) {
946
+ records[existing.recordIndex] = buildRecord({
947
+ manifest,
948
+ candidate,
949
+ manifestPath: manifestRes.manifestPath,
950
+ schemaCacheKey,
951
+ configSchema
952
+ });
953
+ seenIds.set(manifest.id, {
954
+ candidate,
955
+ recordIndex: existing.recordIndex
956
+ });
957
+ }
958
+ if (compareJohnnessPluginVersions(bundledVersion, installedVersion) > 0) diagnostics.push({
959
+ level: "info",
960
+ pluginId: manifest.id,
961
+ source: candidate.source,
962
+ message: `installed copy ${installedVersion ?? "?"} is older than the bundled ${bundledVersion ?? "?"}; run "johnness plugins update ${manifest.id}" to upgrade`
963
+ });
964
+ continue;
965
+ }
919
966
  diagnostics.push({
920
967
  level: "warn",
921
968
  pluginId: manifest.id,