Skip to content

feat(hooks): dynamic Active Hooks block + /create-hook + leftover fixes + projector guard#57

Merged
LeXwDeX merged 5 commits into
devfrom
feat/hooks-dynamic-context
Jul 3, 2026
Merged

feat(hooks): dynamic Active Hooks block + /create-hook + leftover fixes + projector guard#57
LeXwDeX merged 5 commits into
devfrom
feat/hooks-dynamic-context

Conversation

@LeXwDeX

@LeXwDeX LeXwDeX commented Jul 3, 2026

Copy link
Copy Markdown
Owner

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 state
  • SystemPrompt.hooks(): per-turn rendered ## Active Hooks block in system prompt (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 — P0)
  • Complete event list (27) and type list (5 incl mcp) in format reference

2. fix(hooks): Dedup skill descriptions + hot-reload mtime detection

  • Export description constants from core/plugin/skill.ts, import in opencode/skill/index.ts (descriptions had already drifted — one had "commands" the other didn't)
  • hot-reload: m > prevm !== prev to cover cp -p / touch -t restoring older mtimes

3. fix(core): Guard projector against orphaned part updates

  • PartUpdated events from interrupted streams arriving after revert cleanup deleted the parent message no longer crash with FOREIGN KEY constraint failed — silently skipped instead
  • Regression test included

Verification

  • bun typecheck green on all 29 packages (pre-commit hook, FULL TURBO cached)
  • 19 new + 21 existing hook + 79 permission + 3 hot-reload + 9 projector tests all pass
  • Manual smoke: Active Hooks block correctly renders in live TUI system prompt

OpenSpec

  • hooks-dynamic-context-and-create-command (17/17 tasks ✓)
  • fix-hooks-leftovers (6/6 tasks ✓)

Test 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 LeXwDeX merged commit f631d46 into dev Jul 3, 2026
4 checks passed
@LeXwDeX LeXwDeX deleted the feat/hooks-dynamic-context branch July 3, 2026 14:55
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>
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