Skip to content

chore: sync main history into dev#65

Merged
LeXwDeX merged 3 commits into
devfrom
chore/sync-main-history
Jul 4, 2026
Merged

chore: sync main history into dev#65
LeXwDeX merged 3 commits into
devfrom
chore/sync-main-history

Conversation

@LeXwDeX

@LeXwDeX LeXwDeX commented Jul 4, 2026

Copy link
Copy Markdown
Owner

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.

LeXwDeX and others added 3 commits July 3, 2026 14:15
* 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 LeXwDeX enabled auto-merge (squash) July 4, 2026 14:06
@LeXwDeX LeXwDeX merged commit 306b543 into dev Jul 4, 2026
4 checks passed
@LeXwDeX LeXwDeX deleted the chore/sync-main-history branch July 4, 2026 14:07
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant