mock.restore() in afterAll is global in Bun, so the previous attempt to
satisfy mock-module-lifecycle-audit by pairing mock.module('./logger')
with afterAll(() => mock.restore()) tore down mocks owned by other
tests. Concretely, running this file before src/hooks/runtime-fallback
produced 42 cascading fallback test failures.
The logger mock was decorative - tests do not assert log calls, and the
real logger only writes to oh-my-opencode.log in the OS temp dir. Drop
the mock entirely so the audit has nothing to score and other tests
keep their mocks intact.
- prometheus-prompt.test.ts: close missing }) on the OpenSpec expanded
commands describe block (introduced by d66b6bcbf, parse error).
- agent-sort-shim/agent-config-integration/continuation-injection/
unstable-agent-babysitter/subagent-resolver/sync-executor/
resolve-caller-team-lead tests: expect 'Sisyphus - ultraworker'
(lowercase) to match production after cd39f8858, which lowercased the
display name to dodge a TUI ZWSP rendering glitch. Legacy uppercase
inputs that exercise the normalization path are preserved.
- sync-executor.ts + resolve-caller-team-lead.ts: route legacy display
name inputs through normalizeAgentForPrompt so prompt agent names and
caller team lead lookups produce the canonical lowercase form.
PR #4146 fixed the async resolvers (resolveSkillContentAsync, resolveMultipleSkillsAsync) to support unambiguous short-name skill lookups by routing through matchSkillByName. But the public getSkillByName API (exported from src/features/opencode-skill-loader/) was left on the old skills.find(s => s.name === name) lookup, so any external caller asking for 'systematic-debugging' on a 'superpowers/systematic-debugging' skill silently gets undefined.
Wire getSkillByName through the same matchSkillByName helper so the public API matches the async resolution semantics:
- Exact full-name lookup (case-insensitive) is preferred.
- Unambiguous short-name lookup falls back to the unique namespaced match.
- Ambiguous short-name (>=2 namespaces share the same short tail) returns undefined, mirroring matchSkillByName.
Regression tests added to src/features/opencode-skill-loader/loader.test.ts under a new 'getSkillByName' describe block:
- exact full-name returns the skill
- namespaced skill resolves by its unique short name
- ambiguous short name returns undefined
Verification: 3/3 new cases pass, 129/129 full src/features/opencode-skill-loader tests pass (was 126/126 pre-fix), bun run typecheck clean.
The ZAI (Zhipu) provider emits 'Weekly/Monthly Limit Exhausted. Your limit will reset at YYYY-MM-DD HH:MM:SS' when the coding-plan subscription quota is hit. None of the existing quota regex patterns (/quota.?exceeded/, /usage\s+limit/, /exhausted\s+your\s+capacity/, /credit\s+balance.*too\s+low/, etc.) match the 'Limit Exhausted' phrasing, so the runtime-fallback never fires and the user is stuck on the dead model.
Add /limit\s+exhausted/i to both pattern lists that gate fallback dispatch:
- RETRYABLE_ERROR_PATTERNS in constants.ts (text-pattern path used by extractStatusCode + retryable scan)
- classifyErrorType quota_exceeded branch in error-classifier.ts (typed classification path used by isRetryableError)
The pattern is intentionally narrow: it requires the literal token 'Limit' followed by whitespace then 'Exhausted'. It matches the ZAI weekly, monthly, and combined Weekly/Monthly variants but does not collide with unrelated phrases such as 'context limit' or 'rate limit' that already have their own dedicated patterns.
Regression coverage added to quota-error-classifier.regression.test.ts:
- 'Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-05-20 15:43:27' -> quota_exceeded + retryable=true
- 'Weekly Limit Exhausted. Your limit will reset at 2026-05-28 10:30:00' -> quota_exceeded + retryable=true
Verification: 11/11 quota-error-classifier.regression.test.ts pass (was 9 pass + 2 fail pre-fix). Broader runtime-fallback suite goes from 135/196 pass to 137/198 pass (the 61 pre-existing failures are unrelated to this change and reproduce on a clean upstream/dev checkout). bun run typecheck clean.
bun run build:schema brings assets/oh-my-opencode.schema.json back in sync with the Zod source. Surfaces two fields that were already present in src/config/schema/ but missing from the published artifact:
- disabled_providers at the top level (from feat/config-disabled-providers)
- displayName on every agent override (from fix/schema-preserve-custom-agent-overrides)
No Zod source changes; only the generated artifact moves.
src/hooks/comment-checker/apply-patch-edits.ts was already a pure re-export over @oh-my-opencode/comment-checker-core after the core extraction landed. Every importer now reaches into the package directly, so the shim has no remaining call sites and can be removed.
Verified: rg "comment-checker/apply-patch-edits" src/ packages/ returns no matches.
Promote the project-rule constants (PROJECT_MARKERS, PROJECT_RULE_SUBDIRS, PROJECT_RULE_FILES, OPENCODE_USER_RULE_DIRS, USER_RULE_DIR, GITHUB_INSTRUCTIONS_PATTERN, RULE_EXTENSIONS, AGENTS_FILENAME, etc.) and the findAgentsMdUp walk-up helper out of the agents-md-core and rules-injector adapters and into @oh-my-opencode/rules-engine, the single owner of rule discovery.
- packages/agents-md-core/ drops the findAgentsMdUp/AgentsMdDiscoveryInput wrappers (now sourced directly from rules-engine) and its constants module re-exports AGENTS_FILENAME from rules-engine instead of duplicating it.
- src/hooks/directory-agents-injector/finder.ts pulls findAgentsMdUp from rules-engine directly while still re-exporting resolveFilePath from agents-md-core.
- src/hooks/rules-injector/constants.ts becomes a pure re-export shim over the rules-engine constants.
Add packages/agents-md-core/src/injector.test.ts to lock the root-skipping AGENTS.md injection order so future changes to findAgentsMdUp cannot silently regress the [Directory Context: ...] block format the injector emits.
Tests: bun test packages/agents-md-core packages/rules-engine src/hooks/directory-agents-injector src/hooks/rules-injector
Move three pure helpers from src/shared/ into @oh-my-opencode/model-core so the package can stand alone without depending on plugin internals:
- buildModelCapabilitiesSnapshotFromModelsDev + fetchModelCapabilitiesSnapshot (models.dev normalization)
- parseModelSuggestion (cross-provider ProviderModelNotFoundError suggestion extraction)
- resolveActualContextLimit (Anthropic GA 1M context override)
Split provider-model-id-transform into two variants exposed by model-core:
- transformModelForProvider keeps the runtime dash to dot Anthropic rewrite used by the SDK
- transformModelForProviderDisplay preserves hyphenated Anthropic IDs so the installer writes registry-compatible model strings, fixing the ProviderModelNotFoundError fresh installs hit when the dotted form leaks into the config
src/shared/* and src/cli/provider-model-id-transform.ts collapse to re-export shims that point at the new core modules. Stale src/shared/{known-variants,model-capability-aliases,model-capability-guardrails,model-capability-heuristics}.ts re-export files plus the duplicated context-limit-resolver test are removed in favor of the canonical model-core copies.
Tests: bun test packages/model-core src/shared/model-capabilities-cache.test.ts src/cli/provider-model-id-transform.test.ts
Move the hash-anchored edit core (hash computation, validation, edit operations, text normalization, chunk formatter, diff utilities, and a runtime-aware xxHash32 binding) into a new @oh-my-opencode/hashline-core workspace package.
The src/tools/hashline-edit/ surface becomes a set of thin re-export shims, so existing import paths in the plugin keep working while the pure logic lives behind a stable package boundary that has no opencode runtime dependencies.
Tests: bun test packages/hashline-core src/tools/hashline-edit
Treat resolved abort responses with a non-null error payload the same as rejected aborts. This prevents stale-timeout cancellation bookkeeping from reporting success when the child session was not actually aborted.
Plan: .omo/plans/subagent-timeout-active-output.md
The model emits tool names like mcp_background_output but the runtime
registry has them as background_output. While transformToolName already
handles the prefix for display purposes, the tool dispatch path in
tool-execute-before was not stripping it, causing 'unavailable tool' errors.
This adds mcp_ prefix stripping at the earliest point in the tool
execution pipeline, fixing background_output, background_cancel, and
all nocturne-memory_* tools.
Closes#2697
Convert OpenCode v2 session.next stream events into the existing message part activity path so child sessions that are still producing text, reasoning, or tool output refresh lastUpdate before stale polling runs.
This keeps the timeout poller from cancelling active subagents and preserves tool-call progress for session.next.tool.called events.
Plan: .omo/plans/subagent-timeout-active-output.md
The hyperplan trigger \b(hyperplan|hpp)\b/i matched 'hpp' inside common C++ header references like 'check interface.hpp' or 'open buffer.hpp'. The leading '.' is a non-word character, so \b is already satisfied and the false positive fires the hyperplan-mode prompt on routine code questions.
Split the alternation so 'hpp' additionally requires that the preceding character is neither a word character nor a '.'. This preserves every existing trigger ('hpp do this', '/hpp ...', mid-sentence usage, mixed case) while rejecting filename uses of the .hpp extension. The longer 'hyperplan' keyword keeps the original \b boundary semantics.
Reproduction (added regression tests):
- 'please help to check interface.hpp' must NOT fire
- 'open src/include/audio/buffer.hpp and fix the leak' must NOT fire
All 14 cases in hyperplan.test.ts pass (12 existing + 2 new), broader keyword-detector suite stays green (92 pass), typecheck clean.