-
Notifications
You must be signed in to change notification settings - Fork 354
fix(discovery): resolve all native user-scope surfaces from the agent directory (#4769) #4784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1013,7 +1013,9 @@ async function collectCommands(cwd: string, activeSettings: SettingsInstance): P | |
| const disabledExts = disabledExtensionIds(activeSettings); | ||
| const disabledProviders = new Set(activeSettings.get("disabledProviders")); | ||
| // Exact session-startup consumer (interactive/print modes). | ||
| const loadedNames = new Set((await loadSlashCommands({ cwd })).map(cmd => cmd.name)); | ||
| const loadedNames = new Set( | ||
| (await loadSlashCommands({ cwd, agentDir: activeSettings.getAgentDir() })).map(cmd => cmd.name), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| ); | ||
|
|
||
| const items: CustomizeDoctorItem[] = entries.map(entry => { | ||
| const base = baseItem("command", entry); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ import { | |
| discoverExtensionModulePaths, | ||
| expandEnvVarsDeep, | ||
| getExtensionNameFromPath, | ||
| getUserSkillScanDirs, | ||
| loadFilesFromDir, | ||
| SOURCE_PATHS, | ||
| scanSkillsFromDir, | ||
|
|
@@ -41,8 +42,8 @@ const PRIORITY = 100; | |
|
|
||
| const PATHS = SOURCE_PATHS.native; | ||
|
|
||
| function getUserAgentDirs(): string[] { | ||
| return [PATHS.userAgent]; | ||
| function getUserAgentDirs(ctx: LoadContext): string[] { | ||
| return [resolveUserAgentDir(ctx)]; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -56,7 +57,7 @@ function getUserAgentDirs(): string[] { | |
| * profile's servers into it. | ||
| */ | ||
| function resolveUserAgentDir(ctx: LoadContext): string { | ||
| return ctx.userAgentDir ?? getAgentDir(); | ||
| return path.resolve(ctx.userAgentDir ?? getAgentDir()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| function getProjectConfigDirs(): string[] { | ||
|
|
@@ -84,8 +85,8 @@ async function getConfigDirs(ctx: LoadContext): Promise<Array<{ dir: string; lev | |
| result.push({ dir: projectDir, level: "project" }); | ||
| } | ||
| } | ||
| for (const userAgentDir of getUserAgentDirs()) { | ||
| const userDir = await ifNonEmptyDir(ctx.home, userAgentDir); | ||
| for (const userAgentDir of getUserAgentDirs(ctx)) { | ||
| const userDir = await ifNonEmptyDir(userAgentDir); | ||
|
Comment on lines
+88
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| if (userDir) { | ||
| result.push({ dir: userDir, level: "user" }); | ||
| } | ||
|
|
@@ -282,17 +283,18 @@ registerProvider<MCPServer>(mcpCapability.id, { | |
| async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> { | ||
| const items: SystemPrompt[] = []; | ||
|
|
||
| for (const userAgentDir of getUserAgentDirs()) { | ||
| const userPath = path.join(ctx.home, userAgentDir, "SYSTEM.md"); | ||
| const userContent = await readFile(userPath); | ||
| if (userContent) { | ||
| items.push({ | ||
| path: userPath, | ||
| content: userContent, | ||
| level: "user", | ||
| _source: createSourceMeta(PROVIDER_ID, userPath, "user"), | ||
| }); | ||
| } | ||
| // User scope is the agent directory — the directory `gjc config dir` prints | ||
| // and the only user-scope seam. A profile is a separate scope; the default | ||
| // profile's home-relative SYSTEM.md is not read under one. | ||
| const userPath = path.join(resolveUserAgentDir(ctx), "SYSTEM.md"); | ||
| const userContent = await readFile(userPath); | ||
| if (userContent) { | ||
| items.push({ | ||
| path: userPath, | ||
| content: userContent, | ||
| level: "user", | ||
| _source: createSourceMeta(PROVIDER_ID, userPath, "user"), | ||
| }); | ||
| } | ||
|
|
||
| const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot); | ||
|
|
@@ -336,9 +338,9 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> { | |
| ); | ||
|
|
||
| // User-level scan from ~/.gjc/agent/skills/ | ||
| const userScans = getUserAgentDirs().map(userAgentDir => | ||
| const userScans = getUserSkillScanDirs(ctx.home, resolveUserAgentDir(ctx)).map(dir => | ||
| scanSkillsFromDir(ctx, { | ||
| dir: path.join(ctx.home, userAgentDir, "skills"), | ||
| dir, | ||
| providerId: PROVIDER_ID, | ||
| level: "user", | ||
| requireDescription: true, | ||
|
|
@@ -412,13 +414,11 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> { | |
| // Top-level RULES.md is a sticky always-apply rule. The context-file | ||
| // discovery contract treats it as the file "re-injected near the current | ||
| // turn so they keep hold across long conversations". | ||
| // User scope: ~/.gjc/agent/RULES.md | ||
| // User scope: <agentDir>/RULES.md (a profile is a separate user scope; | ||
| // the default profile's home-relative copy is not read) | ||
| // Project scope: nearest .gjc/RULES.md walking up from cwd to repoRoot | ||
| for (const userAgentDir of getUserAgentDirs()) { | ||
| const userRulesFile = path.join(ctx.home, userAgentDir, "RULES.md"); | ||
| const userRule = await loadStickyRulesFile(userRulesFile, "user"); | ||
| if (userRule) items.push(userRule); | ||
| } | ||
| const userRule = await loadStickyRulesFile(path.join(resolveUserAgentDir(ctx), "RULES.md"), "user"); | ||
| if (userRule) items.push(userRule); | ||
|
|
||
| const nearestProjectConfigDir = await findNearestProjectConfigDir(ctx.cwd, ctx.repoRoot); | ||
| if (nearestProjectConfigDir) { | ||
|
|
@@ -933,7 +933,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil | |
| const items: ContextFile[] = []; | ||
| const warnings: string[] = []; | ||
|
|
||
| const userPath = path.join(ctx.home, PATHS.userAgent, "AGENTS.md"); | ||
| const userPath = path.join(resolveUserAgentDir(ctx), "AGENTS.md"); | ||
| const userContent = await readFile(userPath); | ||
| if (userContent) { | ||
| items.push({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,6 +110,36 @@ export function getUserPath(ctx: LoadContext, source: SourceId, subpath: string) | |
| return path.join(ctx.home, paths.userAgent, subpath); | ||
| } | ||
|
|
||
| /** | ||
| * Deterministic user-scope skill scan directories, highest precedence first. | ||
| * | ||
| * The canonical root is the agent directory (`getAgentDir()`, moved by | ||
| * `--agent-dir` / `GJC_CODING_AGENT_DIR` / `setAgentDir()`) — the target of | ||
| * every user-scope skill writer (`gjc migrate`, `gjc skill`). An agent-directory | ||
| * profile is a *separate* user scope, the same contract as MCP user config | ||
| * (#4768): its legacy home-relative roots are not scanned, so a profile cannot | ||
| * pick up the default profile's skills (and vice versa). In the default | ||
| * profile the agent directory is `<home>/<configDir>/agent` and the configured | ||
| * legacy roots below it are still honored, exactly as before. | ||
| */ | ||
| export function resolveUserAgentDir(home: string, userAgentDir?: string): string { | ||
| return path.resolve(userAgentDir ?? path.join(home, SOURCE_PATHS.native.userAgent)); | ||
| } | ||
|
|
||
| export function getUserSkillScanDirs(home: string, userAgentDir?: string): string[] { | ||
| const resolvedAgentDir = resolveUserAgentDir(home, userAgentDir); | ||
| if (resolvedAgentDir !== path.resolve(path.join(home, SOURCE_PATHS.native.userAgent))) { | ||
| return [path.join(resolvedAgentDir, "skills")]; | ||
|
Comment on lines
+129
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This user-visible change redirects skills and other native user-scope surfaces to the selected agent directory, but the diff does not add a corresponding entry to AGENTS.md reference: AGENTS.md:L188-L188 Useful? React with 👍 / 👎. |
||
| } | ||
| return [ | ||
| ...new Set([ | ||
| path.join(home, SOURCE_PATHS.native.userAgent, "skills"), | ||
| path.join(home, SOURCE_PATHS.native.userBase, "skills"), | ||
| path.join(home, ".gjc", "skills"), | ||
| ]), | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Get project-level path for a source (cwd only). | ||
| */ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gjc config dirwithgjc config pathIn both user-scope installation snippets,
gjc config diris an invalid command:parseConfigArgsrejects every action outside its allowlist, while the CLI help identifiesgjc config pathas the command that prints the config directory (packages/coding-agent/src/cli/config-cli.ts:127-146,640-645). Consequently the command substitution produces no path, so these examples fail or attempt to write under/skills/my-skill; use$(gjc config path)in the snippets and related guidance.Useful? React with 👍 / 👎.