Skip to content

refactor(core): move instruction discovery to the config side - #41629

Merged
kitlangton merged 4 commits into
v2from
refactor/instruction-discovery-config-side
Aug 11, 2026
Merged

refactor(core): move instruction discovery to the config side#41629
kitlangton merged 4 commits into
v2from
refactor/instruction-discovery-config-side

Conversation

@kitlangton

@kitlangton kitlangton commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What

Move ambient AGENTS.md filesystem acquisition out of InstructionDiscovery and into a config-side internal plugin.

InstructionDiscovery now stores loaded instruction values in ordered, path-keyed state and exposes draft operations for config-side producers. It keeps the existing Instructions source key and rendering semantics used by session prompt admission.

Why

This continues the ownership direction established by #41618 and #41622: core domain services own values, while config-side modules own filesystem discovery, parsing, canonicalization, and watching.

Previously, InstructionDiscovery.load() performed canonicalization, upward scanning, and file reads inside the core service on every selection. That made the prompt-facing service itself filesystem-aware.

How

  • Refactor packages/core/src/instruction-discovery.ts into a State-backed value service with ordered list/add/update/remove drafts and explicit unavailable state.
  • Add packages/core/src/config/plugin/instruction.ts to canonicalize location/project boundaries, load global and upward-project AGENTS.md sources, and derive the finite candidate set from the working directory's ancestor chain.
  • Watch every candidate with an exact type: "file" subscription. These cheap parent-directory watches detect create/update/delete even when a candidate does not exist yet, avoid recursively watching the project tree, and do not depend on the optional @parcel/watcher native binding.
  • Re-scan and re-draft values on relevant watcher updates; publish InstructionDiscovery.Event.Updated only after committed values are visible.
  • Replacing select-time reads adds a bounded staleness window (filesystem-event latency plus the State reload debounce, approximately <=1s): a racing prompt may render pre-edit content once, while the following prompt is guaranteed fresh.
  • Isolate global/project source failures with warnings, log skipped missing files at debug level, and degrade activation/setup failures to unavailable state instead of failing plugin activation.
  • Register the instruction config plugin with the existing internal post-config plugins so PluginSupervisor.flush completes initial acquisition before SessionContext.select() reads values.
sequenceDiagram
  participant Config as ConfigInstructionPlugin
  participant FS as FSUtil + Watcher
  participant Values as InstructionDiscovery
  participant Session as SessionContext
  participant Prompt as InstructionState

  Config->>FS: canonicalize, scan, and read AGENTS.md sources
  Config->>Values: transform loaded values
  Values-->>Config: commit, then publish Updated
  FS-->>Config: relevant file update
  Config->>Values: refresh and reload draft
  Session->>Values: load filesystem-free instruction source
  Session->>Prompt: compose and admit instruction values
Loading

Testing

  • cd packages/core && bunx tsgo --noEmit
  • cd packages/core && bun run typecheck
  • cd packages/core && bun run test test/instruction-discovery.test.ts test/instruction-state.test.ts test/mcp-instructions.test.ts test/reference-instructions.test.ts test/session-instructions.test.ts test/instructions/builtins.test.ts test/instructions/index.test.ts test/codemode/instructions.test.ts test/skill/instructions.test.ts (60 passed)
  • Push hook: bun turbo typecheck --concurrency=3 (33 tasks passed across the workspace)
  • cd packages/core && bun run test reached 1,632 passed, 16 skipped, and one unrelated environment-dependent failure in PluginSupervisor config > logs invalid packages and continues loading; the assertion sees two plugins from the user's global config, and the test reproduces alone.

Findings

  • InstructionDiscovery was a stateless location-scoped service whose sole load() call canonicalized the working/project boundaries, scanned upward for AGENTS.md, canonicalized discovered files, and read content. It had no cache or watcher; every session selection performed fresh acquisition.
  • Discovery order was global config AGENTS.md, then nearest-to-farthest project files through the canonical project root. Canonical path deduplication prevented the same global/project file from rendering twice.
  • A missing global file contributed nothing; a project file disappearing after discovery or any observation failure yielded Instructions.unavailable, preserving admitted values. Confirmed zero files yielded Instructions.removed.
  • SessionContext.select() waits for PluginSupervisor.flush, then composes built-ins, ambient discovery, code mode, skills, references, MCP guidance, and session entries. InstructionState hashes and durably admits those values before request assembly.
  • SessionInstructions is a separate read-tool path: it loads nested instructions discovered while reading files and publishes durable synthetic messages. This PR deliberately leaves that tool-ground behavior unchanged.
  • The template fit because both the config plugin and values service are location-scoped. The main difference from skills is semantic ordering plus unavailable-vs-removed state, which remain explicit in the value service and tests.

@kitlangton

Copy link
Copy Markdown
Contributor Author

Updated the watching strategy to avoid recursive directory subscriptions entirely.

Watcher.Service implements type: "file" with non-recursive node:fs.watch on the target's parent and filters to the exact path, including paths that do not exist yet. type: "directory" delegates to recursive @parcel/watcher; when its native binding is unavailable, that subscription stream ends. Ambient instruction discovery now computes the static candidate set once (global AGENTS.md plus every AGENTS.md path on the canonical working-directory-to-project-root ancestor chain) and establishes one exact file subscription per deduplicated candidate. This catches create/update/delete without recursively watching the project or depending on the Parcel binding.

The simplify pass also replaced duplicate containment logic with FSUtil.contains, narrowed the read helper to File | undefined, and replaced repeated sentinel array checks with a tagged loaded snapshot. The mutable snapshot remains necessary because the registered synchronous state transform replays the latest scan; the activation catchCause boundary remains nested so setup failures can commit unavailable state.

Tests now assert the exact candidate subscription set and cover creating a previously absent intermediate AGENTS.md with nearest-to-farthest ordering; existing coverage verifies deletion removes the value. bun run typecheck passes, 60 focused instruction tests pass, and the push hook's workspace typecheck passes all 33 tasks.

@kitlangton
kitlangton force-pushed the refactor/instruction-discovery-config-side branch from 127228c to 30234d8 Compare August 11, 2026 01:27
@kitlangton
kitlangton marked this pull request as ready for review August 11, 2026 02:32
@kitlangton
kitlangton merged commit 5d8c487 into v2 Aug 11, 2026
8 checks passed
@kitlangton
kitlangton deleted the refactor/instruction-discovery-config-side branch August 11, 2026 02:33
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