johns-harness 2026.9.12 → 2026.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +12 -5
  2. package/dist/auth-profiles-5CHn7vq1.js +38 -3
  3. package/dist/commands-registry-C78-tPS3.js +14 -0
  4. package/dist/commands-registry-D0Jkz-Sn.js +14 -0
  5. package/dist/commands-registry-DJWLO-6B.js +14 -0
  6. package/dist/commands-registry-l51VuG0m.js +14 -0
  7. package/dist/compact-B247y5Qt.js +29 -3
  8. package/dist/config-koj5M_EO.js +76 -4
  9. package/dist/daemon-cli.js +76 -4
  10. package/dist/image-generate.js +269 -0
  11. package/dist/manifest-registry-BMEqbkWA.js +48 -1
  12. package/dist/manifest-registry-Ba187z7Z.js +48 -1
  13. package/dist/model-selection-BGlGpPgM.js +86 -4
  14. package/dist/model-selection-BU6wl1le.js +86 -4
  15. package/dist/model-selection-L7RMwsG-.js +38 -3
  16. package/dist/pi-embedded-C6ITuRXf.js +29 -3
  17. package/dist/pi-embedded-DoQsYfIY.js +29 -3
  18. package/dist/pi-embedded-helpers-CkWXaNFn.js +9 -0
  19. package/dist/pi-embedded-helpers-DLLQ3MsR.js +9 -0
  20. package/dist/plugin-sdk/bluebubbles.js +12 -0
  21. package/dist/plugin-sdk/commands-registry-B4ZP4Y1B.js +14 -0
  22. package/dist/plugin-sdk/commands-registry-B8Og-fWU.js +14 -0
  23. package/dist/plugin-sdk/commands-registry-BoAXxFpH.js +14 -0
  24. package/dist/plugin-sdk/commands-registry-Ca1h6XvV.js +14 -0
  25. package/dist/plugin-sdk/commands-registry-Do-Y2EhN.js +14 -0
  26. package/dist/plugin-sdk/commands-registry-DpRs-E_M.js +14 -0
  27. package/dist/plugin-sdk/config-4Fgm-yEH.js +86 -4
  28. package/dist/plugin-sdk/config-Di-lyCdh.js +86 -4
  29. package/dist/plugin-sdk/discord.js +76 -4
  30. package/dist/plugin-sdk/dispatch-BP0viZiL.js +29 -3
  31. package/dist/plugin-sdk/dispatch-CQsjmw7g.js +29 -3
  32. package/dist/plugin-sdk/dispatch-Cerq29sy.js +29 -3
  33. package/dist/plugin-sdk/dispatch-Cndjtt0g.js +29 -3
  34. package/dist/plugin-sdk/dispatch-UogiJYul.js +29 -3
  35. package/dist/plugin-sdk/feishu.js +12 -0
  36. package/dist/plugin-sdk/googlechat.js +12 -0
  37. package/dist/plugin-sdk/imessage.js +12 -0
  38. package/dist/plugin-sdk/mattermost.js +14 -0
  39. package/dist/plugin-sdk/model-auth-CX9cPHdC.js +86 -4
  40. package/dist/plugin-sdk/model-auth-Ciehz0x5.js +86 -4
  41. package/dist/plugin-sdk/model-auth-DVyo6JSX.js +86 -4
  42. package/dist/plugin-sdk/model-auth-kiHYHGrs.js +86 -4
  43. package/dist/plugin-sdk/model-selection-Dbj4jDYM.js +86 -4
  44. package/dist/plugin-sdk/pi-embedded-helpers-B89Z8pGU.js +9 -0
  45. package/dist/plugin-sdk/pi-embedded-helpers-BDFUaaiY.js +9 -0
  46. package/dist/plugin-sdk/pi-embedded-helpers-BnFhV3or.js +9 -0
  47. package/dist/plugin-sdk/pi-embedded-helpers-CRUVFL1T.js +9 -0
  48. package/dist/plugin-sdk/pi-embedded-helpers-CY7PsNpl.js +9 -0
  49. package/dist/plugin-sdk/pi-embedded-helpers-Dira0ZRW.js +9 -0
  50. package/dist/plugin-sdk/reply-DbZnH8-h.js +29 -3
  51. package/dist/plugin-sdk/signal.js +12 -0
  52. package/dist/plugin-sdk/slack.js +12 -0
  53. package/dist/plugin-sdk/telegram.js +12 -0
  54. package/dist/plugin-sdk/tlon.js +12 -0
  55. package/dist/plugin-sdk/twitch.js +12 -0
  56. package/dist/plugin-sdk/zalo.js +12 -0
  57. package/dist/plugin-sdk/zalouser.js +12 -0
  58. package/dist/redact-snapshot-CjeFvdcl.js +5 -0
  59. package/dist/redact-snapshot-DuqfRMA9.js +5 -0
  60. package/dist/reply-C5LKjXcC.js +29 -3
  61. package/dist/runtime-path.js +58 -0
  62. package/dist/tool-catalog-CFg6jrp9.js +9 -0
  63. package/dist/tool-catalog-DE9Q8xiB.js +9 -0
  64. package/dist/update-cli-Cm_l3W29.js +1 -1
  65. package/dist/update-command.js +276 -0
  66. package/docs/PATCHES.md +31 -0
  67. package/extensions/mr-memory/johnness.plugin.json +1 -0
  68. package/package.json +1 -1
  69. package/skills/openai-image-gen/SKILL.md +3 -3
  70. package/skills/openai-image-gen/scripts/gen.py +1 -1
  71. package/vendor/patched-deps/@mariozechner/pi-ai/dist/models.generated.js +279 -0
  72. package/vendor/patched-deps/@mariozechner/pi-ai/dist/models.js +5 -0
  73. package/skills/nano-banana-pro/SKILL.md +0 -65
  74. package/skills/nano-banana-pro/scripts/generate_image.py +0 -235
  75. package/skills/nano-banana-pro/scripts/test_generate_image.py +0 -36
package/README.md CHANGED
@@ -53,6 +53,13 @@ Enable "Device code authorization for Codex" once in chatgpt.com > Settings > Se
53
53
  For headless installs, run `johnness models auth login --device`; no browser or localhost callback needed.
54
54
  Expired ChatGPT credentials trigger an owner notice with the `/login` recovery command.
55
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. Image generation and editing use GPT Image 2 through the `image_generate` tool: `openai-codex/gpt-image-2` on the ChatGPT subscription first, `openai/gpt-image-2` on the API key second (`agents.defaults.imageGenerationModel`).
59
+
60
+ ### Updating from chat
61
+ Send `/update` to install the newest `johns-harness` on the machine the gateway runs on and restart into it; `/update 2026.9.14` pins an exact version. Owner only. The reply names the current and target versions, a second `/update` while one is running is refused, and the first message after the restart confirms the running version. On a writable host the package is reinstalled into the existing global prefix. On an immutable image with a persistent volume (Fly), it is installed into `$JOHNNESS_STATE_DIR/runtime/johnness` and the entrypoint prefers that runtime when it is newer than the image: `exec node "$(node /opt/johnness/lib/node_modules/johns-harness/dist/runtime-path.js)" gateway run`. `JOHNNESS_UPDATE_PREFIX` overrides the target. On failure the reply carries the last lines of npm output and the one command that fixes it.
62
+
56
63
  ### From source
57
64
 
58
65
  ```bash
@@ -64,8 +71,8 @@ node johnness.mjs --help
64
71
  ### Verify
65
72
 
66
73
  ```bash
67
- npm test # 701 tests against the installed dependency tree
68
- bash scripts/verify-patches.sh # 258 checks across every patch family
74
+ npm test # 767 tests against the installed dependency tree
75
+ bash scripts/verify-patches.sh # 360 checks across every patch family
69
76
  ```
70
77
 
71
78
  ## Launch a swarm with one command
@@ -111,7 +118,7 @@ Coding agents today run as isolated processes. Two agents on the same repository
111
118
  - **Memory built for production.** Tiered recall that traces from consolidated reflections down to the verbatim record, surviving compaction and shared across the swarm. Installed and enabled by default.
112
119
  - **Production authority per commit.** Promotion is granted to an exact SHA, never to a branch name, so one worker's authorization never covers another worker's commits.
113
120
  - **An append-only action log.** Agents record every consequential action to `ACTION_LOG.md` and read it before acting.
114
- - **A fixed runtime surface.** The core does not grow on a schedule. New mechanisms enter only to solve a demonstrated production failure, one narrow, tested, reversible patch at a time: 59 patch families, each with a regression test and a rollback path.
121
+ - **A fixed runtime surface.** The core does not grow on a schedule. New mechanisms enter only to solve a demonstrated production failure, one narrow, tested, reversible patch at a time: 62 patch families, each with a regression test and a rollback path.
115
122
  - **Reliability with a definition.** Bounded retries, explicit failure states, exact-once continuation, context-pressure recovery, durable delivery, and supervised gateway recovery.
116
123
  - **Coding first, phone optional.** Direct the work at a high level from a chat channel while agents plan, implement, test, coordinate, and stage releases unattended.
117
124
  - **Local voice.** Optional native Whisper transcription of inbound audio, entirely on your machine. Off by default. [Setup](docs/NATIVE-WHISPER-MUSE.md).
@@ -178,7 +185,7 @@ The log is a [`RULES.md`](RULES.md) workspace convention enforced through the ac
178
185
 
179
186
  ## Patch families
180
187
 
181
- The runtime carries 59 patch families. Each is a production fix applied at the source with a regression test, a durable patch marker, and a rollback path. A verifier runs 258 checks against the package tree and installed dependencies, and 701 tests run against the installed dependency tree.
188
+ The runtime carries 62 patch families. Each is a production fix applied at the source with a regression test, a durable patch marker, and a rollback path. A verifier runs 360 checks against the package tree and installed dependencies, and 767 tests run against the installed dependency tree.
182
189
 
183
190
  Every family follows the same six steps: reproduce the failure, trace the exact runtime path, make the smallest source-level change that restores the invariant, add a regression test and a patch marker, run the verifier, and retain rollback artifacts. The full index: [`docs/PATCHES.md`](docs/PATCHES.md).
184
191
 
@@ -195,7 +202,7 @@ Every family follows the same six steps: reproduce the failure, trace the exact
195
202
  ## Documentation
196
203
 
197
204
  - [`docs/index.md`](docs/index.md): runtime docs, from gateway and channels to plugins, nodes, and CLI reference.
198
- - [`docs/PATCHES.md`](docs/PATCHES.md): the 59 patch families, one record each.
205
+ - [`docs/PATCHES.md`](docs/PATCHES.md): the 62 patch families, one record each.
199
206
  - [`docs/NATIVE-WHISPER-MUSE.md`](docs/NATIVE-WHISPER-MUSE.md): local Whisper transcription setup.
200
207
  - [`RULES.md`](RULES.md): the default operating rules, including Rule Zero on production authorization.
201
208
 
@@ -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",
@@ -6421,6 +6443,17 @@ function applyLegacyAudioTranscriptionModel(params) {
6421
6443
  params.changes.push(params.alreadySetMessage);
6422
6444
  }
6423
6445
  const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
6446
+ {
6447
+ id: "agent.image-generation-model-v1",
6448
+ describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
6449
+ apply: (raw, changes) => {
6450
+ const defaults = getRecord(getRecord(raw.agents)?.defaults);
6451
+ if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
6452
+ const primary = defaults.imageGenerationModel.trim();
6453
+ defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
6454
+ changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
6455
+ }
6456
+ }, // JOHNNESS_PATCH_GPT_IMAGE_62
6424
6457
  {
6425
6458
  id: "agent.model-config-v2",
6426
6459
  describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
@@ -10854,6 +10887,7 @@ function isValidNonNegativeByteSizeString(value) {
10854
10887
  const AgentDefaultsSchema = z.object({
10855
10888
  model: AgentModelSchema.optional(),
10856
10889
  imageModel: AgentModelSchema.optional(),
10890
+ imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
10857
10891
  pdfModel: AgentModelSchema.optional(),
10858
10892
  pdfMaxBytesMb: z.number().positive().optional(),
10859
10893
  pdfMaxPages: z.number().int().positive().optional(),
@@ -13762,6 +13796,7 @@ function validateConfigObjectWithPluginsBase(raw, opts) {
13762
13796
  workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)) ?? void 0
13763
13797
  });
13764
13798
  for (const diag of registry.diagnostics) {
13799
+ if (diag.level === "info") continue; // JOHNNESS_PATCH_DEFAULTS_61
13765
13800
  let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
13766
13801
  if (!diag.pluginId && diag.message.includes("plugin path not found")) path = "plugins.load.paths";
13767
13802
  const message = `${diag.pluginId ? `plugin ${diag.pluginId}` : "plugin"}: ${diag.message}`;
@@ -1,3 +1,4 @@
1
+ // JOHNNESS_PATCH_UPDATE_COMMAND_62
1
2
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
3
  // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
4
  import { l as escapeRegExp } from "./utils-DC4zYvW0.js";
@@ -181,6 +182,19 @@ function buildChatCommands() {
181
182
  captureRemaining: true
182
183
  }]
183
184
  }),
185
+ defineChatCommand({
186
+ key: "update",
187
+ nativeName: "update",
188
+ description: "Install the newest johns-harness and restart (owner only).",
189
+ textAlias: "/update",
190
+ category: "management",
191
+ args: [{
192
+ name: "version",
193
+ description: "Exact version to install (default latest)",
194
+ type: "string",
195
+ required: false
196
+ }]
197
+ }),
184
198
  defineChatCommand({
185
199
  key: "login",
186
200
  nativeName: "login",
@@ -1,3 +1,4 @@
1
+ // JOHNNESS_PATCH_UPDATE_COMMAND_62
1
2
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
3
  // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
4
  import { D as isPlainObject, u as escapeRegExp } from "./logger-Bj0Xl6pn.js";
@@ -218,6 +219,19 @@ function buildChatCommands() {
218
219
  captureRemaining: true
219
220
  }]
220
221
  }),
222
+ defineChatCommand({
223
+ key: "update",
224
+ nativeName: "update",
225
+ description: "Install the newest johns-harness and restart (owner only).",
226
+ textAlias: "/update",
227
+ category: "management",
228
+ args: [{
229
+ name: "version",
230
+ description: "Exact version to install (default latest)",
231
+ type: "string",
232
+ required: false
233
+ }]
234
+ }),
221
235
  defineChatCommand({
222
236
  key: "login",
223
237
  nativeName: "login",
@@ -1,3 +1,4 @@
1
+ // JOHNNESS_PATCH_UPDATE_COMMAND_62
1
2
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
3
  // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
4
  import { D as isPlainObject, u as escapeRegExp } from "./logger-BW8uLq6f.js";
@@ -218,6 +219,19 @@ function buildChatCommands() {
218
219
  captureRemaining: true
219
220
  }]
220
221
  }),
222
+ defineChatCommand({
223
+ key: "update",
224
+ nativeName: "update",
225
+ description: "Install the newest johns-harness and restart (owner only).",
226
+ textAlias: "/update",
227
+ category: "management",
228
+ args: [{
229
+ name: "version",
230
+ description: "Exact version to install (default latest)",
231
+ type: "string",
232
+ required: false
233
+ }]
234
+ }),
221
235
  defineChatCommand({
222
236
  key: "login",
223
237
  nativeName: "login",
@@ -1,3 +1,4 @@
1
+ // JOHNNESS_PATCH_UPDATE_COMMAND_62
1
2
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
3
  // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
4
  import { Bi as DEFAULT_PROVIDER, Or as resolveConfiguredModelRef, zi as DEFAULT_MODEL } from "./auth-profiles-5CHn7vq1.js";
@@ -181,6 +182,19 @@ function buildChatCommands() {
181
182
  captureRemaining: true
182
183
  }]
183
184
  }),
185
+ defineChatCommand({
186
+ key: "update",
187
+ nativeName: "update",
188
+ description: "Install the newest johns-harness and restart (owner only).",
189
+ textAlias: "/update",
190
+ category: "management",
191
+ args: [{
192
+ name: "version",
193
+ description: "Exact version to install (default latest)",
194
+ type: "string",
195
+ required: false
196
+ }]
197
+ }),
184
198
  defineChatCommand({
185
199
  key: "login",
186
200
  nativeName: "login",
@@ -1,3 +1,5 @@
1
+ import { createImageGenerateTool } from "./image-generate.js";
2
+ import { handleUpdateCommand } from "./update-command.js";
1
3
  // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
4
  // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
5
  import { handleDeviceLoginCommand } from "./codex-oauth-device.js";
@@ -11283,6 +11285,7 @@ const TRUSTED_TOOL_RESULT_MEDIA = new Set([
11283
11285
  "exec",
11284
11286
  "gateway",
11285
11287
  "image",
11288
+ "image_generate",
11286
11289
  "memory_get",
11287
11290
  "memory_search",
11288
11291
  "message",
@@ -15583,7 +15586,8 @@ function buildAgentSystemPrompt(params) {
15583
15586
  sessions_spawn: acpSpawnRuntimeEnabled ? "Spawn an isolated sub-agent or ACP coding session (runtime=\"acp\" requires `agentId` unless `acp.defaultAgent` is configured; ACP harness ids follow acp.allowedAgents, not agents_list)" : "Spawn an isolated sub-agent session",
15584
15587
  subagents: "List, steer, or kill sub-agent runs for this requester session",
15585
15588
  session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
15586
- image: "Analyze an image with the configured image model"
15589
+ image: "Analyze an image with the configured image model",
15590
+ image_generate: "Generate or edit images with GPT Image 2 (subscription first, API key second)"
15587
15591
  };
15588
15592
  const toolOrder = [
15589
15593
  "read",
@@ -15610,7 +15614,8 @@ function buildAgentSystemPrompt(params) {
15610
15614
  "sessions_send",
15611
15615
  "subagents",
15612
15616
  "session_status",
15613
- "image"
15617
+ "image",
15618
+ "image_generate"
15614
15619
  ];
15615
15620
  const canonicalToolNames = (params.toolNames ?? []).map((tool) => tool.trim()).filter(Boolean);
15616
15621
  const canonicalByNormalized = /* @__PURE__ */ new Map();
@@ -40979,6 +40984,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
40979
40984
  async function handleCommands(params) {
40980
40985
  const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
40981
40986
  if (deviceLoginReply) return deviceLoginReply;
40987
+ const updateReply = await handleUpdateCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway, writeRestartSentinel, extractDeliveryInfo, restart: () => scheduleGatewaySigusr1Restart({ reason: "/update" }) }); // JOHNNESS_PATCH_UPDATE_COMMAND_62
40988
+ if (updateReply) return updateReply;
40982
40989
  if (HANDLERS === null) HANDLERS = [
40983
40990
  handlePluginCommand,
40984
40991
  handleBashCommand,
@@ -55646,6 +55653,16 @@ function createJohnnessTools(options) {
55646
55653
  fsPolicy: options?.fsPolicy,
55647
55654
  modelHasVision: options?.modelHasVision
55648
55655
  }) : null;
55656
+ const imageGenerateTool = options?.agentDir?.trim() ? createImageGenerateTool({
55657
+ config: options?.config,
55658
+ agentDir: options.agentDir,
55659
+ workspaceDir,
55660
+ sandbox: options?.sandboxRoot && options?.sandboxFsBridge ? { root: options.sandboxRoot, bridge: options.sandboxFsBridge } : void 0
55661
+ }, {
55662
+ resolveApiKeyForProvider,
55663
+ loadWebMedia,
55664
+ localRoots: resolveMediaToolLocalRoots(workspaceDir, { workspaceOnly: options?.fsPolicy?.workspaceOnly === true })
55665
+ }) : null; // JOHNNESS_PATCH_GPT_IMAGE_62
55649
55666
  const pdfTool = options?.agentDir?.trim() ? createPdfTool({
55650
55667
  config: options?.config,
55651
55668
  agentDir: options.agentDir,
@@ -55765,6 +55782,7 @@ function createJohnnessTools(options) {
55765
55782
  ...webSearchTool ? [webSearchTool] : [],
55766
55783
  ...webFetchTool ? [webFetchTool] : [],
55767
55784
  ...imageTool ? [imageTool] : [],
55785
+ ...imageGenerateTool ? [imageGenerateTool] : [],
55768
55786
  ...pdfTool ? [pdfTool] : []
55769
55787
  ];
55770
55788
  const pluginTools = resolvePluginTools({
@@ -92632,7 +92650,10 @@ function isTrackedByProvenance(params) {
92632
92650
  function warnWhenAllowlistIsOpen(params) {
92633
92651
  if (!params.pluginsEnabled) return;
92634
92652
  if (params.allow.length > 0) return;
92635
- const nonBundled = params.discoverablePlugins.filter((entry) => entry.origin !== "bundled");
92653
+ // JOHNNESS_PATCH_DEFAULTS_61: plugins the config already implies (memory slot, enabled
92654
+ // entries, install records) are trusted by construction and never warned about.
92655
+ const implied = params.impliedIds ?? new Set();
92656
+ const nonBundled = params.discoverablePlugins.filter((entry) => entry.origin !== "bundled" && !implied.has(entry.id));
92636
92657
  if (nonBundled.length === 0) return;
92637
92658
  const preview = nonBundled.slice(0, 6).map((entry) => `${entry.id} (${entry.source})`).join(", ");
92638
92659
  const extra = nonBundled.length > 6 ? ` (+${nonBundled.length - 6} more)` : "";
@@ -92730,6 +92751,11 @@ function loadJohnnessPlugins(options = {}) {
92730
92751
  logger,
92731
92752
  pluginsEnabled: normalized.enabled,
92732
92753
  allow: normalized.allow,
92754
+ impliedIds: new Set([
92755
+ ...typeof normalized.slots.memory === "string" ? [normalized.slots.memory] : [],
92756
+ ...Object.entries(normalized.entries).filter(([, entry]) => entry?.enabled === true).map(([id]) => id),
92757
+ ...Object.keys(cfg.plugins?.installs ?? {})
92758
+ ]), // JOHNNESS_PATCH_DEFAULTS_61
92733
92759
  discoverablePlugins: manifestRegistry.plugins.map((plugin) => ({
92734
92760
  id: plugin.id,
92735
92761
  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",
@@ -3120,6 +3132,17 @@ function applyLegacyAudioTranscriptionModel(params) {
3120
3132
  params.changes.push(params.alreadySetMessage);
3121
3133
  }
3122
3134
  const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
3135
+ {
3136
+ id: "agent.image-generation-model-v1",
3137
+ describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
3138
+ apply: (raw, changes) => {
3139
+ const defaults = getRecord(getRecord(raw.agents)?.defaults);
3140
+ if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
3141
+ const primary = defaults.imageGenerationModel.trim();
3142
+ defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
3143
+ changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
3144
+ }
3145
+ }, // JOHNNESS_PATCH_GPT_IMAGE_62
3123
3146
  {
3124
3147
  id: "agent.model-config-v2",
3125
3148
  describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
@@ -4407,7 +4430,9 @@ function isBundledChannelEnabledByChannelConfig(cfg, pluginId) {
4407
4430
  if (!channelId) return false;
4408
4431
  const entry = cfg.channels?.[channelId];
4409
4432
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
4410
- return entry.enabled === true;
4433
+ // JOHNNESS_PATCH_DEFAULTS_61: a configured channel block implies the bundled channel
4434
+ // plugin. Only an explicit enabled:false opts out; no plugins.allow needed.
4435
+ return entry.enabled !== false;
4411
4436
  }
4412
4437
  function resolveEffectiveEnableState(params) {
4413
4438
  const base = resolveEnableState(params.id, params.origin, params.config);
@@ -5091,6 +5116,18 @@ function safeStatMtimeMs(filePath) {
5091
5116
  return null;
5092
5117
  }
5093
5118
  }
5119
+ // JOHNNESS_PATCH_DEFAULTS_61: loose semver compare for bundled-vs-installed plugin dedupe.
5120
+ function compareJohnnessPluginVersions(a, b) {
5121
+ 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);
5122
+ const left = parse(a);
5123
+ const right = parse(b);
5124
+ const len = Math.max(left.length, right.length, 1);
5125
+ for (let i = 0; i < len; i += 1) {
5126
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
5127
+ if (diff !== 0) return diff > 0 ? 1 : -1;
5128
+ }
5129
+ return 0;
5130
+ }
5094
5131
  function normalizeManifestLabel(raw) {
5095
5132
  const trimmed = raw?.trim();
5096
5133
  return trimmed ? trimmed : void 0;
@@ -5187,6 +5224,39 @@ function loadPluginManifestRegistry(params) {
5187
5224
  }
5188
5225
  continue;
5189
5226
  }
5227
+ // JOHNNESS_PATCH_DEFAULTS_61: the same plugin id bundled with the harness and installed
5228
+ // under the state dir is the normal shape (mr-memory). The installed copy stays
5229
+ // authoritative (family 59) with no warning; when the bundled copy is newer by
5230
+ // package version, one info line suggests updating. Two non-bundled copies
5231
+ // still warn as before.
5232
+ const existingBundled = existing.candidate.origin === "bundled";
5233
+ const candidateBundled = candidate.origin === "bundled";
5234
+ if (existingBundled !== candidateBundled) {
5235
+ const existingRecord = records[existing.recordIndex];
5236
+ const candidateVersion = normalizeManifestLabel(manifest.version) ?? candidate.packageVersion;
5237
+ const bundledVersion = existingBundled ? existingRecord?.version : candidateVersion;
5238
+ const installedVersion = existingBundled ? candidateVersion : existingRecord?.version;
5239
+ if (existingBundled) {
5240
+ records[existing.recordIndex] = buildRecord({
5241
+ manifest,
5242
+ candidate,
5243
+ manifestPath: manifestRes.manifestPath,
5244
+ schemaCacheKey,
5245
+ configSchema
5246
+ });
5247
+ seenIds.set(manifest.id, {
5248
+ candidate,
5249
+ recordIndex: existing.recordIndex
5250
+ });
5251
+ }
5252
+ if (compareJohnnessPluginVersions(bundledVersion, installedVersion) > 0) diagnostics.push({
5253
+ level: "info",
5254
+ pluginId: manifest.id,
5255
+ source: candidate.source,
5256
+ message: `installed copy ${installedVersion ?? "?"} is older than the bundled ${bundledVersion ?? "?"}; run "johnness plugins update ${manifest.id}" to upgrade`
5257
+ });
5258
+ continue;
5259
+ }
5190
5260
  diagnostics.push({
5191
5261
  level: "warn",
5192
5262
  pluginId: manifest.id,
@@ -6703,6 +6773,7 @@ function isValidNonNegativeByteSizeString(value) {
6703
6773
  const AgentDefaultsSchema = z.object({
6704
6774
  model: AgentModelSchema.optional(),
6705
6775
  imageModel: AgentModelSchema.optional(),
6776
+ imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
6706
6777
  pdfModel: AgentModelSchema.optional(),
6707
6778
  pdfMaxBytesMb: z.number().positive().optional(),
6708
6779
  pdfMaxPages: z.number().int().positive().optional(),
@@ -9576,6 +9647,7 @@ function validateConfigObjectWithPluginsBase(raw, opts) {
9576
9647
  workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)) ?? void 0
9577
9648
  });
9578
9649
  for (const diag of registry.diagnostics) {
9650
+ if (diag.level === "info") continue; // JOHNNESS_PATCH_DEFAULTS_61
9579
9651
  let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
9580
9652
  if (!diag.pluginId && diag.message.includes("plugin path not found")) path = "plugins.load.paths";
9581
9653
  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",
@@ -6100,6 +6112,17 @@ function applyLegacyAudioTranscriptionModel(params) {
6100
6112
  params.changes.push(params.alreadySetMessage);
6101
6113
  }
6102
6114
  const LEGACY_CONFIG_MIGRATIONS_PART_2 = [
6115
+ {
6116
+ id: "agent.image-generation-model-v1",
6117
+ describe: "Migrate agents.defaults.imageGenerationModel string to primary/fallbacks",
6118
+ apply: (raw, changes) => {
6119
+ const defaults = getRecord(getRecord(raw.agents)?.defaults);
6120
+ if (!defaults || typeof defaults.imageGenerationModel !== "string") return;
6121
+ const primary = defaults.imageGenerationModel.trim();
6122
+ defaults.imageGenerationModel = primary ? { primary, fallbacks: [] } : void 0;
6123
+ changes.push("Migrated agents.defaults.imageGenerationModel string to primary/fallbacks");
6124
+ }
6125
+ }, // JOHNNESS_PATCH_GPT_IMAGE_62
6103
6126
  {
6104
6127
  id: "agent.model-config-v2",
6105
6128
  describe: "Migrate legacy agent.model/allowedModels/modelAliases/modelFallbacks/imageModelFallbacks to agent.models + model lists",
@@ -7371,7 +7394,9 @@ function isBundledChannelEnabledByChannelConfig(cfg, pluginId) {
7371
7394
  if (!channelId) return false;
7372
7395
  const entry = cfg.channels?.[channelId];
7373
7396
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
7374
- return entry.enabled === true;
7397
+ // JOHNNESS_PATCH_DEFAULTS_61: a configured channel block implies the bundled channel
7398
+ // plugin. Only an explicit enabled:false opts out; no plugins.allow needed.
7399
+ return entry.enabled !== false;
7375
7400
  }
7376
7401
  function resolveEffectiveEnableState(params) {
7377
7402
  const base = resolveEnableState(params.id, params.origin, params.config);
@@ -8055,6 +8080,18 @@ function safeStatMtimeMs(filePath) {
8055
8080
  return null;
8056
8081
  }
8057
8082
  }
8083
+ // JOHNNESS_PATCH_DEFAULTS_61: loose semver compare for bundled-vs-installed plugin dedupe.
8084
+ function compareJohnnessPluginVersions(a, b) {
8085
+ 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);
8086
+ const left = parse(a);
8087
+ const right = parse(b);
8088
+ const len = Math.max(left.length, right.length, 1);
8089
+ for (let i = 0; i < len; i += 1) {
8090
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
8091
+ if (diff !== 0) return diff > 0 ? 1 : -1;
8092
+ }
8093
+ return 0;
8094
+ }
8058
8095
  function normalizeManifestLabel(raw) {
8059
8096
  const trimmed = raw?.trim();
8060
8097
  return trimmed ? trimmed : void 0;
@@ -8151,6 +8188,39 @@ function loadPluginManifestRegistry(params) {
8151
8188
  }
8152
8189
  continue;
8153
8190
  }
8191
+ // JOHNNESS_PATCH_DEFAULTS_61: the same plugin id bundled with the harness and installed
8192
+ // under the state dir is the normal shape (mr-memory). The installed copy stays
8193
+ // authoritative (family 59) with no warning; when the bundled copy is newer by
8194
+ // package version, one info line suggests updating. Two non-bundled copies
8195
+ // still warn as before.
8196
+ const existingBundled = existing.candidate.origin === "bundled";
8197
+ const candidateBundled = candidate.origin === "bundled";
8198
+ if (existingBundled !== candidateBundled) {
8199
+ const existingRecord = records[existing.recordIndex];
8200
+ const candidateVersion = normalizeManifestLabel(manifest.version) ?? candidate.packageVersion;
8201
+ const bundledVersion = existingBundled ? existingRecord?.version : candidateVersion;
8202
+ const installedVersion = existingBundled ? candidateVersion : existingRecord?.version;
8203
+ if (existingBundled) {
8204
+ records[existing.recordIndex] = buildRecord({
8205
+ manifest,
8206
+ candidate,
8207
+ manifestPath: manifestRes.manifestPath,
8208
+ schemaCacheKey,
8209
+ configSchema
8210
+ });
8211
+ seenIds.set(manifest.id, {
8212
+ candidate,
8213
+ recordIndex: existing.recordIndex
8214
+ });
8215
+ }
8216
+ if (compareJohnnessPluginVersions(bundledVersion, installedVersion) > 0) diagnostics.push({
8217
+ level: "info",
8218
+ pluginId: manifest.id,
8219
+ source: candidate.source,
8220
+ message: `installed copy ${installedVersion ?? "?"} is older than the bundled ${bundledVersion ?? "?"}; run "johnness plugins update ${manifest.id}" to upgrade`
8221
+ });
8222
+ continue;
8223
+ }
8154
8224
  diagnostics.push({
8155
8225
  level: "warn",
8156
8226
  pluginId: manifest.id,
@@ -9635,6 +9705,7 @@ function isValidNonNegativeByteSizeString(value) {
9635
9705
  const AgentDefaultsSchema = z.object({
9636
9706
  model: AgentModelSchema.optional(),
9637
9707
  imageModel: AgentModelSchema.optional(),
9708
+ imageGenerationModel: AgentModelSchema.optional(), // JOHNNESS_PATCH_GPT_IMAGE_62
9638
9709
  pdfModel: AgentModelSchema.optional(),
9639
9710
  pdfMaxBytesMb: z.number().positive().optional(),
9640
9711
  pdfMaxPages: z.number().int().positive().optional(),
@@ -12481,6 +12552,7 @@ function validateConfigObjectWithPluginsBase(raw, opts) {
12481
12552
  workspaceDir: resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)) ?? void 0
12482
12553
  });
12483
12554
  for (const diag of registry.diagnostics) {
12555
+ if (diag.level === "info") continue; // JOHNNESS_PATCH_DEFAULTS_61
12484
12556
  let path = diag.pluginId ? `plugins.entries.${diag.pluginId}` : "plugins";
12485
12557
  if (!diag.pluginId && diag.message.includes("plugin path not found")) path = "plugins.load.paths";
12486
12558
  const message = `${diag.pluginId ? `plugin ${diag.pluginId}` : "plugin"}: ${diag.message}`;