feat(hooks): dynamic Active Hooks block + /create-hook + leftover fixes + projector guard#57
Merged
Merged
Conversation
added 4 commits
July 3, 2026 21:45
- 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
- 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
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.
Temporary analysis document from review process; content is now stale (issues ①② fixed in this PR) or tracked separately (issue ③).
- 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)
LeXwDeX
added a commit
that referenced
this pull request
Jul 3, 2026
* 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>
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.
Summary
Three logical changes bundled in one PR:
1. feat(hooks): Dynamic Active Hooks block + /create-hook command
SettingsHook.list(): read-only scope-tagged summaries over hot-reloaded stateSystemPrompt.hooks(): per-turn rendered## Active Hooksblock in system prompt (zero tokens when no hooks)/create-hookcommand: interactive hook authoring intohooks.jsonHooks_STARTsnapshot (replaced by dynamic block)import-claude-hooks.txtJSON format examples (missinghookswrapper — P0)2. fix(hooks): Dedup skill descriptions + hot-reload mtime detection
core/plugin/skill.ts, import inopencode/skill/index.ts(descriptions had already drifted — one had "commands" the other didn't)m > prev→m !== prevto covercp -p/touch -trestoring older mtimes3. fix(core): Guard projector against orphaned part updates
PartUpdatedevents from interrupted streams arriving after revert cleanup deleted the parent message no longer crash withFOREIGN KEY constraint failed— silently skipped insteadVerification
bun typecheckgreen on all 29 packages (pre-commit hook, FULL TURBO cached)OpenSpec
hooks-dynamic-context-and-create-command(17/17 tasks ✓)fix-hooks-leftovers(6/6 tasks ✓)