mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(workspace-context): load .local. instruction overlays by default
Load a per-directory local overlay in addition to the base instruction file, matching the Claude Code AGENTS.local.md / CLAUDE.local.md convention for git-ignored personal guidance. - New config `localInstructionFileCandidates`, default `['AGENTS.local.md', 'CLAUDE.local.md']`; empty disables the overlay. The default lives in the plugin Config schema, so every front door (TUI/ACP/headless) reads .local. files consistently. - Per project directory the plugin loads the first-existing base candidate, then additively the first-existing local candidate, rendered after the base so it takes precedence within the byte budget. - Base and local tiers get distinct scope keys via a NUL sentinel (scopeKey/decodeScopeKey) so they never collide in the baseline map, pending window, or version cache. - The fixed user-global $DSH_HOME/AGENTS.md stays base-only. Docs: README (config, lifecycle, Known Limitations), regenerated config-catalog, and a new bilingual Agent Note cross-linked to the owning workspace-context note. 100% per-file coverage retained.
This commit is contained in:
@@ -81,6 +81,35 @@ export function scopeForDisplayPath(displayPath: string): string {
|
||||
return dirname(displayPath)
|
||||
}
|
||||
|
||||
/** Instruction tier: the native base file or the additive local overlay. */
|
||||
export type InstructionTier = 'base' | 'local'
|
||||
|
||||
const LOCAL_SCOPE_SUFFIX = '\u0000local'
|
||||
|
||||
/**
|
||||
* Compose the reconciliation key for a directory scope and instruction tier.
|
||||
* The base tier keeps the human-readable directory; the local overlay appends a
|
||||
* NUL-delimited marker that no directory path can contain, so a directory's base
|
||||
* and local files never collide in the scope-keyed state maps.
|
||||
* @param directory - `user-global`, `.`, or a project-relative directory.
|
||||
* @param tier - base file or additive local overlay.
|
||||
* @returns the collision-free logical scope key.
|
||||
*/
|
||||
export function scopeKey(directory: string, tier: InstructionTier): string {
|
||||
return tier === 'local' ? `${directory}${LOCAL_SCOPE_SUFFIX}` : directory
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the directory and tier that {@link scopeKey} encoded.
|
||||
* @param scope - a base or local scope key.
|
||||
* @returns the directory scope and its instruction tier.
|
||||
*/
|
||||
export function decodeScopeKey(scope: string): { directory: string; tier: InstructionTier } {
|
||||
return scope.endsWith(LOCAL_SCOPE_SUFFIX)
|
||||
? { directory: scope.slice(0, -LOCAL_SCOPE_SUFFIX.length), tier: 'local' }
|
||||
: { directory: scope, tier: 'base' }
|
||||
}
|
||||
|
||||
function additionalSectionText(file: LoadedInstructionFile): string {
|
||||
const scope = scopeForDisplayPath(file.displayPath)
|
||||
return [
|
||||
@@ -102,7 +131,7 @@ function changedSectionText(item: ChangeRenderItem): string {
|
||||
}
|
||||
const description = change.previousPath === undefined
|
||||
? 'This file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.'
|
||||
: `The instructions previously loaded from \`${change.previousPath}\` no longer apply. Use the following content for \`${change.scope}\` instead.`
|
||||
: `The instructions previously loaded from \`${change.previousPath}\` no longer apply. Use the following content for \`${scopeForDisplayPath(change.path)}\` instead.`
|
||||
return [
|
||||
`Updated instructions from: ${change.path}`,
|
||||
'',
|
||||
|
||||
Reference in New Issue
Block a user