Grok model IDs (e.g. `grok-4.3`, `grok-3-mini`) were missing from `HEURISTIC_MODEL_FAMILY_REGISTRY`, so `resolveCompatibleModelSettings` returned an "unknown family" result for them.
The `chat.params` hook treats unknown families as "no reasoning support" and deletes `output.options.reasoningEffort` before the request leaves the plugin, so users routing Grok via OpenAI-compatible endpoints never saw their reasoning effort forwarded — even with `forceReasoning: true`.
Add a `grok` family entry that includes any model ID containing "grok", exposes `low | medium | high` variants, and sets `reasoningEfforts` so the heuristic surfaces the same capability flag used by the `chat.params` hook. Extend the family table-driven test in `model-settings-compatibility.test.ts` to cover `grok-4.3`.
Two blockers from the maintainer's Oracle review on PR #4121:
Blocker 1 — load_skills=null should still throw, omitted should default
The previous PR collapsed both `loadSkills === undefined` and
`loadSkills === null` into a silent default of `[]`. The closing
rationale of PR #1663 (which reverted PR #1493) and the maintainer's
review both call out the importance of preserving the distinct
"omitted -> default, explicit invalid -> throw" contract. `null`
strongly signals "I tried to pass something and it was wrong";
silently coercing it hides bugs upstream.
Restored the split: `undefined` -> default `[]` + log,
`null` -> throw with the historical error string.
Blocker 2 — task_id continuation test rewritten, not deleted
The original PR removed the `task_id without run_in_background ->
throws` test entirely. The behavior IS preserved (default false ->
`isExplicitSyncRun` true -> `executeSyncContinuation`), but with the
test gone the new contract was unprotected.
Added a regression test that asserts the new contract: when
`task_id` is present and `run_in_background` is omitted,
`tool.execute` must route through sync continuation without throwing
the legacy required-parameter error. Mocks include `session.abort`
because the sync poller calls it during shutdown.
Also flipped the existing `load_skills=null` regression test from
"normalizes to []" back to "throws with the legacy error string" to
match the restored contract.
Tests:
- bun test src/tools/delegate-task/tools.test.ts -> 132/132 pass
- bun test src/tools/delegate-task/ -> 406/406 pass
- bun run typecheck -> clean
Sisyphus and other delegators occasionally invoke the task() tool without
an explicit run_in_background or load_skills argument. The runtime
validators in tool-argument-preparation.ts threw a hard Error in that
case, which short-circuited tool.execute() entirely. Because OpenCode's
tool.execute.after hook only runs on returned results, the
delegate-task-retry hook never had a chance to attach corrective
guidance — so the model saw a raw failure and either burned several
retries or fell back to a synchronous Explore call, silently losing
parallel execution.
Behavior change:
- run_in_background omitted -> defaults to false (sync delegation), with
a log entry for observability.
- load_skills omitted or null -> normalized to [] with a log entry on
the explicit-null path.
- The Zod schema entries are now .optional() and their .describe()
strings declare the defaults honestly; the markdown tool description
was updated to match (no more 'REQUIRED' lie).
The orthogonal validation 'Must provide either category or
subagent_type.' is unchanged and still surfaces as a returned error.
Tests:
- The five throw-on-missing tests in tools.test.ts are rewritten to
assert the new default-and-proceed contract.
- The 'no category, no subagent_type' test now asserts the
missing-target error remains intact.
Refs the workaround the reporter validated in the original issue body;
matches the design from PR #2375 which was previously reverted by
566031f4.
- Add src/locales/ with en baseline and zh overrides (Partial<Record> fallback)
- Add src/shared/i18n.ts with initI18n/t/setLocale/getLocale (LANG env auto-detect)
- Add I18nConfigSchema with locale field to plugin config
- Internationalize 13 hardcoded strings in task-toast-manager
- Add 18 unit tests for i18n module
- Pin manager tests to en locale for determinism
When all plugin hooks are removed (user disables/uninstalls plugins),
hooksConfigs becomes an empty array. The previous guard
(hooksConfigs.length > 0) skipped setPluginHooksConfigs(), leaving
stale plugin hooks active in pendingPluginHooksConfigs. Now we always
call setPluginHooksConfigs() so empty configs properly clear the
pending state and invalidate the cache.
Previously, loadPluginHooksConfigs() loaded plugin hooks from marketplace
plugins (hookify, superpowers, zzcommon, zzfe, etc.) into
pluginComponents.hooksConfigs, but config-handler.ts never consumed them.
This meant plugin hooks were discovered but never merged into the runtime
hooks dispatch system.
Changes:
- Extend ClaudeHookEvent and ClaudeHooksConfig to support all 12 event
types (PostToolUseFailure, PermissionRequest, Notification,
SubagentStart, SubagentStop, SessionStart, SessionEnd) in addition to
the existing 5
- Add ALL_HOOK_EVENT_TYPES constant as single source of truth for event
type iteration
- Add mergePluginHooksConfigs() to unwrap plugin HooksConfig (with hooks
wrapper) into flat ClaudeHooksConfig, filtering out unsupported
prompt/agent hook types
- Add setPluginHooksConfigs() to store pending plugin configs and
invalidate the config cache
- Create applyHookConfig() handler following existing applyXxxConfig
pattern, wired into config-handler after loadPluginComponents()
- Extend DisabledHooksConfig and mergeDisabledHooks for all 12 events
Closes#4179
Include Vertex Anthropic in the assistant-tail prefill guard after Cubic flagged the strict provider check. Keep the non-Anthropic opencode negative control covered by tests.
Plan: plans/fix-anthropic-assistant-prefill-tail.md
Tighten the assistant-tail repair after review so only the Anthropic provider is treated as prefill-rejecting, and so assistant-tail model metadata is evaluated independently from the last user model.
Add regression coverage for an allowed user model followed by a rejecting Anthropic assistant tail, plus a non-Anthropic provider carrying a Claude-looking model id.
Plan: plans/fix-anthropic-assistant-prefill-tail.md
Claude Opus 4.7 rejects requests whose final message is an assistant turn, because Anthropic treats that as unsupported assistant prefill. Add a transform guard that appends the existing synthetic user recovery turn only for Anthropic prefill-rejecting model IDs, while preserving assistant-prefill behavior for supported and unknown models.
Tests cover Opus 4.7, Opus 4.6 assistant metadata fallback, direct Sonnet 4.6 model fields, allowed OpenAI/Sonnet 4.5 tails, missing metadata, and compaction continuation compatibility.
Plan: plans/fix-anthropic-assistant-prefill-tail.md
Discord support and other guidance still tell users 'bunx oh-my-opencode setup', but the CLI only registers 'install'. Running the recommended command produces:
error: unknown command 'setup'
which dead-ends users who are trying to refresh missing skills (hyperplan, etc.) after upgrading. Commander.js supports first-class aliases, so declaring '.alias("setup")' on the install command makes both names route to the same action without any code duplication. Help output now reads 'Usage: oh-my-opencode install|setup [options]', which is the right discovery hint for both newcomers and users following older docs.
Regression test parses cli-program.ts and asserts the install command block contains '.alias("setup")' before its '.action(' so a future refactor of the CLI cannot silently drop the alias.
ensureBaseDirs unconditionally called chmod(baseDir, 0o700) on every startup
and on every team_create. On filesystems where the OS rejects chmod for the
directory (network mounts, SIP-protected locations, non-owner cases on macOS
shared by multiple GUI users), the call raises EPERM and the entire team-mode
init aborts:
[team-mode] init failed: EPERM: operation not permitted, chmod '/Users/<u>/.omo'
Wrap chmod through a small safeChmod helper that converts EPERM, ENOTSUP, and
EINVAL into a single warning log and continues. mkdir already creates new
directories with mode 0o700, and the existing post-creation stat-guard remains
in place for the case where the directory pre-exists with a different mode and
chmod is permitted, so the security envelope on supported filesystems is
unchanged. All other error codes (ENOENT, EACCES, etc.) still propagate.
Regression test mocks node:fs/promises.chmod to throw EPERM and asserts that
ensureBaseDirs completes successfully and emits exactly the documented warning.
The multimodal-looker prompt described what to do but never told the model which tools are available. Smaller VL models (e.g. Qwen3-VL-8B) would try to call non-existent tools and enter an infinite loop emitting:
Model tried to call unavailable tool 'invalid'. Available tools: call_omo_agent, read.
Add a single sentence at the top of the prompt that explicitly enumerates the only allowed tools ('read' and 'call_omo_agent') and forbids calling any other tool. This matches the runtime allowlist enforced by createAgentToolAllowlist(["read"]).
Regression test asserts the prompt contains the available-tools enumeration so future prompt rewrites don't regress.
Require the latest assistant tool-wait turn itself to be stale before a shouldReply parent wake can bypass tool-call deferral. This prevents an all-complete background wake from forking a second parent prompt loop when OpenCode has repaired the tail to a synthetic user message.
Tests:
- bun test src/features/background-agent/parent-wake-user-message-race.test.ts src/features/background-agent/task-completion-cleanup.test.ts src/hooks/shared/prompt-async-gate.test.ts src/shared/prompt-async-route-audit.test.ts --bail
- bun run typecheck
- bun test
Keep the built-in lsp MCP registered even when the submodule CLI artifact is missing.
The fallback command initializes the lsp-tools-mcp submodule, prefers the source CLI without dirtying the checkout with dist output, and keeps npm build as a last resort when Bun cannot run the source entrypoint.
Plan: plans/fix-lsp-mcp-missing-cli.md
The bundled createTodoDescriptionOverrideHook returns { 'tool.definition': fn }, but plugin-interface.ts never exposes 'tool.definition' as an OpenCode hook handler. Result: the hook is constructed by createToolGuardHooks (line 132-134 of src/plugin/hooks/create-tool-guard-hooks.ts) but the function is never invoked, so todowrite keeps using OpenCode's core 7 KB description instead of the 1.4 KB TODOWRITE_DESCRIPTION. User-defined plugins under ~/.config/opencode/plugin/*.js use the same hook contract and work fine, confirming the contract itself is functional in opencode 1.14.28+.
Fix: add src/plugin/tool-definition.ts (createToolDefinitionHandler) that forwards the OpenCode 'tool.definition' input/output pair into hooks.todoDescriptionOverride. Wire it into plugin-interface.ts alongside tool.execute.before/after.
Regression coverage: src/plugin/tool-definition.test.ts covers (a) todowrite override applied, (b) other tools left untouched, (c) null hook is a no-op.
After the 4.2.0 unified-dispatch refactor (a42f894f / df198d8b / fee515c5 / 989ab717 / dd3fecaf / 1bbe065c / 12bd6580), at least one caller in the new prompt-async-gate path forwards a FallbackModelObject (or some other non-string shape) into parsers that statically claim 'model: string'. The downstream .trim() call then throws 'model.trim is not a function', which rejects the session.processor promise and surfaces as 'Aborted process' + UI 'interrupted'. The issue (#4145) reports this aborts 90% of subagent dispatches across every provider on 4.2.0 + opencode 1.15.4.
This patch adds a 'typeof x !== "string"' runtime guard at the four parser entrypoints called from the dispatch path:
- src/shared/fallback-chain-from-models.ts :: parseVariantFromModel, parseFallbackModelEntry
- src/tools/delegate-task/model-string-parser.ts :: parseVariantFromModelID, parseModelString
- src/shared/model-string-parser.ts (duplicate file with same API) :: parseVariantFromModelID, parseModelString
- src/features/claude-code-agent-loader/claude-model-mapper.ts :: mapClaudeModelString
Each parser now returns undefined / { modelID: "" } for non-string input instead of throwing. This unblocks subagent dispatch and leaves the underlying caller bug for a follow-up.
Regression coverage: three new tests in src/shared/fallback-chain-from-models.test.ts pin the non-string behavior (object, null/undefined, number). Existing 38 tests still pass. Total: 41/41 green, typecheck clean.
Skills with an `agent` frontmatter field are intended for a specific
agent. Previously they still appeared in:
- every agent's system prompt (via `buildAvailableSkills`)
- the `skill` tool's `<available_items>` description visible to all agents
This wasted tokens and could mislead agents into attempting calls that
would be rejected at execution time.
Changes:
- `buildAvailableSkills`: new optional `agentName` parameter; when
provided, skills whose `definition.agent` does not match are excluded
- `builtin-agents.ts`: pass per-agent name to `buildAvailableSkills`
for sisyphus, hephaestus, and atlas, so each agent's prompt only
lists the skills it is allowed to use
- `createSkillTool` (`tools.ts`): exclude agent-restricted skills from
both the eager and lazy description builds, keeping the shared tool
description free of skills the current agent cannot access
Execution-time enforcement (throwing on mismatch) is unchanged; this
change adds the earlier, description-level visibility gate.
Tests: new `available-skills.test.ts` (5 cases) + 3 new cases in
`tools.test.ts` covering the description-filter and execute paths.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When OpenCode reports the original assistant error after a fallback retry has already been accepted, keep waiting for the pending fallback model instead of clearing the awaiting flag.
This prevents a duplicate stale session.error from advancing the fallback chain and dispatching a second assistant retry prompt.