You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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
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.
What
Move ambient
AGENTS.mdfilesystem acquisition out ofInstructionDiscoveryand into a config-side internal plugin.InstructionDiscoverynow stores loaded instruction values in ordered, path-keyed state and exposes draft operations for config-side producers. It keeps the existingInstructionssource 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
packages/core/src/instruction-discovery.tsinto aState-backed value service with orderedlist/add/update/removedrafts and explicit unavailable state.packages/core/src/config/plugin/instruction.tsto canonicalize location/project boundaries, load global and upward-projectAGENTS.mdsources, and derive the finite candidate set from the working directory's ancestor chain.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/watchernative binding.InstructionDiscovery.Event.Updatedonly after committed values are visible.PluginSupervisor.flushcompletes initial acquisition beforeSessionContext.select()reads values.Testing
cd packages/core && bunx tsgo --noEmitcd packages/core && bun run typecheckcd 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)bun turbo typecheck --concurrency=3(33 tasks passed across the workspace)cd packages/core && bun run testreached 1,632 passed, 16 skipped, and one unrelated environment-dependent failure inPluginSupervisor config > logs invalid packages and continues loading; the assertion sees two plugins from the user's global config, and the test reproduces alone.Findings
InstructionDiscoverywas a stateless location-scoped service whose soleload()call canonicalized the working/project boundaries, scanned upward forAGENTS.md, canonicalized discovered files, and read content. It had no cache or watcher; every session selection performed fresh acquisition.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.Instructions.unavailable, preserving admitted values. Confirmed zero files yieldedInstructions.removed.SessionContext.select()waits forPluginSupervisor.flush, then composes built-ins, ambient discovery, code mode, skills, references, MCP guidance, and session entries.InstructionStatehashes and durably admits those values before request assembly.SessionInstructionsis 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.