diff --git a/.changeset/afraid-trees-type.md b/.changeset/afraid-trees-type.md new file mode 100644 index 0000000..e1447a1 --- /dev/null +++ b/.changeset/afraid-trees-type.md @@ -0,0 +1,9 @@ +--- +'@tanstack/intent': patch +--- + +Harden Intent skill checks for nested workspaces, generated GitHub workflows, Yarn PnP discovery, and Agent Skills spec compatibility. + +`intent validate` now discovers package-local `skills/` directories from workspace configuration, including nested layouts. The generated `check-skills.yml` workflow now delegates PR validation and release/manual review PR generation to the CLI. `intent stale --github-review` writes review files with the reasons each skill or package was flagged. + +Intent package scanning also supports Yarn PnP projects through Yarn's PnP API, and validation now emits warning-only Agent Skills spec compatibility notices without failing existing Intent skills. diff --git a/docs/cli/intent-setup.md b/docs/cli/intent-setup.md index 1c85bf6..72edc87 100644 --- a/docs/cli/intent-setup.md +++ b/docs/cli/intent-setup.md @@ -24,7 +24,7 @@ npx @tanstack/intent@latest setup - Ensures `files` includes required publish entries - Preserves existing indentation - `setup` - - Copies templates from `@tanstack/intent/meta/templates/workflows` to `.github/workflows` + - Copies the `check-skills.yml` workflow template from `@tanstack/intent/meta/templates/workflows` to `.github/workflows` - Applies variable substitution (`PACKAGE_NAME`, `PACKAGE_LABEL`, `PAYLOAD_PACKAGE`, `REPO`, `DOCS_PATH`, `SRC_PATH`, `WATCH_PATHS`) - Detects the workspace root in monorepos and writes repo-level workflows there - Skips files that already exist at destination @@ -41,9 +41,12 @@ npx @tanstack/intent@latest setup - Missing or invalid `package.json` when running `edit-package-json` - Missing template source when running `setup` -## Notes - +## Notes + - `setup` skips existing files +- `check-skills.yml` validates skills on PRs and opens review PRs from release/manual runs +- To adopt updated workflow templates, delete or move the old generated workflow files first, then rerun `setup` +- If your repo has an older generated `validate-skills.yml`, remove it after adopting the current `check-skills.yml`; PR validation now lives in `check-skills.yml` - In monorepos, run `setup` from either the repo root or a package directory; Intent writes workflows to the workspace root ## Related diff --git a/docs/getting-started/quick-start-maintainers.md b/docs/getting-started/quick-start-maintainers.md index a3456e2..f5879f8 100644 --- a/docs/getting-started/quick-start-maintainers.md +++ b/docs/getting-started/quick-start-maintainers.md @@ -101,7 +101,7 @@ Run these commands to prepare your package for skill publishing: # Update package.json with required fields npx @tanstack/intent@latest edit-package-json -# Copy CI workflow templates (validate + stale checks) +# Copy the CI workflow template npx @tanstack/intent@latest setup ``` @@ -112,9 +112,13 @@ npx @tanstack/intent@latest setup - `files` array entries for `skills/` - For single packages: also adds `!skills/_artifacts` to exclude artifacts from npm - For monorepos: skips the artifacts exclusion (artifacts live at repo root) -- `setup` copies workflow templates to `.github/workflows/` for automated validation and staleness checking - -### 5. Ship skills with your package +- `setup` copies `check-skills.yml` to `.github/workflows/` for automated validation and staleness checking + +`setup` does not overwrite existing workflow files. To pick up newer generated workflows, delete or move the old generated files in `.github/workflows/`, then rerun `npx @tanstack/intent@latest setup`. + +If your repo already has an older generated `validate-skills.yml`, remove it after adopting the current `check-skills.yml`; PR validation now runs from `check-skills.yml`. + +### 5. Ship skills with your package Skills ship inside your npm package. When you publish: @@ -133,18 +137,16 @@ Consumers who install your library automatically get the skills. They discover l ## Ongoing Maintenance (Manual or Agent-Assisted) -### 6. Set up CI workflows - -After running `setup`, you'll have two workflows in `.github/workflows/`: - -**validate-skills.yml** (runs on PRs touching `skills/`) -- Validates SKILL.md frontmatter and structure -- Ensures files stay under 500 lines -- Runs automatically on every pull request that modifies skills - -**check-skills.yml** (runs on release or manual trigger) +### 6. Set up the CI workflow + +After running `setup`, you'll have `check-skills.yml` in `.github/workflows/`: + +**check-skills.yml** (runs on PRs touching skills/artifacts, release, or manual trigger) +- Validates SKILL.md frontmatter and structure +- Ensures files stay under 500 lines - Automatically detects stale skills and coverage gaps after you publish a new release - Opens one grouped review PR with an agent-friendly prompt +- Includes the reason each skill or package was flagged - Requires you to copy the prompt into Claude Code, Cursor, or your agent to update skills ### 7. Update stale skills diff --git a/packages/intent/meta/templates/workflows/check-skills.yml b/packages/intent/meta/templates/workflows/check-skills.yml index 55a9516..43b09ad 100644 --- a/packages/intent/meta/templates/workflows/check-skills.yml +++ b/packages/intent/meta/templates/workflows/check-skills.yml @@ -1,11 +1,13 @@ # check-skills.yml — Drop this into your library repo's .github/workflows/ # -# Checks intent skills after a release and opens or updates one review PR when -# existing skills, artifact coverage, or workspace package coverage need review. +# Validates intent skills on PRs. After a release or manual run, opens or +# updates one review PR when existing skills, artifact coverage, or workspace +# package coverage need review. # -# Triggers: new release published, or manual workflow_dispatch. +# Triggers: pull requests touching skills/artifacts, new release published, or +# manual workflow_dispatch. # -# intent-workflow-version: 2 +# intent-workflow-version: 3 # # Template variables (replaced by `intent setup`): # {{PACKAGE_LABEL}} — e.g. @tanstack/query or my-workspace workspace @@ -13,6 +15,12 @@ name: Check Skills on: + pull_request: + paths: + - 'skills/**' + - '**/skills/**' + - '_artifacts/**' + - '**/_artifacts/**' release: types: [published] workflow_dispatch: {} @@ -22,8 +30,28 @@ permissions: pull-requests: write jobs: - check: + validate: + name: Validate intent skills + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install intent + run: npm install -g @tanstack/intent + + - name: Validate skills + run: intent validate --github-summary + + review: name: Check intent skill coverage + if: github.event_name != 'pull_request' runs-on: ubuntu-latest steps: - name: Checkout @@ -42,181 +70,7 @@ jobs: - name: Check skills id: stale run: | - set +e - intent stale --json > stale.json - STATUS=$? - set -e - - cat stale.json - - if [ "$STATUS" -ne 0 ]; then - echo "has_review=true" >> "$GITHUB_OUTPUT" - echo "check_failed=true" >> "$GITHUB_OUTPUT" - cat > review-items.json <<'JSON' - [ - { - "type": "stale-check-failed", - "library": "{{PACKAGE_LABEL}}", - "subject": "intent stale --json", - "reasons": ["The stale check command failed. Review the workflow logs before updating skills."] - } - ] - JSON - else - node <<'NODE' - const fs = require('fs') - const reports = JSON.parse(fs.readFileSync('stale.json', 'utf8')) - const items = [] - - for (const report of reports) { - for (const skill of report.skills ?? []) { - if (!skill?.needsReview) continue - items.push({ - type: 'stale-skill', - library: report.library, - subject: skill.name, - reasons: skill.reasons ?? [], - }) - } - - for (const signal of report.signals ?? []) { - if (signal?.needsReview === false) continue - items.push({ - type: signal?.type ?? 'review-signal', - library: signal?.library ?? report.library, - subject: - signal?.packageName ?? - signal?.packageRoot ?? - signal?.skill ?? - signal?.artifactPath ?? - signal?.subject ?? - report.library, - reasons: signal?.reasons ?? [], - artifactPath: signal?.artifactPath, - packageName: signal?.packageName, - packageRoot: signal?.packageRoot, - skill: signal?.skill, - }) - } - } - - fs.writeFileSync('review-items.json', JSON.stringify(items, null, 2) + '\n') - fs.appendFileSync( - process.env.GITHUB_OUTPUT, - `has_review=${items.length > 0 ? 'true' : 'false'}\n`, - ) - NODE - fi - - { - echo "review_items<> "$GITHUB_OUTPUT" - - - name: Write clean summary - if: steps.stale.outputs.has_review == 'false' - run: | - { - echo "### Intent skill review" - echo "" - echo "No stale skills or coverage gaps found." - } >> "$GITHUB_STEP_SUMMARY" - - - name: Build review PR body - if: steps.stale.outputs.has_review == 'true' - run: | - node <<'NODE' - const fs = require('fs') - const items = JSON.parse(fs.readFileSync('review-items.json', 'utf8')) - const grouped = new Map() - - for (const item of items) { - grouped.set(item.type, (grouped.get(item.type) ?? 0) + 1) - } - - const signalRows = [...grouped.entries()] - .sort(([a], [b]) => a.localeCompare(b)) - .map(([type, count]) => `| \`${type}\` | ${count} |`) - - const itemRows = items.map((item) => { - const subject = item.subject ? `\`${item.subject}\`` : '-' - const reasons = item.reasons?.length ? item.reasons.join('; ') : '-' - return `| \`${item.type}\` | ${subject} | \`${item.library}\` | ${reasons} |` - }) - - const prompt = [ - 'You are helping maintain Intent skills for this repository.', - '', - 'Goal:', - 'Resolve the Intent skill review signals below while preserving the existing scope, taxonomy, and maintainer-reviewed artifacts.', - '', - 'Review signals:', - JSON.stringify(items, null, 2), - '', - 'Required workflow:', - '1. Read the existing `_artifacts/*domain_map.yaml`, `_artifacts/*skill_tree.yaml`, and generated `skills/**/SKILL.md` files.', - '2. Read each flagged package package.json, public exports, README/docs if present, and source entry points.', - '3. Compare flagged packages against the existing domains, skills, tasks, packages, covers, sources, tensions, and cross-references in the artifacts.', - '4. For each signal, decide whether it means existing skill coverage, a missing generated skill, a new skill candidate, out-of-scope coverage, or deferred work.', - '', - 'Maintainer questions:', - 'Before editing skills or artifacts, ask the maintainer:', - '1. For each flagged package, is this package user-facing enough to need agent guidance?', - '2. If yes, should it extend an existing skill or become a new skill?', - '3. If it extends an existing skill, which current skill should own it?', - '4. If it is out of scope, what short reason should be recorded in artifact coverage ignores?', - '5. Are any of these packages experimental or unstable enough to exclude for now?', - '', - 'Decision rules:', - '- Do not auto-generate skills.', - '- Do not create broad new skill areas without maintainer confirmation.', - '- Prefer adding package coverage to an existing skill when the package is an implementation variant of an existing domain.', - '- Create a new skill only when the package introduces a distinct developer task or failure mode.', - '- Preserve current naming, path, and package layout conventions.', - '- Keep generated skills under the package-local `skills/` directory.', - '- Keep repo-root `_artifacts` as the reviewed plan.', - '', - 'If maintainer confirms updates:', - '1. Update the relevant `_artifacts/*domain_map.yaml` or `_artifacts/*skill_tree.yaml`.', - '2. Update or create `SKILL.md` files only for confirmed coverage changes.', - '3. Keep `sources` aligned between artifact skill entries and SKILL frontmatter.', - '4. Bump `library_version` only for skills whose covered source package version changed.', - '5. Run `npx @tanstack/intent@latest validate` on touched skill directories.', - '6. Summarize every package as one of: existing-skill coverage, new skill, ignored, or deferred.', - ].join('\n') - - const body = [ - '## Intent Skill Review Needed', - '', - 'Intent found skills, artifact coverage, or workspace package coverage that need maintainer review.', - '', - '### Summary', - '', - '| Signal | Count |', - '| --- | ---: |', - ...signalRows, - '', - '### Review Items', - '', - '| Signal | Subject | Library | Reason |', - '| --- | --- | --- | --- |', - ...itemRows, - '', - '### Agent Prompt', - '', - 'Paste this into your coding agent:', - '', - '```text', - prompt, - '```', - '', - 'This PR is a review reminder only. It does not update skills automatically.', - ].join('\n') - - fs.writeFileSync('pr-body.md', body + '\n') - fs.writeFileSync(process.env.GITHUB_STEP_SUMMARY, body + '\n') - NODE + intent stale --github-review --package-label "{{PACKAGE_LABEL}}" - name: Open or update review PR if: steps.stale.outputs.has_review == 'true' diff --git a/packages/intent/meta/templates/workflows/validate-skills.yml b/packages/intent/meta/templates/workflows/validate-skills.yml deleted file mode 100644 index 8f39716..0000000 --- a/packages/intent/meta/templates/workflows/validate-skills.yml +++ /dev/null @@ -1,52 +0,0 @@ -# validate-skills.yml — Drop this into your library repo's .github/workflows/ -# -# Validates skill files on PRs that touch the skills/ directory. -# Ensures frontmatter is correct, names match paths, and files stay under -# the 500-line limit. - -name: Validate Skills - -on: - pull_request: - paths: - - 'skills/**' - - '**/skills/**' - -jobs: - validate: - name: Validate skill files - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install intent CLI - run: npm install -g @tanstack/intent - - - name: Find and validate skills - run: | - # Find all directories containing SKILL.md files - SKILLS_DIR="" - if [ -d "skills" ]; then - SKILLS_DIR="skills" - elif [ -d "packages" ]; then - # Monorepo — find skills/ under packages - for dir in packages/*/skills; do - if [ -d "$dir" ]; then - echo "Validating $dir..." - intent validate "$dir" - fi - done - exit 0 - fi - - if [ -n "$SKILLS_DIR" ]; then - intent validate "$SKILLS_DIR" - else - echo "No skills/ directory found — skipping validation." - fi diff --git a/packages/intent/src/cli-support.ts b/packages/intent/src/cli-support.ts index 559a3fa..7b5e962 100644 --- a/packages/intent/src/cli-support.ts +++ b/packages/intent/src/cli-support.ts @@ -17,7 +17,7 @@ export interface StaleTargetResult { workflowAdvisories: Array } -export const INTENT_CHECK_SKILLS_WORKFLOW_VERSION = 2 +export const INTENT_CHECK_SKILLS_WORKFLOW_VERSION = 3 export function getMetaDir(): string { const thisDir = dirname(fileURLToPath(import.meta.url)) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 2ea6710..98f8ada 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -22,6 +22,8 @@ import type { CAC } from 'cac' import type { InstallCommandOptions } from './commands/install.js' import type { ListCommandOptions } from './commands/list.js' import type { LoadCommandOptions } from './commands/load.js' +import type { StaleCommandOptions } from './commands/stale.js' +import type { ValidateCommandOptions } from './commands/validate.js' function createCli(): CAC { const cli = cac('intent') @@ -67,12 +69,15 @@ function createCli(): CAC { cli .command('validate [dir]', 'Validate skill files') - .usage('validate [dir]') + .usage('validate [dir] [--github-summary]') + .option('--github-summary', 'Write a GitHub Actions step summary') .example('validate') .example('validate packages/query/skills') - .action(async (dir?: string) => { - await runValidateCommand(dir) - }) + .action( + async (dir: string | undefined, options: ValidateCommandOptions) => { + await runValidateCommand(dir, options) + }, + ) cli .command( @@ -111,13 +116,15 @@ function createCli(): CAC { 'stale [dir]', 'Check skills for staleness in the current package or workspace', ) - .usage('stale [dir] [--json]') + .usage('stale [dir] [--json] [--github-review]') .option('--json', 'Output JSON') + .option('--github-review', 'Write GitHub Actions review PR files') + .option('--package-label