DSH / Atlas
2026-07-21implementedfeature

Default local instruction overlay

默认的本地指令覆盖层

Personal, git-ignored guidance (`AGENTS.local.md` / `CLAUDE.local.md`) is a Claude Code convention for per-developer overrides that are deliberately not committed. The [agent-instructions plugin](2026-06-24-workspace-context.md) loaded only one candidate per directory, so a `.local.` name could only be reached by adding it to `instructionFileCandidates`, where — because a directory has one winner — it would *shadow*

English

Problem

Personal, git-ignored guidance (AGENTS.local.md / CLAUDE.local.md) is a Claude Code convention for per-developer overrides that are deliberately not committed. The agent-instructions plugin loaded only one candidate per directory, so a .local. name could only be reached by adding it to instructionFileCandidates, where — because a directory has one winner — it would shadow the committed base file instead of supplementing it. That inverts the additive "base plus personal overlay" model the names evoke, and it was off by default.

Decision

The plugin loads a second, independent candidate list per project directory. localInstructionFileCandidates defaults to ['AGENTS.local.md', 'CLAUDE.local.md'] and is resolved with the same same-directory validation as instructionFileCandidates. In every project directory from the root to the session cwd, the plugin loads the base candidates and then, additively, the local candidates; the local files are ordered after the base files so their guidance takes precedence within the byte budget. Both lists load in full under per-directory content dedup. An empty localInstructionFileCandidates disables the overlay.

The default lives in the plugin Config schema rather than a product cordis.yml, so every embedder (TUI, ACP, headless) reads .local. files consistently and a deployment overrides or disables the behavior in one place. This is symmetric with the plugin-owned instructionFileCandidates default.

The fixed user-global $DSH_HOME/AGENTS.md has no local overlay and stays base-only.

Independent scopes per candidate

The base and local candidates in one directory must stay independent across baseline freezing, the pending window, the version cache, and reconciliation, so a change to one never suppresses the other. Each (directory, candidateName) pair is its own scope key — see per-candidate scope keys, which replaced the earlier base/local tier sentinel. Discovery iterates the base list and then the local list in each project directory, reconcileInstructionContext enumerates every configured candidate per directory, and probeScopeInstruction decodes the candidate name to read exactly that file. The model-facing prompt derives its human directory label from the file display path, so the scope key never reaches the model.

Alternatives considered

Higher-priority first-wins (.local. loaded instead of the base file). Rejected: a personal overlay that replaces the committed file drops shared project guidance whenever the overlay exists, which is the opposite of the additive Claude Code model.

Keep it opt-in through instructionFileCandidates. Rejected: one directory has a single winner, so a .local. name added to that list shadows the base file rather than supplementing it. The packages guidance to keep opt-ins out of shipped defaults is outweighed here by strong prior art and the user-facing expectation that .local. files are always read.

Default at the product cordis.yml level instead of the plugin schema. Rejected: it would enable .local. only for whichever entry point remembered to opt in, splitting behavior across TUI/ACP/headless and duplicating a value that belongs beside the existing candidate default.

Reuse the bare directory as the scope key for base and local files. Rejected: base and local files in one directory would collide in every scope-keyed map, so a change to one would suppress or overwrite the other. A distinct scope key per candidate keeps them independent without widening the persisted metadata shape.

Extend the overlay to the user-global scope. Deferred: $DSH_HOME is a single fixed AGENTS.md with no committed base to supplement, so it stays base-only until a concrete need appears.

Consequences

.local. guidance is read by default across all products with no per-deployment configuration, matching neighboring tools. Each project directory can contribute a durable scope per existing candidate rather than one, so dynamic discovery, edits, and removals reconcile the base and local files independently. The scope key is now per-candidate; dsh-session keeps no compatibility promise for older sessions, so this is a free change. The user-global scope remains base-only, recorded as a Known Limitation in the package README.

中文

问题

被 git 忽略的个人指导文件(AGENTS.local.md / CLAUDE.local.md)是 Claude Code 的一项约定,用于存放刻意不提交、每位开发者各自的覆盖内容。agent-instructions 插件每个目录只加载一个候选,因此只有把某个 .local. 名字加进 instructionFileCandidates 才能读到它;而由于一个目录只有一个胜出者,这样做只会让它遮蔽已提交的基础文件,而不是补充它。这与这些名字所暗示的「基础文件加个人覆盖层」的叠加模型正好相反,而且它默认是关闭的。

决策

插件为每个项目目录额外加载第二个独立的候选列表。localInstructionFileCandidates 默认为 ['AGENTS.local.md', 'CLAUDE.local.md'],并与 instructionFileCandidates 采用相同的同目录校验来解析。在从项目根到会话 cwd 的每个项目目录中,插件先加载基础候选,然后叠加加载本地候选;本地文件排在基础文件之后,因此在字节预算之内其内容优先级更高。两个列表都会在按目录内容去重之下完整加载。将 localInstructionFileCandidates 置空即可关闭该覆盖层。

该默认值定义在插件的 Config schema 中,而非某个产品的 cordis.yml 里,因此每个嵌入方(TUI、ACP、headless)读取 .local. 文件的行为一致,部署方也可以在一处覆盖或关闭该行为。这与插件自身持有的 instructionFileCandidates 默认值保持对称。

固定的用户全局文件 $DSH_HOME/AGENTS.md 没有本地覆盖层,始终只有基础文件。

每个候选各自独立的 scope

同一目录下的基础候选与本地候选,在基线冻结、待定窗口、版本缓存和协调过程中都必须彼此独立,因此对其中一个的改动绝不能抑制另一个。现在每个 (directory, candidateName) 对都是各自独立的 scope 键——参见按候选划分的 scope 键,它取代了此前基础/本地的层级哨兵。发现过程在每个项目目录中先遍历基础列表、再遍历本地列表,reconcileInstructionContext 为每个目录枚举每个配置的候选,probeScopeInstruction 则解码候选名以精确读取该文件。面向模型的提示词从文件的展示路径推导出供人阅读的目录标签,因此 scope 键永远不会到达模型。

备选方案

更高优先级的先到先得(加载 .local. 而非基础文件)。 否决:一个会替换已提交文件的个人覆盖层,会在覆盖层存在时丢弃共享的项目指导,这与 Claude Code 的叠加模型正好相反。

通过 instructionFileCandidates 保持按需开启。 否决:一个目录只有一个胜出者,因此加进该列表的 .local. 名字会遮蔽基础文件,而非补充它。packages 指引要求把按需开启项排除在出厂默认之外,但此处强有力的现有实践、以及用户对 .local. 文件总会被读取的预期,压过了这一考量。

在产品 cordis.yml 层面设默认,而非在插件 schema 中。 否决:这样只会为记得开启该功能的那个产品入口启用 .local.,从而在 TUI/ACP/headless 之间割裂行为,并重复一个本应与既有候选默认值放在一起的取值。

两个层级复用原始目录作为 scope 键。 否决:同一目录下的基础文件与本地文件会在每个以 scope 为键的映射中冲突,于是对其中一个的改动会抑制或覆盖另一个。为每个候选设置各自独立的 scope 键让两者保持独立,且无需扩展持久化的元数据结构。

将覆盖层扩展到用户全局 scope。 暂缓:$DSH_HOME 是单个固定的 AGENTS.md,没有可供补充的已提交基础文件,因此在出现具体需求前始终只有基础文件。

后果

.local. 指导在所有产品中默认被读取,无需按部署单独配置,与邻近工具保持一致。每个项目目录可以为每个存在的候选贡献一个持久 scope 而非仅一个,因此动态发现、编辑和移除会分别独立地协调基础文件与本地文件。scope 键现在按候选划分dsh-session 对旧会话不作兼容承诺,因此这是一次无成本的改动。用户全局 scope 仍然只有基础文件,这一点作为已知限制记录在包 README 中。