From a5b71eae8b44c5c91cae11ca0c2255095ce81134 Mon Sep 17 00:00:00 2001 From: ugeshpraavin Date: Fri, 25 Sep 2026 14:39:20 +0530 Subject: [PATCH 1/6] fix: ignore hidden agent skills during review --- .changeset/icy-loops-act.md | 5 +++++ packages/intent/src/maintainer/existing.ts | 6 ++---- packages/intent/src/review/review.ts | 4 ++-- packages/intent/src/shared/utils.ts | 13 +++++++++++++ packages/intent/tests/review.test.ts | 20 ++++++++++++++++++++ 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 .changeset/icy-loops-act.md diff --git a/.changeset/icy-loops-act.md b/.changeset/icy-loops-act.md new file mode 100644 index 0000000..30d8324 --- /dev/null +++ b/.changeset/icy-loops-act.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Fix maintainer review discovering skills inside hidden agent directories diff --git a/packages/intent/src/maintainer/existing.ts b/packages/intent/src/maintainer/existing.ts index 0434ca0..4489ef4 100644 --- a/packages/intent/src/maintainer/existing.ts +++ b/packages/intent/src/maintainer/existing.ts @@ -3,7 +3,7 @@ import { existsSync } from 'node:fs' import { basename, dirname, relative } from 'node:path' import { resolveProjectContext } from '../core/project-context.js' import { resolveWorkspacePackages } from '../setup/workspace-patterns.js' -import { parseFrontmatter } from '../shared/utils.js' +import { parseFrontmatter, isDefaultSkillPath } from '../shared/utils.js' import { stringList } from './add.js' import { isObject, @@ -76,9 +76,7 @@ export function findExistingSkills( (path) => basename(path) === 'SKILL.md' && /(^|\/)skills\//.test(path) && - !path - .split('/') - .some((part) => part.startsWith('.') || part === 'node_modules') && + isDefaultSkillPath(path) && !registered.has(path), ) .sort() diff --git a/packages/intent/src/review/review.ts b/packages/intent/src/review/review.ts index 77c9b0d..d3d6394 100644 --- a/packages/intent/src/review/review.ts +++ b/packages/intent/src/review/review.ts @@ -14,7 +14,7 @@ import { import { basename, dirname, join, relative, resolve } from 'node:path' import { parse as parseYaml } from 'yaml' import { resolveProjectContext } from '../core/project-context.js' -import { parseFrontmatter } from '../shared/utils.js' +import { parseFrontmatter, isDefaultSkillPath } from '../shared/utils.js' type Snapshot = Record type Outcome = 'updated' | 'no-change' | 'out-of-scope' | 'unresolved' @@ -565,7 +565,7 @@ export function createReview(cwd: string, baseRef?: string): ReviewReport { const skillFiles = files.filter( (path) => basename(path) === 'SKILL.md' && - (/(^|\/)skills\//.test(path) || + ((/(^|\/)skills\//.test(path) && isDefaultSkillPath(path)) || customRoots.some((dir) => path.startsWith(`${dir}/`)) || declaredSkills.has(path) || state?.items[`skill:${path}`]), diff --git a/packages/intent/src/shared/utils.ts b/packages/intent/src/shared/utils.ts index d998558..8e47165 100644 --- a/packages/intent/src/shared/utils.ts +++ b/packages/intent/src/shared/utils.ts @@ -58,6 +58,19 @@ export function toPosixPath(p: string): string { return p.split(sep).join('/') } +/** + * Check whether a skill path belongs to the default skill discovery tree. + * + * Hidden directories and node_modules are excluded from default discovery, + * while explicitly declared or custom-root skills can still be handled by + * their respective discovery paths. + */ +export function isDefaultSkillPath(path: string): boolean { + return !path + .split('/') + .some((part) => part.startsWith('.') || part === 'node_modules') +} + export interface FsIdentityResolver { (path: string): string /** diff --git a/packages/intent/tests/review.test.ts b/packages/intent/tests/review.test.ts index 7741909..0ca0719 100644 --- a/packages/intent/tests/review.test.ts +++ b/packages/intent/tests/review.test.ts @@ -147,7 +147,27 @@ it('does not classify shipped meta skills or unrelated agent instructions as lib accept() expect(createReview(root).items).toEqual([]) }) +it('ignores hidden agent directories during default skill discovery', () => { + write( + '.claude/skills/deploy/SKILL.md', + '---\nname: deploy\ndescription: Deploy safely\n---\nDeploy guidance.\n', + ) + write( + '.cursor/skills/review/SKILL.md', + '---\nname: review\ndescription: Review safely\n---\nReview guidance.\n', + ) + write( + '.agents/skills/hidden/SKILL.md', + '---\nname: hidden\ndescription: Hidden guidance\n---\nHidden guidance.\n', + ) + git('add', '.') + git('commit', '-qm', 'hidden agent skills') + + expect(createReview(root).items.map((item) => item.id)).toEqual([ + 'skill:skills/request/SKILL.md', + ]) +}) it('reviews a repository-root skill without including its own review state', () => { renameSync(join(root, 'skills/request/SKILL.md'), join(root, 'SKILL.md')) planningRecords('_artifacts') From fc58f1c80f70e3901d8e80e397feb150c23cee94 Mon Sep 17 00:00:00 2001 From: ugeshpraavin Date: Thu, 1 Oct 2026 10:41:53 +0530 Subject: [PATCH 2/6] fix: sort shared utility imports --- packages/intent/src/maintainer/existing.ts | 2 +- packages/intent/src/review/review.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/intent/src/maintainer/existing.ts b/packages/intent/src/maintainer/existing.ts index 4489ef4..967e65d 100644 --- a/packages/intent/src/maintainer/existing.ts +++ b/packages/intent/src/maintainer/existing.ts @@ -3,7 +3,7 @@ import { existsSync } from 'node:fs' import { basename, dirname, relative } from 'node:path' import { resolveProjectContext } from '../core/project-context.js' import { resolveWorkspacePackages } from '../setup/workspace-patterns.js' -import { parseFrontmatter, isDefaultSkillPath } from '../shared/utils.js' +import { isDefaultSkillPath, parseFrontmatter } from '../shared/utils.js' import { stringList } from './add.js' import { isObject, diff --git a/packages/intent/src/review/review.ts b/packages/intent/src/review/review.ts index d3d6394..686fd34 100644 --- a/packages/intent/src/review/review.ts +++ b/packages/intent/src/review/review.ts @@ -14,7 +14,7 @@ import { import { basename, dirname, join, relative, resolve } from 'node:path' import { parse as parseYaml } from 'yaml' import { resolveProjectContext } from '../core/project-context.js' -import { parseFrontmatter, isDefaultSkillPath } from '../shared/utils.js' +import { isDefaultSkillPath, parseFrontmatter } from '../shared/utils.js' type Snapshot = Record type Outcome = 'updated' | 'no-change' | 'out-of-scope' | 'unresolved' @@ -648,10 +648,10 @@ export function createReview(cwd: string, baseRef?: string): ReviewReport { const dirs = existingArtifactDirs.length ? existingArtifactDirs : [ - resolveProjectContext({ cwd: root }).isMonorepo - ? '_artifacts' - : 'skills/_artifacts', - ] + resolveProjectContext({ cwd: root }).isMonorepo + ? '_artifacts' + : 'skills/_artifacts', + ] for (const dir of dirs) { const paths = artifactNames.map((name) => join(dir, name).replaceAll('\\', '/'), From bda7aa873f2f0ca6d1152bd7e312ec31a129e101 Mon Sep 17 00:00:00 2001 From: ugeshpraavin Date: Thu, 1 Oct 2026 12:26:41 +0530 Subject: [PATCH 3/6] test: cover hidden skills retained in review state --- packages/intent/tests/review.test.ts | 50 ++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/intent/tests/review.test.ts b/packages/intent/tests/review.test.ts index 0ca0719..ffe36ef 100644 --- a/packages/intent/tests/review.test.ts +++ b/packages/intent/tests/review.test.ts @@ -168,6 +168,52 @@ it('ignores hidden agent directories during default skill discovery', () => { 'skill:skills/request/SKILL.md', ]) }) +it('retains a hidden skill through review state without explicit declaration or custom root', () => { + const skillPath = '.agents/skills/hidden/SKILL.md' + + write( + skillPath, + '---\nname: hidden\nsources: [src/hidden.ts]\n---\nHidden guidance.\n', + ) + write('src/hidden.ts', 'export const hidden = true\n') + + planningRecords('_artifacts') + write( + '_artifacts/skill_tree.yaml', + `library: { name: library }\nskills: [{path: ${skillPath}}]\n`, + ) + + git('add', '.') + git('commit', '-qm', 'record hidden skill') + + const initial = createReview(root) + + expect(initial.items.map((item) => item.id)).toContain( + `skill:${skillPath}`, + ) + + expect( + initial.items.find((item) => item.id === `skill:${skillPath}`)?.problems, + ).toEqual([]) + + accept(initial) + + // Remove the explicit declaration. The skill should still be recognized + // because its previous review is retained in review state. + write( + '_artifacts/skill_tree.yaml', + 'library: { name: library }\nskills: []\n', + ) + + const retained = createReview(root) + + // The unchanged hidden skill is retained in review state and therefore + // remains eligible for review discovery. Since it has already been + // reviewed and has no changes, it does not need to appear as a new item. + expect(retained.items.map((item) => item.id)).not.toContain( + `source:${skillPath}`, + ) +}) it('reviews a repository-root skill without including its own review state', () => { renameSync(join(root, 'skills/request/SKILL.md'), join(root, 'SKILL.md')) planningRecords('_artifacts') @@ -764,7 +810,7 @@ it('still tracks an ignored path when a skill maps it as a source', () => { }) it('rejects a review record that annotates nothing instead of silently recording zero outcomes', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { }) const report = join(root, '.intent/review.json') write('.intent/review.json', JSON.stringify(createReview(root))) expect(await main(['review', root, '--record', report])).toBe(1) @@ -799,7 +845,7 @@ it('accepts a single evidence string when recording', () => { for (const item of report.items) { item.outcome = 'no-change' item.reason = 'Compared the source with the documented request behavior.' - ;(item as { evidence: unknown }).evidence = 'src/request.ts' + ; (item as { evidence: unknown }).evidence = 'src/request.ts' } expect(recordReview(root, report)).toBe(1) expect( From bd860b9e27e9f970277feb98a103036ca6101e9c Mon Sep 17 00:00:00 2001 From: Ugesh praavin Date: Fri, 2 Oct 2026 09:48:46 +0530 Subject: [PATCH 4/6] test: verify hidden skill retention through review state Co-authored-by: Sarah Gerrard --- packages/intent/tests/review.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/intent/tests/review.test.ts b/packages/intent/tests/review.test.ts index ffe36ef..0967b78 100644 --- a/packages/intent/tests/review.test.ts +++ b/packages/intent/tests/review.test.ts @@ -205,14 +205,16 @@ it('retains a hidden skill through review state without explicit declaration or 'library: { name: library }\nskills: []\n', ) - const retained = createReview(root) +write('src/hidden.ts', 'export const hidden = false\n') - // The unchanged hidden skill is retained in review state and therefore - // remains eligible for review discovery. Since it has already been - // reviewed and has no changes, it does not need to appear as a new item. - expect(retained.items.map((item) => item.id)).not.toContain( - `source:${skillPath}`, - ) +const retained = createReview(root) +const retainedSkill = retained.items.find( + (item) => item.id === `skill:${skillPath}`, +) + +expect(retainedSkill).toBeDefined() +expect(retainedSkill?.problems).toEqual([]) +expect(retainedSkill?.changedFiles).toContain('src/hidden.ts') }) it('reviews a repository-root skill without including its own review state', () => { renameSync(join(root, 'skills/request/SKILL.md'), join(root, 'SKILL.md')) From 96262ba13b5ab8906c21a302e9de9ffa0d85d94e Mon Sep 17 00:00:00 2001 From: ugeshpraavin Date: Fri, 2 Oct 2026 10:02:21 +0530 Subject: [PATCH 5/6] test: remove redundant review comment --- packages/intent/tests/review.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/intent/tests/review.test.ts b/packages/intent/tests/review.test.ts index 0967b78..3fa1299 100644 --- a/packages/intent/tests/review.test.ts +++ b/packages/intent/tests/review.test.ts @@ -198,8 +198,6 @@ it('retains a hidden skill through review state without explicit declaration or accept(initial) - // Remove the explicit declaration. The skill should still be recognized - // because its previous review is retained in review state. write( '_artifacts/skill_tree.yaml', 'library: { name: library }\nskills: []\n', From d0e6e1a443d9f595103f7c246241dcf3b8a2ba68 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:45:08 +0000 Subject: [PATCH 6/6] ci: apply automated fixes --- packages/intent/src/review/review.ts | 8 ++++---- packages/intent/tests/review.test.ts | 24 +++++++++++------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/intent/src/review/review.ts b/packages/intent/src/review/review.ts index 686fd34..106413d 100644 --- a/packages/intent/src/review/review.ts +++ b/packages/intent/src/review/review.ts @@ -648,10 +648,10 @@ export function createReview(cwd: string, baseRef?: string): ReviewReport { const dirs = existingArtifactDirs.length ? existingArtifactDirs : [ - resolveProjectContext({ cwd: root }).isMonorepo - ? '_artifacts' - : 'skills/_artifacts', - ] + resolveProjectContext({ cwd: root }).isMonorepo + ? '_artifacts' + : 'skills/_artifacts', + ] for (const dir of dirs) { const paths = artifactNames.map((name) => join(dir, name).replaceAll('\\', '/'), diff --git a/packages/intent/tests/review.test.ts b/packages/intent/tests/review.test.ts index 3fa1299..bcf07b2 100644 --- a/packages/intent/tests/review.test.ts +++ b/packages/intent/tests/review.test.ts @@ -188,9 +188,7 @@ it('retains a hidden skill through review state without explicit declaration or const initial = createReview(root) - expect(initial.items.map((item) => item.id)).toContain( - `skill:${skillPath}`, - ) + expect(initial.items.map((item) => item.id)).toContain(`skill:${skillPath}`) expect( initial.items.find((item) => item.id === `skill:${skillPath}`)?.problems, @@ -203,16 +201,16 @@ it('retains a hidden skill through review state without explicit declaration or 'library: { name: library }\nskills: []\n', ) -write('src/hidden.ts', 'export const hidden = false\n') + write('src/hidden.ts', 'export const hidden = false\n') -const retained = createReview(root) -const retainedSkill = retained.items.find( - (item) => item.id === `skill:${skillPath}`, -) + const retained = createReview(root) + const retainedSkill = retained.items.find( + (item) => item.id === `skill:${skillPath}`, + ) -expect(retainedSkill).toBeDefined() -expect(retainedSkill?.problems).toEqual([]) -expect(retainedSkill?.changedFiles).toContain('src/hidden.ts') + expect(retainedSkill).toBeDefined() + expect(retainedSkill?.problems).toEqual([]) + expect(retainedSkill?.changedFiles).toContain('src/hidden.ts') }) it('reviews a repository-root skill without including its own review state', () => { renameSync(join(root, 'skills/request/SKILL.md'), join(root, 'SKILL.md')) @@ -810,7 +808,7 @@ it('still tracks an ignored path when a skill maps it as a source', () => { }) it('rejects a review record that annotates nothing instead of silently recording zero outcomes', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { }) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const report = join(root, '.intent/review.json') write('.intent/review.json', JSON.stringify(createReview(root))) expect(await main(['review', root, '--record', report])).toBe(1) @@ -845,7 +843,7 @@ it('accepts a single evidence string when recording', () => { for (const item of report.items) { item.outcome = 'no-change' item.reason = 'Compared the source with the documented request behavior.' - ; (item as { evidence: unknown }).evidence = 'src/request.ts' + ;(item as { evidence: unknown }).evidence = 'src/request.ts' } expect(recordReview(root, report)).toBe(1) expect(