Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/stale-per-package-version.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Stop `intent stale` from reporting artifact library version drift for a skill whose `library_version` matches the current version of its own package. Monorepos that version packages independently no longer get this false warning, and a review signal raised by more than one artifact file now prints once in the text output.
1 change: 1 addition & 0 deletions docs/cli/intent-stale.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Reason generation:
- Report header format: `<library> (<skillVersion> → <currentVersion>) [<versionDrift> drift]`
- When no skill reasons exist: `All skills up-to-date`
- Otherwise: one warning line per stale skill or review signal (`⚠ <name>: <reason1>, <reason2>, ...`)
- Review signals that produce the same line, such as one raised by both `domain_map.yaml` and `skill_tree.yaml`, print once; JSON output keeps each signal with its `artifactPath`

## Generated review files

Expand Down
6 changes: 5 additions & 1 deletion packages/intent/src/commands/stale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export async function runStaleCommand(
for (const skill of stale) {
console.log(` ⚠ ${skill.name}: ${skill.reasons.join(', ')}`)
}
const printed = new Set<string>()
for (const signal of signals) {
const subject =
signal.packageName ??
Expand All @@ -83,7 +84,10 @@ export async function runStaleCommand(
signal.artifactPath ??
signal.subject ??
report.library
console.log(` ⚠ ${subject}: ${signal.reasons.join(', ')}`)
const line = ` ⚠ ${subject}: ${signal.reasons.join(', ')}`
if (printed.has(line)) continue
printed.add(line)
console.log(line)
}
}

Expand Down
6 changes: 5 additions & 1 deletion packages/intent/src/staleness/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,14 @@ function findMatchingSkill(
function buildArtifactSignals({
artifactRoot,
artifacts,
currentVersion,
library,
packageDir,
skillMetas,
}: {
artifactRoot: string
artifacts: IntentArtifactSet | null
currentVersion: string | null
library: string
packageDir: string
skillMetas: Array<SkillMeta>
Expand Down Expand Up @@ -355,7 +357,8 @@ function buildArtifactSignals({
if (
artifactVersion &&
matchingSkill.libraryVersion &&
artifactVersion !== matchingSkill.libraryVersion
artifactVersion !== matchingSkill.libraryVersion &&
matchingSkill.libraryVersion !== currentVersion
) {
signals.push({
type: 'artifact-library-version-drift',
Expand Down Expand Up @@ -550,6 +553,7 @@ export async function checkStaleness(
signals: buildArtifactSignals({
artifactRoot,
artifacts,
currentVersion,
library,
packageDir,
skillMetas,
Expand Down
32 changes: 32 additions & 0 deletions packages/intent/tests/stale-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,38 @@ describe('runStaleCommand', () => {
expect(output).not.toContain('All skills up-to-date')
})

it('prints a review signal raised by two artifact files once in non-json output', async () => {
const reason =
'artifact library.version (1.2.0) differs from SKILL.md library_version (1.0.0)'
await runStaleCommand(undefined, {}, () =>
Promise.resolve({
reports: [
{
library: '@tanstack/router',
currentVersion: '1.1.0',
skillVersion: '1.0.0',
versionDrift: 'minor',
skills: [],
signals: ['domain_map.yaml', 'skill_tree.yaml'].map((file) => ({
type: 'artifact-library-version-drift',
library: '@tanstack/router',
subject: 'routing',
reasons: [reason],
needsReview: true,
artifactPath: `_artifacts/${file}`,
skill: 'routing',
})),
},
],
}),
)

const lines = logSpy.mock.calls.map((call) => String(call[0]))
expect(lines.filter((line) => line.includes(reason))).toEqual([
` ⚠ routing: ${reason}`,
])
})

it('prints workflow update advisories in non-json output', async () => {
await runStaleCommand(undefined, {}, () =>
Promise.resolve({
Expand Down
94 changes: 94 additions & 0 deletions packages/intent/tests/staleness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,100 @@ skills:
])
})

it('does not flag artifact library version drift when the skill matches its independently versioned package', async () => {
const tablePackageDir = join(tmpDir, 'packages', 'table')
const utilsPackageDir = join(tmpDir, 'packages', 'utils')
mkdirSync(tablePackageDir, { recursive: true })
mkdirSync(utilsPackageDir, { recursive: true })
writeFileSync(
join(tablePackageDir, 'package.json'),
JSON.stringify({ name: '@example/table', version: '9.2.5' }),
)
writeFileSync(
join(utilsPackageDir, 'package.json'),
JSON.stringify({ name: '@example/utils', version: '9.1.2' }),
)
writeSkill(tablePackageDir, 'core', {
name: 'core',
description: 'Core',
library_version: '9.2.5',
})
writeSkill(utilsPackageDir, 'ranking', {
name: 'ranking',
description: 'Ranking',
library_version: '9.1.2',
})
const artifact = `
library:
name: '@example/table'
version: '9.2.5'
skills:
- name: 'Core'
slug: 'core'
path: 'packages/table/skills/core/SKILL.md'
package: 'packages/table'
- name: 'Ranking'
slug: 'ranking'
path: 'packages/utils/skills/ranking/SKILL.md'
package: 'packages/utils'
`
writeArtifact(tmpDir, 'skill_tree.yaml', artifact)
writeArtifact(tmpDir, 'domain_map.yaml', artifact)
mockFetchNotOk()

const tableReport = await checkStaleness(
tablePackageDir,
'@example/table',
tmpDir,
)
const utilsReport = await checkStaleness(
utilsPackageDir,
'@example/utils',
tmpDir,
)

expect(tableReport.signals).toEqual([])
expect(utilsReport.currentVersion).toBe('9.1.2')
expect(utilsReport.signals).toEqual([])
})

it('flags artifact library version drift when the skill differs from its package and the artifact', async () => {
writeFileSync(
join(tmpDir, 'package.json'),
JSON.stringify({ name: '@example/lib', version: '1.1.0' }),
)
writeSkill(tmpDir, 'core', {
name: 'core',
description: 'Core',
library_version: '1.0.0',
})
writeArtifact(
tmpDir,
'skill_tree.yaml',
`
library:
name: '@example/lib'
version: '1.2.0'
skills:
- name: 'Core'
slug: 'core'
path: 'skills/core/SKILL.md'
package: '@example/lib'
`,
)
mockFetchNotOk()

const report = await checkStaleness(tmpDir, '@example/lib')

expect(report.signals).toEqual([
expect.objectContaining({
type: 'artifact-library-version-drift',
skill: 'core',
needsReview: true,
}),
])
})

it('flags artifact parse warnings as review signals', async () => {
writeArtifact(tmpDir, 'skill_tree.yaml', 'skills:\n - name: [broken\n')
mockFetchNotOk()
Expand Down
Loading