Skip to content
5 changes: 5 additions & 0 deletions .changeset/icy-loops-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Fix maintainer review discovering skills inside hidden agent directories
6 changes: 2 additions & 4 deletions packages/intent/src/maintainer/existing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { isDefaultSkillPath, parseFrontmatter } from '../shared/utils.js'
import { stringList } from './add.js'
import {
isObject,
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions packages/intent/src/review/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { isDefaultSkillPath, parseFrontmatter } from '../shared/utils.js'

type Snapshot = Record<string, string | null>
type Outcome = 'updated' | 'no-change' | 'out-of-scope' | 'unresolved'
Expand Down Expand Up @@ -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}`]),
Expand Down
13 changes: 13 additions & 0 deletions packages/intent/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand Down
64 changes: 64 additions & 0 deletions packages/intent/tests/review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,71 @@ 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('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)

write(
'_artifacts/skill_tree.yaml',
'library: { name: library }\nskills: []\n',
)

write('src/hidden.ts', 'export const hidden = false\n')

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'))
planningRecords('_artifacts')
Expand Down
Loading