Skip to content

feat(core): reload commands from config change feed - #39160

Merged
kitlangton merged 1 commit into
v2from
command-reload
Jul 27, 2026
Merged

feat(core): reload commands from config change feed#39160
kitlangton merged 1 commit into
v2from
command-reload

Conversation

@kitlangton

@kitlangton kitlangton commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What

Command source invalidation via config.changes: editing, creating, renaming, or deleting Markdown files under a config root's {command,commands} directory now rebuilds command state directly, without waiting for a config.updated event that descendant edits never trigger. Fourth slice of the config source reload series (#38983, #39131, #39157); fixes #37429 for existing config roots (new-root discovery lands later in the series).

How

ConfigCommandPlugin subscribes to the shared config.changes() feed exposed in #39131 and owns its path predicate locally, per the design's ownership split (Config owns topology, domains own which paths matter):

commands/review.md changes
  -> Watcher update flows through config.changes
  -> plugin filter matches is-or-under <root>/{command,commands}
  -> 100ms debounce coalesces bursts
  -> command state rebuilds from inline and Markdown sources
  -> command.updated publishes committed state
  • The predicate matches anything at or under <root>/{command,commands} with no file-suffix check, so directory-level events (e.g. mv commands commands-old) that carry no per-file paths still invalidate.
  • The subscription forks before the initial scan so an update racing the scan still triggers a rebuild.
  • The existing config.updated handler stays for structured config edits (inline commands in opencode.json); both feeds share one reload effect.

Testing

Reworked the source-lifecycle matrix against the renamed services (Command, Bus) using the #39157 fixtures — Config.testLayer provides the entries and Config.Test.emitChange drives the feed, no hand-rolled Watcher stubs:

  • Matrix: created / updated / renamed / deleted, each verifying inside the command.updated subscription so the event is asserted as a read barrier (committed state visible at delivery).
  • Debounce: three updates in one window produce exactly one rebuild; a later update produces a second (TestClock-driven).
  • Isolation: updates outside command source directories produce zero rebuilds, and the feed stays live afterwards.

Tests were red first (all six fail without the fix), then green with it. TestClock note: both Stream.debounce (100ms) and State.reload (500ms batch debounce) register their sleeps from fiber hops a single TestClock.adjust flush can miss, so the tests drive the clock with a settle/adjust loop (advance) that converges deterministically — verified stable across 10 consecutive full-file runs.

bun test test/config/ passes except the pre-existing PluginSupervisor config > reloads an auto-discovered plugin when its file changes failure present on clean base; bun typecheck clean.

Stack

  1. test-fixtures
  2. feat(core): reload commands from config change feed #39160 👈 current

Base automatically changed from test-fixtures to v2 July 27, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant