Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1bb4da6
feat: typecheck skill code blocks against the library's own source
LadyBluenotes Sep 12, 2026
c8d1a0c
fix: resolve the library entry from declared types, mapping build out…
LadyBluenotes Sep 12, 2026
cec1123
fix: resolve sibling workspace packages when checking skill examples
LadyBluenotes Sep 12, 2026
36d55f3
perf: typecheck each skill once per process and batch review summaries
LadyBluenotes Sep 12, 2026
0156db6
fix: address review findings on skill example checks
LadyBluenotes Sep 12, 2026
1a8d467
fix: accept angle-bracket link destinations in skill link checks
LadyBluenotes Sep 12, 2026
9f06be9
chore: keep skill example helpers module-private for knip
LadyBluenotes Sep 12, 2026
09beb7a
ci: apply automated fixes
autofix-ci[bot] Sep 12, 2026
05320fb
fix: key virtual example files the way TypeScript reads them and drop…
LadyBluenotes Sep 12, 2026
2667fdd
test: give every fixture commit an author so the suite passes without…
LadyBluenotes Sep 12, 2026
1c518b1
perf: group skills by library in one pass instead of re-spreading per…
LadyBluenotes Sep 12, 2026
43a1c39
fix: give each virtual example file the extension of its fence language
LadyBluenotes Sep 13, 2026
070fb8f
fix: validate independent examples against current source
LadyBluenotes Sep 13, 2026
8b68745
test: keep startup benchmark imports ordered
LadyBluenotes Sep 13, 2026
3b5e1b7
Merge branch 'audit-fix-289' into audit-combined
LadyBluenotes Sep 13, 2026
668bdab
test: initialize the prose fixture before each benchmark run
LadyBluenotes Sep 13, 2026
54b9522
Merge commit '3b5e1b7febdd3f9b4e074d595e62bb59aba404e5' into audit-fi…
LadyBluenotes Sep 13, 2026
68ed322
Merge branch 'audit-fix-288' into audit-fix-289
LadyBluenotes Sep 13, 2026
9db01da
Merge updated base for PR #289
LadyBluenotes Sep 13, 2026
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/skill-code-block-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': minor
---

`validate` checks every fenced TypeScript and JavaScript block in a `SKILL.md` against the owning package's own types when TypeScript is available in the repository. A renamed export, a removed option, or a changed shape fails validation with the skill file and line. Imports of exports marked `@deprecated` produce warnings, and relative Markdown links must resolve. Names, modules, and globals a partial snippet leaves out are not reported. Repositories without TypeScript skip the code checks with a notice. Pending review items for skills report whether their examples still compile.
38 changes: 36 additions & 2 deletions benchmarks/intent/startup.bench.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { bench, describe } from 'vitest'
import { afterAll, beforeAll, bench, describe } from 'vitest'

const cliPath = fileURLToPath(
new URL('../../packages/intent/dist/cli.mjs', import.meta.url),
Expand All @@ -11,8 +14,9 @@ const coldStartBenchOptions = {
time: 3_000,
}

function runNode(args: Array<string>): void {
function runNode(args: Array<string>, cwd?: string): void {
const result = spawnSync(process.execPath, args, {
cwd,
stdio: 'ignore',
timeout: 10_000,
})
Expand All @@ -24,6 +28,27 @@ function runNode(args: Array<string>): void {
}

describe('cold start', () => {
let root: string | undefined
function setup() {
if (root) return
root = mkdtempSync(join(tmpdir(), 'intent-prose-startup-'))
writeFileSync(
join(root, 'package.json'),
'{"name":"prose-library","version":"1.0.0"}\n',
)
mkdirSync(join(root, 'skills', 'guide'), { recursive: true })
writeFileSync(
join(root, 'skills', 'guide', 'SKILL.md'),
'---\nname: guide\ndescription: Use when reading the guide.\n---\nProse-only guidance.\n',
)
}
function teardown() {
if (root) rmSync(root, { recursive: true, force: true })
root = undefined
}
beforeAll(setup)
afterAll(teardown)

bench(
'empty node process (baseline)',
() => {
Expand All @@ -39,4 +64,13 @@ describe('cold start', () => {
},
coldStartBenchOptions,
)

bench(
'intent validate prose-only skills',
() => {
setup()
runNode([cliPath, 'validate'], root)
},
{ ...coldStartBenchOptions, setup, teardown },
)
})
12 changes: 11 additions & 1 deletion packages/intent/src/commands/maintainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '../maintainer/distribution.js'
import { runSetupGithubActions } from '../setup/index.js'
import { detectIntentCommandPackageManager } from '../shared/command-runner.js'
import { describeSkillExamples } from '../validate/blocks.js'
import { getMetaDir } from './support.js'
import {
buildMaintainerGuidanceBlock,
Expand Down Expand Up @@ -447,6 +448,12 @@ export async function runMaintainerCommand(
for (const problem of status.problems) console.log(` ${problem}`)
for (const path of status.staleFiles)
console.log(` Run intent maintainer sync: ${path}`)
const examples = describeSkillExamples(
project.root,
review.items
.filter((item) => item.kind === 'skill' && !item.problems.length)
.map((item) => item.path),
)
for (const item of review.items) {
const label =
item.kind === 'skill'
Expand All @@ -459,7 +466,10 @@ export async function runMaintainerCommand(
: item.changedFiles.length
? `changed ${item.changedFiles.join(', ')}`
: 'no recorded review'
console.log(` ${label} ${item.path}: ${detail}`)
const example = examples.get(item.path)
console.log(
` ${label} ${item.path}: ${detail}${example ? `; ${example}` : ''}`,
)
}
}
if (action === 'check') {
Expand Down
53 changes: 53 additions & 0 deletions packages/intent/src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { fail, isCliFailure } from '../shared/cli-error.js'
import { resolveProjectContext } from '../core/project-context.js'
import { findWorkspacePackages } from '../setup/workspace-patterns.js'
import { createIntentFsCache } from '../discovery/fs-cache.js'
import { checkSkillBlocks } from '../validate/blocks.js'
import { printWarnings } from './support.js'
import type { ProjectContext } from '../core/project-context.js'

Expand Down Expand Up @@ -462,6 +463,7 @@ async function runValidateCommandInternal(

const errors: Array<ValidationError> = []
const warnings: Array<string> = []
const skippedBlockChecks = new Set<string>()
const fixPlans: Array<FrontmatterFixPlan> = []
const setVersionPlans: Array<SetVersionPlan> = []
let validatedCount = 0
Expand All @@ -482,6 +484,11 @@ async function runValidateCommandInternal(
targetPath: skillsDir,
})

const checkedSkills: Array<{
file: string
content: string
library: string | undefined
}> = []
for (const filePath of skillFiles) {
const rel = relative(process.cwd(), filePath)
const content = readFileSync(filePath, 'utf8')
Expand Down Expand Up @@ -611,6 +618,12 @@ async function runValidateCommandInternal(
...collectAgentSkillSpecWarnings({ fm, rel }).map(formatWarning),
)

checkedSkills.push({
file: rel,
content,
library: readScalarField(fm, 'library'),
})

const lineCount = content.split(/\r?\n/).length
if (lineCount > 500) {
errors.push({
Expand All @@ -620,6 +633,43 @@ async function runValidateCommandInternal(
}
}

// Code blocks and links are checked against the owning package's own
// source, so a renamed export or option fails here with a skill line.
if (validateContext.packageRoot && checkedSkills.length) {
let packageName: string | undefined
try {
packageName = JSON.parse(
readFileSync(validateContext.targetPackageJsonPath!, 'utf8'),
).name
} catch {
packageName = undefined
}
const byLibrary: Record<string, typeof checkedSkills> =
Object.create(null)
for (const skill of checkedSkills) {
const library = skill.library ?? packageName
if (library) (byLibrary[library] ??= []).push(skill)
}
for (const [library, skills] of Object.entries(byLibrary)) {
const result = checkSkillBlocks({
root: process.cwd(),
packageDir: validateContext.packageRoot,
library,
skills,
})
if (result.skipped) skippedBlockChecks.add(result.skipped)
for (const finding of result.findings) {
if (finding.severity === 'error')
errors.push({
file: `${finding.file}:${finding.line}`,
message: finding.message,
})
else
warnings.push(`${finding.file}:${finding.line}: ${finding.message}`)
}
}
}

// In monorepos, _artifacts lives at the workspace root, not under each package's skills/ dir.
const artifactsDir = join(skillsDir, '_artifacts')
if (!validateContext.isMonorepo && existsSync(artifactsDir)) {
Expand Down Expand Up @@ -668,6 +718,9 @@ async function runValidateCommandInternal(
)
}

for (const reason of skippedBlockChecks)
warnings.push(`Skill code blocks were not typechecked: ${reason}`)

if (options.check) {
for (const plan of fixPlans) {
errors.push({
Expand Down
Loading
Loading