chore: sync main history into dev#65
Merged
Merged
Conversation
* feat(hooks): move hooks config to dedicated hooks.json, cut .claude/ dependency
BREAKING: .claude/ directories and settings.json hooks field are no
longer read. Run /import-claude-hooks to migrate existing Claude
Code hook configurations.
Config location (6 layers → 2 dedicated files):
~/.config/opencode/hooks.json (global, loaded once at startup)
.opencode/hooks.json (project, hot-reloaded via polling)
<worktree>/.opencode/hooks.json (worktree, hot-reloaded)
Key changes:
- loadChain reads hooks.json from OpenCode-owned dirs only (.claude/
and .local variants removed)
- Top-level event format (no wrapper); legacy {"hooks":{...}} tolerated
- readJSON filters via VALID_HOOK_EVENTS whitelist + Array.isArray
- Deprecation warning for hooks left in old settings.json
- Hot-reload: project-level only, interval polling (2s) instead of
fs.watch (WSL2 DrvFs reliability); detects file deletion
- Global hooks.json is startup-only (no hot-reload), restart required
- Built-in /import-claude-hooks command for QA-guided migration
- AGENTS.md managed section (<!-- Hooks_START/END -->) for agent
self-awareness of configured hooks
OpenSpec: hooks-config-independence (spec-driven, 43/43 tasks complete)
Tests: 21 hook tests (load-chain + settings-dedup + hot-reload)
* chore: remove openspec change artifacts after archive
* feat(skill): add built-in configure-hooks skill
Agents have no innate knowledge of opencode's hooks system — the format
lives only in source/docs, so without a prompt nudge an agent either
guesses wrong or never discovers hooks are possible. Registers a
customize-opencode-style built-in skill: description alone tells the
agent hooks exist and when to reach for them, full event/format/handler
reference loads lazily only when the skill is actually invoked.
* chore: ignore local openspec artifacts
* fix(prompt): remove stale always-on hooks system prompt
hooks.txt taught the retired settings.json 6-layer protocol (hooks now
live in dedicated hooks.json chains; .claude/ is never read) and stale
event counts (22 vs actual 27). Discovery is preserved via the built-in
configure-hooks skill injected through sys.skills(agent). Also fix
README hooks section: 27 events and dead hooks-reference.md link now
points at the skill doc.
* fix(tool): resolve Goal.Service at execute time, not ToolRegistry build
The goal tool probed Effect.serviceOption(Goal.Service) in Tool.define's
build-phase init gen. ToolRegistry builds in an AppLayer mergeAll group
that cannot see sibling-group outputs, so the probe was always None and
the closure permanently no-op'd the tool ('goal service unavailable';
complete never ran markDone, the loop could not terminate via tool).
Move the probe into execute (request phase, full AppLayer context).
serviceOption keeps R = never, so no signature change; headless
runtimes still degrade gracefully. Document why task.ts's build-phase
probe is safe (SettingsHook arrives via provideMerge). Add an
integration test that mirrors the production layer topology (Goal
absent at build, provided at execute).
* fix(ci): designate a single bun cache saver per OS to stop save races
Concurrent Linux/Windows jobs across ci-test.yml, ci-typecheck.yml, and
release-fork.yml all computed the same {OS}-bun-{hash} cache key and raced
to save it, causing "Unable to reserve cache... another job may be
creating this cache" warnings and the cache never actually updating.
Now only one job per OS saves (ci-typecheck linux, ci-test e2e windows,
release-fork macos); everything else only restores.
* feat(hooks): dynamic Active Hooks + /create-hook + review fixes + projector guard
* feat(hooks): dynamic Active Hooks block + /create-hook command
- SettingsHook.list(): read-only scope-tagged summaries over hot-reloaded state
- SystemPrompt.hooks(): per-turn rendered block, zero tokens when no hooks
- /create-hook command: interactive hook authoring into hooks.json
- Remove static AGENTS.md Hooks_START snapshot (replaced by dynamic block)
- Fix import-claude-hooks.txt JSON format examples (missing hooks wrapper)
- Complete event list (27) and type list (5 incl mcp) in format reference
* fix(hooks): dedup skill descriptions + hot-reload mtime detection
- Export CustomizeOpencodeDescription / ConfigureHooksDescription from core,
import in opencode instead of hardcoding (descriptions had already drifted)
- hot-reload: m > prev → m !== prev to cover cp -p / touch -t restoring
older mtimes; reload is idempotent so the broader trigger is safe
- Add mtime-decrease regression test
- Include hooks-leftover-issues.md analysis document
* fix(core): guard projector against orphaned part updates after cleanup
PartUpdated events from interrupted streams can arrive after revert cleanup
has deleted the parent message. The projector now silently skips parts whose
parent message no longer exists instead of crashing with FOREIGN KEY
constraint failure. Includes regression test.
* chore: remove working notes file (hooks-leftover-issues.md)
Temporary analysis document from review process; content is now stale
(issues ①② fixed in this PR) or tracked separately (issue ③).
* refactor(hooks): address FABLE5 PR #57 review feedback
- Extract chainCandidates() shared by loadChain + summarizeChain (eliminate
duplicated path logic and Global.Path.config fallback)
- Projector: add Effect.logWarning when skipping orphaned PartUpdated (was
silent — genuine ordering bugs now observable in logs)
- descriptorFor: unify .slice(0,60) across all types (http/prompt/agent/mcp
were not truncated)
- Integration test: real SettingsHook layer reaches sys.hooks() via
serviceOption (not Layer.mock — proves the full chain works end-to-end)
---------
Co-authored-by: Test <test@opencode.test>
---------
Co-authored-by: Test <test@opencode.test>
LeXwDeX
added a commit
that referenced
this pull request
Jul 5, 2026
…ardening (#76) * feat(hooks): move hooks config to dedicated hooks.json, cut .claude/ dependency BREAKING: .claude/ directories and settings.json hooks field are no longer read. Run /import-claude-hooks to migrate existing Claude Code hook configurations. Config location (6 layers → 2 dedicated files): ~/.config/opencode/hooks.json (global, loaded once at startup) .opencode/hooks.json (project, hot-reloaded via polling) <worktree>/.opencode/hooks.json (worktree, hot-reloaded) Key changes: - loadChain reads hooks.json from OpenCode-owned dirs only (.claude/ and .local variants removed) - Top-level event format (no wrapper); legacy {"hooks":{...}} tolerated - readJSON filters via VALID_HOOK_EVENTS whitelist + Array.isArray - Deprecation warning for hooks left in old settings.json - Hot-reload: project-level only, interval polling (2s) instead of fs.watch (WSL2 DrvFs reliability); detects file deletion - Global hooks.json is startup-only (no hot-reload), restart required - Built-in /import-claude-hooks command for QA-guided migration - AGENTS.md managed section (<!-- Hooks_START/END -->) for agent self-awareness of configured hooks OpenSpec: hooks-config-independence (spec-driven, 43/43 tasks complete) Tests: 21 hook tests (load-chain + settings-dedup + hot-reload) * chore: remove openspec change artifacts after archive * feat(skill): add built-in configure-hooks skill Agents have no innate knowledge of opencode's hooks system — the format lives only in source/docs, so without a prompt nudge an agent either guesses wrong or never discovers hooks are possible. Registers a customize-opencode-style built-in skill: description alone tells the agent hooks exist and when to reach for them, full event/format/handler reference loads lazily only when the skill is actually invoked. * chore: ignore local openspec artifacts * fix(prompt): remove stale always-on hooks system prompt hooks.txt taught the retired settings.json 6-layer protocol (hooks now live in dedicated hooks.json chains; .claude/ is never read) and stale event counts (22 vs actual 27). Discovery is preserved via the built-in configure-hooks skill injected through sys.skills(agent). Also fix README hooks section: 27 events and dead hooks-reference.md link now points at the skill doc. * fix(tool): resolve Goal.Service at execute time, not ToolRegistry build The goal tool probed Effect.serviceOption(Goal.Service) in Tool.define's build-phase init gen. ToolRegistry builds in an AppLayer mergeAll group that cannot see sibling-group outputs, so the probe was always None and the closure permanently no-op'd the tool ('goal service unavailable'; complete never ran markDone, the loop could not terminate via tool). Move the probe into execute (request phase, full AppLayer context). serviceOption keeps R = never, so no signature change; headless runtimes still degrade gracefully. Document why task.ts's build-phase probe is safe (SettingsHook arrives via provideMerge). Add an integration test that mirrors the production layer topology (Goal absent at build, provided at execute). * fix(ci): designate a single bun cache saver per OS to stop save races Concurrent Linux/Windows jobs across ci-test.yml, ci-typecheck.yml, and release-fork.yml all computed the same {OS}-bun-{hash} cache key and raced to save it, causing "Unable to reserve cache... another job may be creating this cache" warnings and the cache never actually updating. Now only one job per OS saves (ci-typecheck linux, ci-test e2e windows, release-fork macos); everything else only restores. * feat(hooks): dynamic Active Hooks + /create-hook + review fixes + projector guard * feat(hooks): dynamic Active Hooks block + /create-hook command - SettingsHook.list(): read-only scope-tagged summaries over hot-reloaded state - SystemPrompt.hooks(): per-turn rendered block, zero tokens when no hooks - /create-hook command: interactive hook authoring into hooks.json - Remove static AGENTS.md Hooks_START snapshot (replaced by dynamic block) - Fix import-claude-hooks.txt JSON format examples (missing hooks wrapper) - Complete event list (27) and type list (5 incl mcp) in format reference * fix(hooks): dedup skill descriptions + hot-reload mtime detection - Export CustomizeOpencodeDescription / ConfigureHooksDescription from core, import in opencode instead of hardcoding (descriptions had already drifted) - hot-reload: m > prev → m !== prev to cover cp -p / touch -t restoring older mtimes; reload is idempotent so the broader trigger is safe - Add mtime-decrease regression test - Include hooks-leftover-issues.md analysis document * fix(core): guard projector against orphaned part updates after cleanup PartUpdated events from interrupted streams can arrive after revert cleanup has deleted the parent message. The projector now silently skips parts whose parent message no longer exists instead of crashing with FOREIGN KEY constraint failure. Includes regression test. * chore: remove working notes file (hooks-leftover-issues.md) Temporary analysis document from review process; content is now stale (issues ①② fixed in this PR) or tracked separately (issue ③). * refactor(hooks): address FABLE5 PR #57 review feedback - Extract chainCandidates() shared by loadChain + summarizeChain (eliminate duplicated path logic and Global.Path.config fallback) - Projector: add Effect.logWarning when skipping orphaned PartUpdated (was silent — genuine ordering bugs now observable in logs) - descriptorFor: unify .slice(0,60) across all types (http/prompt/agent/mcp were not truncated) - Integration test: real SettingsHook layer reaches sys.hooks() via serviceOption (not Layer.mock — proves the full chain works end-to-end) --------- Co-authored-by: Test <test@opencode.test> * feat(hooks): implement async hook execution + asyncRewake delivery (#60) Async hooks (async:true) fork into a background fiber scoped to the SettingsHook service layer; their output never participates in the current TriggerResult. asyncRewake:true delivers rewake-worthy output (exit 2 stderr, systemMessage, decision:block, additionalContext) back to the agent via the HookRewake bridge service driving the V1 SessionPrompt loop. - src/hook/rewake.ts: tag-only leaf service (no import cycle) - src/hook/rewake-live.ts: live impl via SessionPrompt.prompt, wired in AppLayer + httpapi server node graph (dual composition system) - settings.ts: async fork branch in trigger entry loop, onAsyncComplete rewake callback, HOOK_REWAKE_SENTINEL + buildRewakePrompt - prompt.ts: loop guard skips UserPromptSubmit hooks for sentinel-prefixed prompts (prevents hook -> rewake -> hook loops) - 9 integration tests (async non-blocking, output isolation, rewake e2e, suppression, once-at-fork, sentinel validation) Co-authored-by: Test <test@opencode.test> * fix(goal): self-clean loop fibers + guard resume + e2e regression (#61) - goal.ts: clearLoopFiberIf (identity-scoped, no interrupt) for D4 fiber lifecycle; /goal resume busy guard (D5); removeSubgoal 1-based doc - loop.ts: idle subscription skips goal-less sessions; Fiber.await watcher self-cleans fibers map; callLLM extraction to GoalLoopJudgeLLM tag (D5) - state.ts: drop dead skipped Verdict enum value - tests: clearLoopFiberIf contract, resume busy guard, full-cycle e2e (scripted judge continue->done), prompts type sync Co-authored-by: Test <test@opencode.test> * feat(hooks): async execution + stdout injection + workspace-trust + global hot-reload (#62) Bundles three layered efforts on the hooks subsystem (openspec changes fix-hooks-goal-review-findings + hooks-goal-fidelity-hardening) plus the async-hook execution work. settings.ts / session.ts carry interleaved changes from all three and cannot be split per-type at the file level. - async execution: async/asyncRewake hook execution + asyncRewake delivery, MCP elicitation transport, notification emitter, PreHookDecision reducer - fix-hooks (hooks slice): break session→settings→provider→plugin cycle via deferred dynamic import in session.ts; if-field no longer reported unsupported (condition-filter implements it); remove dead post-tool-batch extension (ForkHooks.afterRunEntry point retained); extract landSystemMessages /TriggerResult to cycle-free hook/trigger-result.ts; complete fidelity- deviation docs - hardening (hooks slice): exit-0 plain-text stdout → additionalContext for UserPromptSubmit/SessionStart (CC parity); workspace-trust gate (opt-in requireTrust/OPENCODE_HOOKS_REQUIRE_TRUST, path-boundary isTrusted, silent skip); global ~/.config/opencode/hooks.json hot-reload; readChain single-pass (loadChain + summarizeChain share one parse) Verification: bun typecheck clean; bun test test/goal test/hook 182 pass / 0 fail. * fix(opencode): exit prompt loop on blocked turn + relax elicitation poll timeout (#64) The #62 Stop-hook refactor routed loop exit through the top-of-loop finished-check, but a blocked turn (permission rejected -> processor returns "stop") still has finish="tool-calls", so the check never fired and the model got an extra turn. Track outcome="break" in a turnStopped flag so the exit path (and its Stop hook) fires for blocked turns too. Also bump the elicitation-transport poll timeout 5s -> 15s for slow CI hosts. Co-authored-by: Lex's Agent <lex-agent@noreply.local> * dev → main: hooks system maturity + goal/hook fixes + CI hardening (#59) (#65) * feat(hooks): move hooks config to dedicated hooks.json, cut .claude/ dependency BREAKING: .claude/ directories and settings.json hooks field are no longer read. Run /import-claude-hooks to migrate existing Claude Code hook configurations. Config location (6 layers → 2 dedicated files): ~/.config/opencode/hooks.json (global, loaded once at startup) .opencode/hooks.json (project, hot-reloaded via polling) <worktree>/.opencode/hooks.json (worktree, hot-reloaded) Key changes: - loadChain reads hooks.json from OpenCode-owned dirs only (.claude/ and .local variants removed) - Top-level event format (no wrapper); legacy {"hooks":{...}} tolerated - readJSON filters via VALID_HOOK_EVENTS whitelist + Array.isArray - Deprecation warning for hooks left in old settings.json - Hot-reload: project-level only, interval polling (2s) instead of fs.watch (WSL2 DrvFs reliability); detects file deletion - Global hooks.json is startup-only (no hot-reload), restart required - Built-in /import-claude-hooks command for QA-guided migration - AGENTS.md managed section (<!-- Hooks_START/END -->) for agent self-awareness of configured hooks OpenSpec: hooks-config-independence (spec-driven, 43/43 tasks complete) Tests: 21 hook tests (load-chain + settings-dedup + hot-reload) * chore: remove openspec change artifacts after archive * feat(skill): add built-in configure-hooks skill Agents have no innate knowledge of opencode's hooks system — the format lives only in source/docs, so without a prompt nudge an agent either guesses wrong or never discovers hooks are possible. Registers a customize-opencode-style built-in skill: description alone tells the agent hooks exist and when to reach for them, full event/format/handler reference loads lazily only when the skill is actually invoked. * chore: ignore local openspec artifacts * fix(prompt): remove stale always-on hooks system prompt hooks.txt taught the retired settings.json 6-layer protocol (hooks now live in dedicated hooks.json chains; .claude/ is never read) and stale event counts (22 vs actual 27). Discovery is preserved via the built-in configure-hooks skill injected through sys.skills(agent). Also fix README hooks section: 27 events and dead hooks-reference.md link now points at the skill doc. * fix(tool): resolve Goal.Service at execute time, not ToolRegistry build The goal tool probed Effect.serviceOption(Goal.Service) in Tool.define's build-phase init gen. ToolRegistry builds in an AppLayer mergeAll group that cannot see sibling-group outputs, so the probe was always None and the closure permanently no-op'd the tool ('goal service unavailable'; complete never ran markDone, the loop could not terminate via tool). Move the probe into execute (request phase, full AppLayer context). serviceOption keeps R = never, so no signature change; headless runtimes still degrade gracefully. Document why task.ts's build-phase probe is safe (SettingsHook arrives via provideMerge). Add an integration test that mirrors the production layer topology (Goal absent at build, provided at execute). * fix(ci): designate a single bun cache saver per OS to stop save races Concurrent Linux/Windows jobs across ci-test.yml, ci-typecheck.yml, and release-fork.yml all computed the same {OS}-bun-{hash} cache key and raced to save it, causing "Unable to reserve cache... another job may be creating this cache" warnings and the cache never actually updating. Now only one job per OS saves (ci-typecheck linux, ci-test e2e windows, release-fork macos); everything else only restores. * feat(hooks): dynamic Active Hooks + /create-hook + review fixes + projector guard * feat(hooks): dynamic Active Hooks block + /create-hook command - SettingsHook.list(): read-only scope-tagged summaries over hot-reloaded state - SystemPrompt.hooks(): per-turn rendered block, zero tokens when no hooks - /create-hook command: interactive hook authoring into hooks.json - Remove static AGENTS.md Hooks_START snapshot (replaced by dynamic block) - Fix import-claude-hooks.txt JSON format examples (missing hooks wrapper) - Complete event list (27) and type list (5 incl mcp) in format reference * fix(hooks): dedup skill descriptions + hot-reload mtime detection - Export CustomizeOpencodeDescription / ConfigureHooksDescription from core, import in opencode instead of hardcoding (descriptions had already drifted) - hot-reload: m > prev → m !== prev to cover cp -p / touch -t restoring older mtimes; reload is idempotent so the broader trigger is safe - Add mtime-decrease regression test - Include hooks-leftover-issues.md analysis document * fix(core): guard projector against orphaned part updates after cleanup PartUpdated events from interrupted streams can arrive after revert cleanup has deleted the parent message. The projector now silently skips parts whose parent message no longer exists instead of crashing with FOREIGN KEY constraint failure. Includes regression test. * chore: remove working notes file (hooks-leftover-issues.md) Temporary analysis document from review process; content is now stale (issues ①② fixed in this PR) or tracked separately (issue ③). * refactor(hooks): address FABLE5 PR #57 review feedback - Extract chainCandidates() shared by loadChain + summarizeChain (eliminate duplicated path logic and Global.Path.config fallback) - Projector: add Effect.logWarning when skipping orphaned PartUpdated (was silent — genuine ordering bugs now observable in logs) - descriptorFor: unify .slice(0,60) across all types (http/prompt/agent/mcp were not truncated) - Integration test: real SettingsHook layer reaches sys.hooks() via serviceOption (not Layer.mock — proves the full chain works end-to-end) --------- --------- Co-authored-by: Test <test@opencode.test> * fix(mcp): deterministic elicitation session routing cleanup (#66) The active elicitation session was a module-level single value with 'last writer wins' semantics, cleared by non-atomic set/undefined pairs in SessionTools.resolve. Under concurrent/nested/failing tool execution the value could be wiped or leaked, so the elicitation handler read an undefined sessionID, declined immediately, and the transport round-trip test timed out ('elicitation never surfaced as a Question'). Replace the single slot with a tokenized LIFO stack. setActiveElicitationSession returns a cleanup that removes only its own token, and SessionTools.resolve wraps tool execution in Effect.ensuring so cleanup runs on success, failure, and interruption. The handler reads the latest stack entry as fallback. Also clean up the transport test's client/server and active-session token via Effect.addFinalizer so failed assertions no longer leak resources, and add a stale-token routing assertion. Co-authored-by: Lex's Agent <lex-agent@noreply.local> * fix(mcp): diagnose elicitation-transport CI flake with observability (#67) * fix(mcp): deterministic elicitation session routing cleanup The active elicitation session was a module-level single value with 'last writer wins' semantics, cleared by non-atomic set/undefined pairs in SessionTools.resolve. Under concurrent/nested/failing tool execution the value could be wiped or leaked, so the elicitation handler read an undefined sessionID, declined immediately, and the transport round-trip test timed out ('elicitation never surfaced as a Question'). Replace the single slot with a tokenized LIFO stack. setActiveElicitationSession returns a cleanup that removes only its own token, and SessionTools.resolve wraps tool execution in Effect.ensuring so cleanup runs on success, failure, and interruption. The handler reads the latest stack entry as fallback. Also clean up the transport test's client/server and active-session token via Effect.addFinalizer so failed assertions no longer leak resources, and add a stale-token routing assertion. Co-Authored-By: Lex's Agent <lex-agent@noreply.local> * fix(mcp): diagnose elicitation-transport CI flake with observability CI has failed on the elicitation transport round-trip test since #62 with "elicitation never surfaced as a Question", timing out at the poll limit every time despite passing locally in isolation. Add the diagnostics needed to tell a silent decline from a genuine hang: filter the surfacing poll by this test's sessionID (immune to leaked pending questions from other files in the same bun process), reset the module-level active-session slot in beforeEach (not just afterEach), enrich the timeout error with a question.list() snapshot and the callTool fiber's poll state, and log the routing snapshot whenever handleElicitation declines without surfacing. Co-Authored-By: Lex's Agent <lex-agent@noreply.local> --------- Co-authored-by: Lex's Agent <lex-agent@noreply.local> * feat(hook): close hooks/goal completeness gaps (#68) Closes four product-loop gaps and several hygiene items from the 2026-07-05 HOOKS/GOAL review, with no architecture changes and no TUI modifications. - goal continuation failure -> recoverable pause: afterIdle's continuation prompt is wrapped in Effect.catchCause; a dispatch failure (provider fault, session write error) transitions the goal to paused with a 'continuation dispatch failed' reason + goal.updated(paused) event, instead of silently stalling active with no idle driver. - session-scoped hook HTTP API: POST/GET/DELETE /session/:id/hook[/...], delegating to the existing SessionHooks store (the pipeline's first production producer). Schema-validated (unknown event/type -> 4xx); once entries auto-remove; SessionEnd clears the store. - /trust command: in-session text entry point for the workspace-trust gate (prompt.ts early-return dispatch, logic in workspace-trust.ts; renders as transcript text, no TUI/popup). /trust adds the cwd (idempotent); /trust status shows trust + requireTrust source (hooks.json / env / off) + trust file path. Orthogonal to the permission allow:* config. - if condition coverage (BEHAVIOR CHANGE): condition-filter now evaluates 'if' on PermissionRequest/PermissionDenied too — previously 'if' was always-true on those two events even though matcherTarget matched them by tool_name. D4 in the change design. Hygiene: SessionHookCommand fields aligned (command optional + url/prompt/ headers), headless seen-bucket cap (MAX_SEEN_PER_BUCKET=1000), goal NonNegativeInt 'as any' consolidated into GoalState.nni(), and corrected/ added comments (markDone budget-neutral, MAX_AGENT_TURNS<->timeout, judge 4000-char snippet tradeoff, dispatch TOCTOU). Verification: bun test test/hook test/goal -> 206 pass/0 fail; bun typecheck clean; httpapi-exercise pass=204 fail=0 missing=0; JS SDK regenerated for the new session-hook routes. Co-authored-by: Lex's Agent <lex-agent@noreply.local> * fix(mcp): restore SDK client mocks after MCP tests (#69) Bun module mocks are process-global across the full test run. The MCP OAuth/lifecycle tests mock @modelcontextprotocol/sdk/client/index.js with reduced MockClient implementations that intentionally lack real transport helpers like callTool. When those mocks leak into later tests, the real elicitation transport test imports the mock Client and fails with: client.callTool is not a function. Restore the mock registry in afterAll for the MCP test files that mock the SDK Client module so subsequent tests receive the real SDK Client implementation. Verification: - bun test test/mcp/oauth-browser.test.ts test/mcp/oauth-auto-connect.test.ts test/mcp/lifecycle.test.ts test/mcp/elicitation-transport.test.ts -> 42 pass / 0 fail - bun test test/mcp -> 83 pass / 0 fail - bun typecheck clean Co-authored-by: Lex's Agent <lex-agent@noreply.local> * fix(mcp): isolate elicitation Client import from Bun mock registry (#71) The companion fix (#69) restores Bun's mock registry after the MCP test files that replace the SDK Client, but dev CI still reds: when bun re-runs the failed elicitation test under --only-failures, the module cache still holds the mocked Client binding, and the static `import { Client }` at the top of elicitation-transport.test.ts resolves to the MockClient lacking callTool. Two layered defenses: 1. Restore the mock registry at the top of this file (mock.restore()) — clears any stale process-global mock before the test runs. 2. Load the Client via dynamic `await import(...)` after the restore — ensures the runtime resolution goes through the post-restore registry, not a static-import-hoisted cache entry that may already be contaminated. This is defense-in-depth on top of #69's afterAll mock restoration in the mock-using files; together they close the leak both on the producer side (those tests stop polluting on exit) and the consumer side (this test refuses poisoned cache entries). Verification: - bun test test/mcp/elicitation-transport.test.ts -> 1 pass - bun test test/mcp/oauth-browser.test.ts test/mcp/oauth-auto-connect.test.ts test/mcp/lifecycle.test.ts test/mcp/elicitation-transport.test.ts -> 42 pass (mock-heavy files followed by elicitation) - bun test test/mcp -> 83 pass Co-authored-by: Lex's Agent <lex-agent@noreply.local> * fix(mcp): isolate elicitation Client binding from Bun mock cache (#72) The previous two fixes (#69 mock-restore in mock-using files + the prior mock.restore() + dynamic import in this file) closed the mock registry leak but not the module-cache leak: Bun keys module instances by specifier and cached the mocked "@modelcontextprotocol/sdk/client/index.js" binding from a prior test file, so even after mock.restore() the dynamic await import(...) on the same specifier returned the cached MockClient (no `callTool`). Use an alias specifier "@modelcontextprotocol/sdk/client" (no trailing /index.js) that resolves to the same ./client exports entry (so the real SDK Client is loaded) but is a different key in Bun's module cache (cache miss → re-resolves) and is not registered in the mock registry (mock-registry keys are exact specifier matches, not prefix-matched). Combined with mock.restore() clearing any stale mocks from siblings, this reliably loads the real Client with full callTool() support. Verification: - bun test test/mcp/elicitation-transport.test.ts -> 1 pass - bun test test/mcp/oauth-browser.test.ts test/mcp/oauth-auto-connect.test.ts test/mcp/lifecycle.test.ts test/mcp/elicitation-transport.test.ts -> 42 pass (mock-heavy followed by elicitation) - bun test test/mcp -> 83 pass - bun typecheck clean Co-authored-by: Lex's Agent <lex-agent@noreply.local> * fix(mcp): use query string cache-busting for Client import (#73) Turbo runs tests from multiple packages in parallel, and other packages' `mock.module` calls can pollute the global module cache even after `mock.restore()`. The previous fix (PR #72) used a sibling specifier `@modelcontextprotocol/sdk/client` to bypass the mocked `/index.js` path, but CI reruns still failed with `client.callTool is not a function`. Add a unique query string (`?bust=timestamp`) to force Bun to load a fresh module instance on each import. This bypasses any cached (mocked) module instances and ensures we get the real SDK Client with full `callTool` support, regardless of what other packages' tests have done to the global module cache. Verification: all 83 MCP tests pass locally (including mock-heavy OAuth tests that previously polluted the cache). Co-authored-by: Lex's Agent <lex-agent@noreply.local> * fix(hook): echo trust-file write failure in /trust (#74) Co-authored-by: Lex's Agent <lex-agent@noreply.local> * fix(hook,goal): close trust-gate self-escape + dead layer deps + single-pass chain + judge small model (#75) F1: readChain now strips allowUntrusted from non-global layers (project / worktree .opencode/hooks.json are authored by the repo the gate is meant to contain — letting them opt out would defeat a globally-enforced requireTrust). Updated docs; reversed the self-escaping integration test and added 3 unit cases (project / worktree stripped, global honored). F2: settings.ts layer body no longer yields unused events (EventV2Bridge) and db (Database); defaultLayer + node now depend on SessionHooks only, matching the documented 'lightweight construction' principle. F3: added loadChainWithSummaries (one readChain pass + deprecation scan). loadChain becomes a thin shell; the layer's initial state and hot-reload callback now read every hooks.json once (settings + summaries together), closing the transient-inconsistency window readChain was built to eliminate. F4: goal judge now prefers provider.getSmallModel for the ~200-token JSON verdict (config small_model / plugin hint / built-in haiku-flash-nano priority), falling back to the default model byte-for-byte when none resolves. 215 hook/goal/goal-tool tests pass; bun typecheck clean. Co-authored-by: Lex's Agent <lex-agent@noreply.local> --------- Co-authored-by: Test <test@opencode.test> Co-authored-by: Lex's Agent <lex-agent@noreply.local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR records origin/main as merged into dev using the ours strategy, while preserving dev's current tree exactly. It fixes the history divergence caused by previous dev → main release merges/squashes, so future dev → main PRs do not replay old main snapshots or hit the same conflicts.\n\nVerification:\n- Tree diff against origin/dev is empty.\n- origin/main is an ancestor of this branch.\n- Push hook typecheck passed.