From 54868335405b8e25f698c363fa47dceb919a97c1 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:06:36 -0700 Subject: [PATCH 01/19] fix: scaffold runs meta skills sequentially instead of all-at-once The scaffold prompt now instructs the AI to load each meta skill's SKILL.md one at a time with explicit review stops between steps, rather than summarizing all three phases in a single prompt dump. This prevents quality degradation from the AI racing ahead. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/cli.ts | 117 ++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 67d69d3..e273b11 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -351,58 +351,71 @@ function cmdValidate(args: string[]): void { } function cmdScaffold(): void { - const prompt = `You are an AI assistant helping a library maintainer scaffold Intent skills. -You MUST use the Intent meta skills in this exact order and follow their output requirements. - -Before you start, ask the maintainer: -1. Skills root path (default: skills/). If custom, replace "skills/" in all paths below. -2. Is this a monorepo? If yes, you need the following layout: - - Domain map artifacts live at the REPO ROOT: _artifacts/domain_map.yaml, _artifacts/skill_spec.md, _artifacts/skill_tree.yaml - - Skills live INSIDE EACH PACKAGE: packages//skills///SKILL.md - - Each publishable package needs: - a. @tanstack/intent as a devDependency - b. A bin entry: "bin": { "intent": "./bin/intent.js" } - c. The shim file at bin/intent.js (run npx @tanstack/intent setup --shim in each package) - d. "skills" and "bin" in the package.json "files" array, with "!skills/_artifacts" to exclude artifacts - - Ask the maintainer which packages should get skills (usually client SDKs and primary framework adapters) - -1) Meta skill: domain-discovery - - Input: library name, repo URL, docs URL(s), scope constraints, target audience. - - Output files: - - Single-repo: skills/_artifacts/domain_map.yaml, skills/_artifacts/skill_spec.md - - Monorepo: _artifacts/domain_map.yaml, _artifacts/skill_spec.md (at repo root) - - Domain discovery covers the WHOLE library. One domain map for the entire monorepo. - - These artifacts are maintainer-owned and should be committed to the repo. - -2) Meta skill: tree-generator - - Input: domain map + skill spec artifacts - - Output file: _artifacts/skill_tree.yaml (same location as domain map) - - The skill tree must specify which PACKAGE each skill belongs to. - - For monorepos, the tree maps skills to packages: each skill entry should include - a "package" field (e.g. packages/client, packages/react-client). - -3) Meta skill: generate-skill - - Input: skill tree - - Output files: - - Single-repo: skills///SKILL.md - - Monorepo: packages//skills///SKILL.md - - Skills are written into the package they describe, not a shared root. - -Guidance for the maintainer: -- If any input is missing, ask for it. -- After each step, clearly tell the maintainer what files to create and where to save them. -- Do not skip steps. -- Use the library's actual terminology from docs and source. - -At the end, produce a single Markdown feedback doc with three sections (Domain Discovery, Tree Generator, Generate Skill). -Ask if the maintainer wants to edit it, then submit it via: npx @tanstack/intent feedback --meta --submit --file - -Finish with a short checklist: -- Run npx @tanstack/intent validate in each package directory (or for single-repo: at the root) -- Commit skills/ and artifacts -- Exclude artifacts from package publishing (add "!skills/_artifacts" to the "files" array in each package.json) -- For monorepos: ensure each package has @tanstack/intent as devDependency, bin entry, and shim -- Add README snippet: If you use an AI agent, run npx @tanstack/intent init + const metaDir = getMetaDir() + const metaSkillPath = (name: string) => + join(metaDir, name, 'SKILL.md') + + const prompt = `You are helping a library maintainer scaffold Intent skills. + +Run the three meta skills below **one at a time, in order**. For each step: +1. Load the SKILL.md file specified +2. Follow its instructions completely +3. Present outputs to the maintainer for review +4. Do NOT proceed to the next step until the maintainer confirms + +## Before you start + +Ask the maintainer: +1. Library name, repo URL, and docs URL(s) +2. Skills root path (default: skills/) +3. Is this a monorepo? If yes: + - Domain map artifacts go at the REPO ROOT: _artifacts/ + - Skills go INSIDE EACH PACKAGE: packages//skills/ + - Ask which packages should get skills (usually client SDKs and primary framework adapters) + +--- + +## Step 1 — Domain Discovery + +Load and follow: ${metaSkillPath('domain-discovery')} + +This produces: domain_map.yaml and skill_spec.md in the artifacts directory. +Domain discovery covers the WHOLE library (one domain map even for monorepos). + +**STOP. Review outputs with the maintainer before continuing.** + +--- + +## Step 2 — Tree Generator + +Load and follow: ${metaSkillPath('tree-generator')} + +This produces: skill_tree.yaml in the artifacts directory. +For monorepos, each skill entry should include a \`package\` field. + +**STOP. Review outputs with the maintainer before continuing.** + +--- + +## Step 3 — Generate Skills + +Load and follow: ${metaSkillPath('generate-skill')} + +This produces: individual SKILL.md files. +- Single-repo: skills///SKILL.md +- Monorepo: packages//skills///SKILL.md + +--- + +## After all skills are generated + +1. Run \`npx @tanstack/intent validate\` in each package directory +2. Commit skills/ and artifacts +3. For each publishable package, run: \`npx @tanstack/intent setup --shim\` +4. Ensure each package has \`@tanstack/intent\` as a devDependency +5. Add \`"skills"\`, \`"bin"\` to the \`"files"\` array in each package.json +6. Add \`"!skills/_artifacts"\` to exclude artifacts from publishing +7. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent init\`" ` console.log(prompt) From 5a116fba0a5a083311bff376f2753fb4d97fac3e Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:07:12 -0700 Subject: [PATCH 02/19] fix: validator checks packaging setup and description char limit Adds packaging warnings when package.json exists: checks for @tanstack/intent in devDependencies, bin entry, shim file, and "skills"/"bin"/"!skills/_artifacts" in files array. Also validates SKILL.md description doesn't exceed 1024 characters. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/cli.ts | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index e273b11..8a5c0b3 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -281,6 +281,14 @@ function cmdValidate(args: string[]): void { } } + // Description character limit + if (typeof fm.description === 'string' && fm.description.length > 1024) { + errors.push({ + file: rel, + message: `Description exceeds 1024 character limit (${fm.description.length} chars)`, + }) + } + // Framework skills must have requires if (fm.type === 'framework' && !Array.isArray(fm.requires)) { errors.push({ @@ -339,15 +347,70 @@ function cmdValidate(args: string[]): void { } } + // Packaging checks — run when package.json exists at cwd + const pkgJsonPath = join(process.cwd(), 'package.json') + const warnings: string[] = [] + if (existsSync(pkgJsonPath)) { + let pkgJson: Record = {} + try { + pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) + } catch { + // skip packaging checks if we can't read package.json + } + + if (Object.keys(pkgJson).length > 0) { + // Check @tanstack/intent in devDependencies + const devDeps = pkgJson.devDependencies as Record | undefined + if (!devDeps?.['@tanstack/intent']) { + warnings.push('@tanstack/intent is not in devDependencies') + } + + // Check bin entry + const bin = pkgJson.bin as Record | undefined + if (!bin?.intent) { + warnings.push('Missing "bin": { "intent": ... } entry in package.json') + } + + // Check shim file exists + const shimJs = join(process.cwd(), 'bin', 'intent.js') + const shimMjs = join(process.cwd(), 'bin', 'intent.mjs') + if (!existsSync(shimJs) && !existsSync(shimMjs)) { + warnings.push('No bin/intent.js or bin/intent.mjs shim found (run: npx @tanstack/intent setup --shim)') + } + + // Check files array + const files = pkgJson.files as string[] | undefined + if (Array.isArray(files)) { + if (!files.includes('skills')) { + warnings.push('"skills" is not in the "files" array — skills won\'t be published') + } + if (!files.includes('bin')) { + warnings.push('"bin" is not in the "files" array — shim won\'t be published') + } + if (!files.includes('!skills/_artifacts')) { + warnings.push('"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily') + } + } + } + } + if (errors.length > 0) { console.error(`\n❌ Validation failed with ${errors.length} error(s):\n`) for (const { file, message } of errors) { console.error(` ${file}: ${message}`) } + if (warnings.length > 0) { + console.error(`\n⚠ Packaging warnings:`) + for (const w of warnings) console.error(` ${w}`) + } process.exit(1) } console.log(`✅ Validated ${skillFiles.length} skill files — all passed`) + if (warnings.length > 0) { + console.log(`\n⚠ Packaging warnings:`) + for (const w of warnings) console.log(` ${w}`) + } } function cmdScaffold(): void { From bdb90b202df2a2e8d1b80b2f7ab52b0611b0bdde Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:07:41 -0700 Subject: [PATCH 03/19] fix: show SKILL.md file paths in list output Both maintainer and consumer list commands now display the file path for each skill, making the output actionable for AI coding agents that need to load the skill file. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/cli.ts | 4 ++++ packages/intent/src/intent-library.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 8a5c0b3..dfb06fb 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -45,6 +45,7 @@ interface SkillDisplay { name: string description: string type?: string + path?: string } function printSkillTree( @@ -96,6 +97,9 @@ function printSkillLine( ? (skill.type ? `[${skill.type}]` : '').padEnd(14) : '' console.log(`${nameStr}${padding}${typeCol}${skill.description}`) + if (skill.path) { + console.log(`${' '.repeat(indent + 2)}${skill.path}`) + } } function computeSkillNameWidth(allPackageSkills: SkillDisplay[][]): number { diff --git a/packages/intent/src/intent-library.ts b/packages/intent/src/intent-library.ts index 3d83b76..bbfd123 100644 --- a/packages/intent/src/intent-library.ts +++ b/packages/intent/src/intent-library.ts @@ -29,6 +29,7 @@ interface SkillDisplay { name: string description: string type?: string + path?: string } function printSkillTree( @@ -80,6 +81,9 @@ function printSkillLine( ? (skill.type ? `[${skill.type}]` : '').padEnd(14) : '' console.log(`${nameStr}${padding}${typeCol}${skill.description}`) + if (skill.path) { + console.log(`${' '.repeat(indent + 2)}${skill.path}`) + } } function computeSkillNameWidth(allPackageSkills: SkillDisplay[][]): number { From ab8eb4f76df09f72b81c8ace61c9f1589f56b88a Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:08:05 -0700 Subject: [PATCH 04/19] fix: scaffold gathers context autonomously instead of asking maintainer The agent should read package.json and detect monorepo structure itself rather than asking the maintainer for information that's trivially discoverable. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/cli.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index dfb06fb..37d99f0 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -432,13 +432,15 @@ Run the three meta skills below **one at a time, in order**. For each step: ## Before you start -Ask the maintainer: -1. Library name, repo URL, and docs URL(s) -2. Skills root path (default: skills/) -3. Is this a monorepo? If yes: +Gather this context yourself (do not ask the maintainer — agents should never +ask for information they can discover): +1. Read package.json for library name, repository URL, and homepage/docs URL +2. Detect if this is a monorepo (look for workspaces field, packages/ directory, lerna.json) +3. Use skills/ as the default skills root +4. For monorepos: - Domain map artifacts go at the REPO ROOT: _artifacts/ - Skills go INSIDE EACH PACKAGE: packages//skills/ - - Ask which packages should get skills (usually client SDKs and primary framework adapters) + - Identify which packages are client-facing (usually client SDKs and primary framework adapters) --- From ef287e917e05eb4e7c1f6d9e12897b6a5425bda2 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:08:55 -0700 Subject: [PATCH 05/19] fix: feedback logs errors on gh failure and accepts Markdown files When gh issue submission fails, the error message is now logged instead of silently falling through. Also adds Markdown file support: .md files are submitted directly as issue bodies without requiring JSON format. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/feedback.ts | 75 +++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/packages/intent/src/feedback.ts b/packages/intent/src/feedback.ts index 0808b11..a0db6f2 100644 --- a/packages/intent/src/feedback.ts +++ b/packages/intent/src/feedback.ts @@ -307,8 +307,10 @@ export function submitFeedback( { input: md, stdio: ['pipe', 'pipe', 'pipe'] }, ) return { method: 'gh', detail: `Submitted issue to ${repo}` } - } catch { - // Fall through to file + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error(`GitHub submission failed: ${msg}`) + console.error('Falling back to file output.') } } @@ -343,8 +345,10 @@ export function submitMetaFeedback( method: 'gh', detail: `Submitted issue to ${META_FEEDBACK_REPO}`, } - } catch { - // Fall through to file + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error(`GitHub submission failed: ${msg}`) + console.error('Falling back to file output.') } } @@ -380,14 +384,6 @@ export function runFeedback(args: string[]): void { process.exit(1) } - let raw: unknown - try { - raw = JSON.parse(readFileSync(filePath, 'utf8')) - } catch { - console.error('Invalid JSON in feedback file') - process.exit(1) - } - const ghAvailable = hasGhCli() const frequency = resolveFrequency(process.cwd()) @@ -397,6 +393,61 @@ export function runFeedback(args: string[]): void { } const dateSuffix = new Date().toISOString().slice(0, 10) + const isMarkdown = filePath.endsWith('.md') + + // Markdown mode: submit the file content directly as an issue body + if (isMarkdown) { + const md = readFileSync(filePath, 'utf8') + + if (containsSecrets(md)) { + console.error('Feedback file appears to contain secrets or tokens — submission rejected') + process.exit(1) + } + + // Extract title from first heading, or use a default + const titleMatch = md.match(/^#\s+(.+)/m) + const title = titleMatch?.[1] ?? (isMeta ? 'Meta-Skill Feedback' : 'Skill Feedback') + const repo = isMeta ? META_FEEDBACK_REPO : undefined + + if (!repo && !isMeta) { + console.error('Markdown feedback for standard skills requires --meta flag or JSON format with a "package" field') + process.exit(1) + } + + if (ghAvailable && repo) { + try { + const labelArg = isMeta && titleMatch?.[1] + ? (() => { + const skillMatch = titleMatch[1].match(/:\s*(\S+)/) + return skillMatch ? ` --label "feedback:${skillMatch[1]}"` : '' + })() + : '' + execSync( + `gh issue create --repo ${repo} --title "${title.replace(/"/g, '\\"')}"${labelArg} --body -`, + { input: md, stdio: ['pipe', 'pipe', 'pipe'] }, + ) + console.log(`✓ Submitted issue to ${repo}`) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error(`GitHub submission failed: ${msg}`) + console.log('--- Feedback markdown (copy/paste to issue) ---') + console.log(md) + } + } else { + console.log('--- Feedback markdown (copy/paste to issue) ---') + console.log(md) + } + return + } + + // JSON mode + let raw: unknown + try { + raw = JSON.parse(readFileSync(filePath, 'utf8')) + } catch { + console.error('Invalid JSON in feedback file') + process.exit(1) + } if (isMeta) { const validation = validateMetaPayload(raw) From 6ad09537a45934d3d9594e6a3723d47723858696 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:09:22 -0700 Subject: [PATCH 06/19] fix: shim uses .mjs by default, .js when package has type:module The generated bin/intent shim now uses .mjs extension to avoid Node.js ESM warnings in packages without "type": "module". When the package does declare "type": "module", it uses .js instead. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/setup.ts | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index f7d4e55..8abd319 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -112,24 +112,39 @@ function copyTemplates( // Shim generation // --------------------------------------------------------------------------- -const SHIM_CONTENT = `#!/usr/bin/env node +function getShimContent(ext: string): string { + return `#!/usr/bin/env node // Auto-generated by @tanstack/intent setup // Exposes the intent end-user CLI for consumers of this library. // Commit this file, then add to your package.json: -// "bin": { "intent": "./bin/intent.js" } +// "bin": { "intent": "./bin/intent.${ext}" } await import('@tanstack/intent/intent-library') ` +} function generateShim(root: string, result: SetupResult): void { - const shimPath = join(root, 'bin', 'intent.js') + // Check if either extension already exists + const shimJs = join(root, 'bin', 'intent.js') + const shimMjs = join(root, 'bin', 'intent.mjs') - if (existsSync(shimPath)) { - result.skipped.push(shimPath) + if (existsSync(shimJs) || existsSync(shimMjs)) { + result.skipped.push(existsSync(shimJs) ? shimJs : shimMjs) return } + // Use .js if package has "type": "module", otherwise .mjs + let ext = 'mjs' + const pkgPath = join(root, 'package.json') + try { + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) + if (pkg.type === 'module') ext = 'js' + } catch { + // default to .mjs + } + + const shimPath = join(root, 'bin', `intent.${ext}`) mkdirSync(join(root, 'bin'), { recursive: true }) - writeFileSync(shimPath, SHIM_CONTENT) + writeFileSync(shimPath, getShimContent(ext)) result.shim = shimPath } @@ -173,10 +188,11 @@ export function runSetup( for (const f of result.skipped) console.log(` Already exists: ${f}`) if (result.shim) { + const shimRelative = result.shim.replace(root + '/', './') console.log(`✓ Generated intent shim: ${result.shim}`) console.log(`\n Add to your package.json:`) - console.log(` "bin": { "intent": "./bin/intent.js" }`) - console.log(`\n Add bin/intent.js to your package.json "files" array.`) + console.log(` "bin": { "intent": "${shimRelative}" }`) + console.log(`\n Add "bin" to your package.json "files" array.`) } if ( From dc3393bd949756622edf583957ae806841a9ecfb Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:10:43 -0700 Subject: [PATCH 07/19] feat: add cross_references section to domain map schema Domain discovery now identifies cross-skill "See also" pointers beyond tensions and shared failure modes. The tree generator consumes these to add cross-reference lines in generated skills, forming a semi-lattice of connected knowledge. Co-Authored-By: Claude Opus 4.6 --- .../intent/meta/domain-discovery/SKILL.md | 41 +++++++++++++++++-- packages/intent/meta/tree-generator/SKILL.md | 8 ++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/intent/meta/domain-discovery/SKILL.md b/packages/intent/meta/domain-discovery/SKILL.md index d6b85a9..5d04d10 100644 --- a/packages/intent/meta/domain-discovery/SKILL.md +++ b/packages/intent/meta/domain-discovery/SKILL.md @@ -337,7 +337,28 @@ only considers one side. Target 2–4 tensions. If you find none, the skills may be too isolated — revisit whether you're missing cross-connections. -### 3f — Identify gaps +### 3f — Map cross-references + +Beyond tensions (conflicts) and shared failure modes, identify skills +that illuminate each other without conflicting. A cross-reference means: +"an agent loading skill A would produce better code if it knew about +skill B." These become "See also" pointers in the generated SKILL.md +files. + +For each pair, note: +- Which skill references which (can be bidirectional) +- Why awareness of the other skill improves output + +Examples: +- A quickstart skill references the security checklist ("after setup, audit") +- A state management skill references an SSR skill ("state hydration + requires understanding SSR lifecycle") +- A data writing skill references a data reading skill ("writes affect + how queries invalidate") + +Output these in the `cross_references` section of domain_map.yaml. + +### 3g — Identify gaps For each skill, explicitly list what you could NOT determine from docs and source alone. These become interview questions in Phase 4. @@ -351,7 +372,7 @@ Common gaps: - "GitHub issues show confusion about X but docs don't address it" - "I found two patterns for doing X — unclear which is current/preferred" -### 3g — Discover composition targets +### 3h — Discover composition targets Scan `package.json` for peer dependencies, optional dependencies, and `peerDependenciesMeta`. Scan example directories and integration tests @@ -363,7 +384,7 @@ for import patterns. For each frequently co-used library, log: These become targeted composition questions in Phase 4e. -### 3h — Produce the draft +### 3i — Produce the draft Write the full `domain_map.yaml` (format in Output Artifacts below) with a `status: draft` field. Flag every gap in the `gaps` section. @@ -472,7 +493,7 @@ These surface knowledge that doesn't appear in any docs: ### 4e — Composition questions (if library interacts with others) -Use what you discovered in Phase 3g. For each integration target +Use what you discovered in Phase 3h. For each integration target identified from peer dependencies and example code, ask targeted questions: @@ -573,6 +594,11 @@ tensions: description: '[what conflicts — one sentence]' implication: '[what an agent gets wrong when it only considers one side]' +cross_references: + - from: '[skill-slug]' + to: '[skill-slug]' + reason: '[why loading one skill benefits from awareness of the other]' + gaps: - skill: '[skill slug]' question: '[what still needs input]' @@ -618,6 +644,12 @@ not promotional.] | -------------- | ------------------- | ----------------------- | | [short phrase] | [slug-a] ↔ [slug-b] | [what agents get wrong] | +## Cross-References + +| From | To | Reason | +| ------ | ------ | ---------------------------------------------- | +| [slug] | [slug] | [why awareness of one improves the other] | + ## Subsystems & Reference Candidates | Skill | Subsystems | Reference candidates | @@ -672,6 +704,7 @@ not promotional.] | Subsystems flagged | Skills with 3+ adapters/backends list them as subsystems | | Dense surfaces flagged | Topics with >10 patterns noted as reference_candidates | | Lifecycle skills considered | Suggest journey skills when docs have the material | +| Cross-references mapped | Skills that illuminate each other get "See also" pointers | --- diff --git a/packages/intent/meta/tree-generator/SKILL.md b/packages/intent/meta/tree-generator/SKILL.md index 046f2c8..a64cd18 100644 --- a/packages/intent/meta/tree-generator/SKILL.md +++ b/packages/intent/meta/tree-generator/SKILL.md @@ -520,6 +520,14 @@ See also: [lib]-core/[other-domain]/SKILL.md § Common Mistakes The cross-reference ensures agents that load one skill are pointed toward the related skill where the other side of the tension lives. +Also check the domain map's `cross_references` section for non-tension +relationships between skills. For each cross-reference, add a "See also" +line at the end of the relevant skill's body: + +```markdown +See also: [other-skill]/SKILL.md — [reason] +``` + ### Step 6 — Write composition skills (if applicable) Use the `compositions` entries from `domain_map.yaml` (populated during From e0d31bd538eb57c2d0e6c23caacb5677b2029e83 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:11:43 -0700 Subject: [PATCH 08/19] feat: setup --labels creates feedback labels on GitHub repo Adds --labels flag to intent setup that creates meta-feedback labels (feedback:domain-discovery, feedback:tree-generator, etc.) on the target GitHub repo using gh CLI. Prevents silent failures when the feedback command tries to add labels that don't exist. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/cli.ts | 2 +- packages/intent/src/setup.ts | 42 ++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 37d99f0..aa919e3 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -502,7 +502,7 @@ Usage: intent validate [] Validate skill files (default: skills/) intent init Set up intent discovery in agent configs intent scaffold Print maintainer scaffold prompt - intent setup [--workflows] [--all] Copy CI templates into your repo + intent setup [--workflows] [--shim] [--labels] [--all] Copy CI templates, generate shim, create labels intent stale Check skills for staleness intent feedback --submit --file Submit skill feedback intent feedback --meta --submit --file Submit meta-skill feedback` diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index 8abd319..2c05acd 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -1,3 +1,4 @@ +import { execSync } from 'node:child_process' import { existsSync, mkdirSync, @@ -15,6 +16,7 @@ export interface SetupResult { workflows: string[] skipped: string[] shim: string | null + labels: string[] } interface TemplateVars { @@ -148,6 +150,31 @@ function generateShim(root: string, result: SetupResult): void { result.shim = shimPath } +// --------------------------------------------------------------------------- +// Label creation +// --------------------------------------------------------------------------- + +const META_SKILL_LABELS = [ + 'feedback:domain-discovery', + 'feedback:tree-generator', + 'feedback:generate-skill', + 'feedback:skill-staleness-check', +] + +function createLabels(repo: string, result: SetupResult): void { + for (const label of META_SKILL_LABELS) { + try { + execSync( + `gh label create "${label}" --repo ${repo} --description "Feedback on the ${label.replace('feedback:', '')} meta skill" --color c5def5`, + { stdio: ['pipe', 'pipe', 'pipe'] }, + ) + result.labels.push(label) + } catch { + // Label likely already exists + } + } +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -160,14 +187,15 @@ export function runSetup( const doAll = args.includes('--all') const doWorkflows = doAll || args.includes('--workflows') const doShim = doAll || args.includes('--shim') + const doLabels = doAll || args.includes('--labels') // If no flags, default to --all - const defaultAll = !doWorkflows && !doShim + const defaultAll = !doWorkflows && !doShim && !doLabels const installWorkflows = doWorkflows || defaultAll const installShim = doShim || defaultAll const vars = detectVars(root) - const result: SetupResult = { workflows: [], skipped: [], shim: null } + const result: SetupResult = { workflows: [], skipped: [], shim: null, labels: [] } const templatesDir = join(metaDir, 'templates') @@ -183,6 +211,11 @@ export function runSetup( generateShim(root, result) } + if (doLabels) { + const repo = vars.REPO + createLabels(repo, result) + } + // Print results for (const f of result.workflows) console.log(`✓ Copied workflow: ${f}`) for (const f of result.skipped) console.log(` Already exists: ${f}`) @@ -195,9 +228,14 @@ export function runSetup( console.log(`\n Add "bin" to your package.json "files" array.`) } + if (result.labels.length > 0) { + console.log(`✓ Created ${result.labels.length} feedback labels on ${vars.REPO}`) + } + if ( result.workflows.length === 0 && result.shim === null && + result.labels.length === 0 && result.skipped.length === 0 ) { console.log('No templates directory found. Is @tanstack/intent installed?') From 6a52368c86d949111308804e5f321b3e1696ae4c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:19:57 +0000 Subject: [PATCH 09/19] ci: apply automated fixes --- docs/intent/functions/runSetup.md | 2 +- docs/intent/functions/submitMetaFeedback.md | 2 +- .../intent/meta/domain-discovery/SKILL.md | 10 ++++---- packages/intent/src/cli.ts | 23 ++++++++++++------ packages/intent/src/feedback.ts | 24 ++++++++++++------- packages/intent/src/setup.ts | 11 +++++++-- 6 files changed, 48 insertions(+), 24 deletions(-) diff --git a/docs/intent/functions/runSetup.md b/docs/intent/functions/runSetup.md index 7381c92..5c16aa9 100644 --- a/docs/intent/functions/runSetup.md +++ b/docs/intent/functions/runSetup.md @@ -12,7 +12,7 @@ function runSetup( args): SetupResult; ``` -Defined in: [setup.ts:140](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L140) +Defined in: [setup.ts:182](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L182) ## Parameters diff --git a/docs/intent/functions/submitMetaFeedback.md b/docs/intent/functions/submitMetaFeedback.md index 118bb87..d0dc750 100644 --- a/docs/intent/functions/submitMetaFeedback.md +++ b/docs/intent/functions/submitMetaFeedback.md @@ -9,7 +9,7 @@ title: submitMetaFeedback function submitMetaFeedback(payload, opts): SubmitResult; ``` -Defined in: [feedback.ts:329](https://github.com/TanStack/intent/blob/main/packages/intent/src/feedback.ts#L329) +Defined in: [feedback.ts:331](https://github.com/TanStack/intent/blob/main/packages/intent/src/feedback.ts#L331) ## Parameters diff --git a/packages/intent/meta/domain-discovery/SKILL.md b/packages/intent/meta/domain-discovery/SKILL.md index 5d04d10..97fec7b 100644 --- a/packages/intent/meta/domain-discovery/SKILL.md +++ b/packages/intent/meta/domain-discovery/SKILL.md @@ -346,10 +346,12 @@ skill B." These become "See also" pointers in the generated SKILL.md files. For each pair, note: + - Which skill references which (can be bidirectional) - Why awareness of the other skill improves output Examples: + - A quickstart skill references the security checklist ("after setup, audit") - A state management skill references an SSR skill ("state hydration requires understanding SSR lifecycle") @@ -646,9 +648,9 @@ not promotional.] ## Cross-References -| From | To | Reason | -| ------ | ------ | ---------------------------------------------- | -| [slug] | [slug] | [why awareness of one improves the other] | +| From | To | Reason | +| ------ | ------ | ----------------------------------------- | +| [slug] | [slug] | [why awareness of one improves the other] | ## Subsystems & Reference Candidates @@ -704,7 +706,7 @@ not promotional.] | Subsystems flagged | Skills with 3+ adapters/backends list them as subsystems | | Dense surfaces flagged | Topics with >10 patterns noted as reference_candidates | | Lifecycle skills considered | Suggest journey skills when docs have the material | -| Cross-references mapped | Skills that illuminate each other get "See also" pointers | +| Cross-references mapped | Skills that illuminate each other get "See also" pointers | --- diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index aa919e3..ddd1640 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -364,7 +364,9 @@ function cmdValidate(args: string[]): void { if (Object.keys(pkgJson).length > 0) { // Check @tanstack/intent in devDependencies - const devDeps = pkgJson.devDependencies as Record | undefined + const devDeps = pkgJson.devDependencies as + | Record + | undefined if (!devDeps?.['@tanstack/intent']) { warnings.push('@tanstack/intent is not in devDependencies') } @@ -379,20 +381,28 @@ function cmdValidate(args: string[]): void { const shimJs = join(process.cwd(), 'bin', 'intent.js') const shimMjs = join(process.cwd(), 'bin', 'intent.mjs') if (!existsSync(shimJs) && !existsSync(shimMjs)) { - warnings.push('No bin/intent.js or bin/intent.mjs shim found (run: npx @tanstack/intent setup --shim)') + warnings.push( + 'No bin/intent.js or bin/intent.mjs shim found (run: npx @tanstack/intent setup --shim)', + ) } // Check files array const files = pkgJson.files as string[] | undefined if (Array.isArray(files)) { if (!files.includes('skills')) { - warnings.push('"skills" is not in the "files" array — skills won\'t be published') + warnings.push( + '"skills" is not in the "files" array — skills won\'t be published', + ) } if (!files.includes('bin')) { - warnings.push('"bin" is not in the "files" array — shim won\'t be published') + warnings.push( + '"bin" is not in the "files" array — shim won\'t be published', + ) } if (!files.includes('!skills/_artifacts')) { - warnings.push('"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily') + warnings.push( + '"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily', + ) } } } @@ -419,8 +429,7 @@ function cmdValidate(args: string[]): void { function cmdScaffold(): void { const metaDir = getMetaDir() - const metaSkillPath = (name: string) => - join(metaDir, name, 'SKILL.md') + const metaSkillPath = (name: string) => join(metaDir, name, 'SKILL.md') const prompt = `You are helping a library maintainer scaffold Intent skills. diff --git a/packages/intent/src/feedback.ts b/packages/intent/src/feedback.ts index a0db6f2..33c5820 100644 --- a/packages/intent/src/feedback.ts +++ b/packages/intent/src/feedback.ts @@ -400,28 +400,34 @@ export function runFeedback(args: string[]): void { const md = readFileSync(filePath, 'utf8') if (containsSecrets(md)) { - console.error('Feedback file appears to contain secrets or tokens — submission rejected') + console.error( + 'Feedback file appears to contain secrets or tokens — submission rejected', + ) process.exit(1) } // Extract title from first heading, or use a default const titleMatch = md.match(/^#\s+(.+)/m) - const title = titleMatch?.[1] ?? (isMeta ? 'Meta-Skill Feedback' : 'Skill Feedback') + const title = + titleMatch?.[1] ?? (isMeta ? 'Meta-Skill Feedback' : 'Skill Feedback') const repo = isMeta ? META_FEEDBACK_REPO : undefined if (!repo && !isMeta) { - console.error('Markdown feedback for standard skills requires --meta flag or JSON format with a "package" field') + console.error( + 'Markdown feedback for standard skills requires --meta flag or JSON format with a "package" field', + ) process.exit(1) } if (ghAvailable && repo) { try { - const labelArg = isMeta && titleMatch?.[1] - ? (() => { - const skillMatch = titleMatch[1].match(/:\s*(\S+)/) - return skillMatch ? ` --label "feedback:${skillMatch[1]}"` : '' - })() - : '' + const labelArg = + isMeta && titleMatch?.[1] + ? (() => { + const skillMatch = titleMatch[1].match(/:\s*(\S+)/) + return skillMatch ? ` --label "feedback:${skillMatch[1]}"` : '' + })() + : '' execSync( `gh issue create --repo ${repo} --title "${title.replace(/"/g, '\\"')}"${labelArg} --body -`, { input: md, stdio: ['pipe', 'pipe', 'pipe'] }, diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index 2c05acd..7e48e4a 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -195,7 +195,12 @@ export function runSetup( const installShim = doShim || defaultAll const vars = detectVars(root) - const result: SetupResult = { workflows: [], skipped: [], shim: null, labels: [] } + const result: SetupResult = { + workflows: [], + skipped: [], + shim: null, + labels: [], + } const templatesDir = join(metaDir, 'templates') @@ -229,7 +234,9 @@ export function runSetup( } if (result.labels.length > 0) { - console.log(`✓ Created ${result.labels.length} feedback labels on ${vars.REPO}`) + console.log( + `✓ Created ${result.labels.length} feedback labels on ${vars.REPO}`, + ) } if ( From 9d7e3f7029dbf80f420a552627ecdb0364c0362e Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:28:55 -0700 Subject: [PATCH 10/19] fix: setup --labels creates per-skill feedback labels, not meta labels Labels are derived from the library's own skills/ directory (e.g. feedback:typography-core, feedback:react-typography), not hardcoded meta-skill labels. Added as a scaffold post-generation step. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/cli.ts | 3 ++- packages/intent/src/setup.ts | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index ddd1640..78d6fa9 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -493,7 +493,8 @@ This produces: individual SKILL.md files. 4. Ensure each package has \`@tanstack/intent\` as a devDependency 5. Add \`"skills"\`, \`"bin"\` to the \`"files"\` array in each package.json 6. Add \`"!skills/_artifacts"\` to exclude artifacts from publishing -7. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent init\`" +7. Run \`npx @tanstack/intent setup --labels\` to create feedback labels on the GitHub repo +8. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent init\`" ` console.log(prompt) diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index 7e48e4a..2071e03 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -7,6 +7,7 @@ import { writeFileSync, } from 'node:fs' import { join } from 'node:path' +import { findSkillFiles, parseFrontmatter } from './utils.js' // --------------------------------------------------------------------------- // Types @@ -154,18 +155,20 @@ function generateShim(root: string, result: SetupResult): void { // Label creation // --------------------------------------------------------------------------- -const META_SKILL_LABELS = [ - 'feedback:domain-discovery', - 'feedback:tree-generator', - 'feedback:generate-skill', - 'feedback:skill-staleness-check', -] +function createSkillLabels(root: string, repo: string, result: SetupResult): void { + const skillsDir = join(root, 'skills') + if (!existsSync(skillsDir)) return -function createLabels(repo: string, result: SetupResult): void { - for (const label of META_SKILL_LABELS) { + const skillFiles = findSkillFiles(skillsDir) + for (const filePath of skillFiles) { + const fm = parseFrontmatter(filePath) + const name = typeof fm?.name === 'string' ? fm.name : null + if (!name) continue + + const label = `feedback:${name}` try { execSync( - `gh label create "${label}" --repo ${repo} --description "Feedback on the ${label.replace('feedback:', '')} meta skill" --color c5def5`, + `gh label create "${label}" --repo ${repo} --description "Feedback on the ${name} skill" --color c5def5`, { stdio: ['pipe', 'pipe', 'pipe'] }, ) result.labels.push(label) @@ -217,8 +220,7 @@ export function runSetup( } if (doLabels) { - const repo = vars.REPO - createLabels(repo, result) + createSkillLabels(root, vars.REPO, result) } // Print results From 9eba70056bd78eae702820819e300489262922d3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:29:58 +0000 Subject: [PATCH 11/19] ci: apply automated fixes --- docs/intent/functions/runSetup.md | 2 +- packages/intent/src/setup.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/intent/functions/runSetup.md b/docs/intent/functions/runSetup.md index 5c16aa9..dfc3129 100644 --- a/docs/intent/functions/runSetup.md +++ b/docs/intent/functions/runSetup.md @@ -12,7 +12,7 @@ function runSetup( args): SetupResult; ``` -Defined in: [setup.ts:182](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L182) +Defined in: [setup.ts:189](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L189) ## Parameters diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index 2071e03..c609137 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -155,7 +155,11 @@ function generateShim(root: string, result: SetupResult): void { // Label creation // --------------------------------------------------------------------------- -function createSkillLabels(root: string, repo: string, result: SetupResult): void { +function createSkillLabels( + root: string, + repo: string, + result: SetupResult, +): void { const skillsDir = join(root, 'skills') if (!existsSync(skillsDir)) return From db1eefa782f83153b80ffd3e831f7e65b0eaa4e5 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:30:21 -0700 Subject: [PATCH 12/19] fix: feedback skill submits directly via gh, no CLI intermediary The feedback-collection skill now instructs agents to use gh issue create directly (or tell the user how to submit manually) instead of routing through the intent feedback CLI. Simpler, fewer failure modes, better formatting. Co-Authored-By: Claude Opus 4.6 --- .../intent/meta/feedback-collection/SKILL.md | 129 +++++++++--------- packages/intent/src/cli.ts | 5 +- packages/intent/src/intent-library.ts | 5 +- 3 files changed, 69 insertions(+), 70 deletions(-) diff --git a/packages/intent/meta/feedback-collection/SKILL.md b/packages/intent/meta/feedback-collection/SKILL.md index fb7971d..3153c6f 100644 --- a/packages/intent/meta/feedback-collection/SKILL.md +++ b/packages/intent/meta/feedback-collection/SKILL.md @@ -4,12 +4,11 @@ description: > Collect structured feedback about skill usage after completing a coding task. Activate at the end of any session where one or more SKILL.md files were loaded. Captures agent signals (gaps, errors, corrections, human interventions) - and brief human input, then submits via the intent feedback CLI. + and brief human input, then submits directly via gh CLI or provides manual + submission instructions. metadata: - version: '1.0' + version: '2.0' category: meta-tooling - output_artifacts: - - intent-feedback.json --- # Skill Feedback Collection @@ -83,74 +82,80 @@ If the human gives an explicit rating, use that instead. --- -## Phase 3 — Build the Payload - -Construct one JSON payload per skill used. The schema must match exactly: - -```json -{ - "skill": "", - "package": "", - "skillVersion": "", - "task": "", - "whatWorked": "", - "whatFailed": "", - "missing": "", - "selfCorrections": "", - "userRating": "good | mixed | bad", - "userComments": "" -} -``` +## Phase 3 — Build the Feedback -### Field derivation guide +Write one Markdown feedback file per skill used. Only include skills that were +actually used during the session — skip any that were loaded but never +referenced. + +### Template + +```markdown +# Skill Feedback: [skill name from SKILL.md frontmatter] + +**Package:** [npm package name that contains the skill] +**Skill version:** [metadata.version or library_version from frontmatter] +**Rating:** [good | mixed | bad] + +## Task +[one-sentence summary of what the human asked you to do] + +## What Worked +[patterns/instructions from the skill that were accurate and helpful] + +## What Failed +[from 1b — skill instructions that produced errors] + +## Missing +[from 1a — gaps where the skill should have covered] -| Field | Source | -| ----------------- | ------------------------------------------------------------------ | -| `skill` | Frontmatter `name` field of the SKILL.md you loaded | -| `package` | The npm package the skill lives in (e.g. `@tanstack/query-intent`) | -| `skillVersion` | Frontmatter `metadata.version` or `library_version` | -| `task` | Summarize the human's original request in one sentence | -| `whatWorked` | List skill sections/patterns that were correct and useful | -| `whatFailed` | From 1b — skill instructions that produced errors | -| `missing` | From 1a — gaps where the skill was silent | -| `selfCorrections` | From 1b fixes + 1c human interventions, combined | -| `userRating` | From Phase 2 sentiment analysis or explicit rating | -| `userComments` | From Phase 2 answers, keep brief | - -### Example - -```json -{ - "skill": "tanstack-query/core", - "package": "@anthropic/tanstack-query-intent", - "skillVersion": "1.0", - "task": "Add optimistic updates to a mutation with rollback on error", - "whatWorked": "Setup pattern was correct. onMutate/onError/onSettled lifecycle was accurate.", - "whatFailed": "Cache key format used array syntax that doesn't match v5 — had to switch to queryOptions pattern.", - "missing": "No guidance on TypeScript generics for mutation variables. Had to read source.", - "selfCorrections": "Fixed cache key format. Human corrected the rollback type to include undefined.", - "userRating": "mixed", - "userComments": "The optimistic update pattern was helpful but the cache key thing wasted 10 minutes." -} +## Self-Corrections +[from 1b fixes + 1c human interventions, combined] + +## User Comments +[optional — direct quotes or paraphrased human input from Phase 2] ``` -If multiple skills were loaded, produce one payload per skill. Only include -skills that were actually used during the session — skip any that were loaded -but never referenced. +### Field derivation guide + +| Field | Source | +| ---------------- | ------------------------------------------------------------------ | +| Skill name | Frontmatter `name` field of the SKILL.md you loaded | +| Package | The npm package the skill lives in (e.g. `@tanstack/query-intent`) | +| Skill version | Frontmatter `metadata.version` or `library_version` | +| Task | Summarize the human's original request in one sentence | +| What Worked | List skill sections/patterns that were correct and useful | +| What Failed | From 1b — skill instructions that produced errors | +| Missing | From 1a — gaps where the skill was silent | +| Self-Corrections | From 1b fixes + 1c human interventions, combined | +| Rating | From Phase 2 sentiment analysis or explicit rating | +| User Comments | From Phase 2 answers, keep brief | --- ## Phase 4 — Submit -1. Save the payload to `intent-feedback.json` in the project root. - If multiple skills, save as a JSON array. +Determine the target repo from the skill's package. The repo is typically +derivable from the `repository` field in the package's `package.json`, or +from the `sources` field in the SKILL.md frontmatter. + +### If `gh` CLI is available + +Submit directly as a GitHub issue: + +```bash +gh issue create --repo [owner/repo] --title "Skill Feedback: [skill-name] ([rating])" --label "feedback:[skill-name]" --body-file intent-feedback.md +``` + +If the label doesn't exist, omit the `--label` flag — don't let a missing +label block submission. -2. Submit: +If submission succeeds, delete the feedback file. - ```bash - npx intent feedback --submit --file intent-feedback.json - ``` +### If `gh` CLI is not available -3. If the submission succeeds, delete `intent-feedback.json`. +Tell the human: -4. If it fails, tell the human and leave the file for manual retry. +> "I've written skill feedback to `intent-feedback.md`. To submit it, +> open an issue at https://github.com/[owner/repo]/issues and paste the +> contents." diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 78d6fa9..e36ec6a 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -172,10 +172,7 @@ async function cmdList(args: string[]): Promise { console.log(`Feedback:`) console.log(` After completing your task, collect feedback on skill usage.`) console.log( - ` Instructions: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md`, - ) - console.log( - ` Submit: npx intent feedback --submit --file intent-feedback.json`, + ` Load: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md`, ) console.log() diff --git a/packages/intent/src/intent-library.ts b/packages/intent/src/intent-library.ts index bbfd123..6052f43 100644 --- a/packages/intent/src/intent-library.ts +++ b/packages/intent/src/intent-library.ts @@ -152,10 +152,7 @@ async function cmdList(): Promise { console.log(`Feedback:`) console.log(` After completing your task, collect feedback on skill usage.`) console.log( - ` Instructions: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md`, - ) - console.log( - ` Submit: npx intent feedback --submit --file intent-feedback.json`, + ` Load: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md`, ) console.log() From 110018cf4d2dfeda1dfbfb7391709a4980912e13 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:31:29 +0000 Subject: [PATCH 13/19] ci: apply automated fixes --- .../intent/meta/feedback-collection/SKILL.md | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/intent/meta/feedback-collection/SKILL.md b/packages/intent/meta/feedback-collection/SKILL.md index 3153c6f..4c9386d 100644 --- a/packages/intent/meta/feedback-collection/SKILL.md +++ b/packages/intent/meta/feedback-collection/SKILL.md @@ -98,21 +98,27 @@ referenced. **Rating:** [good | mixed | bad] ## Task + [one-sentence summary of what the human asked you to do] ## What Worked + [patterns/instructions from the skill that were accurate and helpful] ## What Failed + [from 1b — skill instructions that produced errors] ## Missing + [from 1a — gaps where the skill should have covered] ## Self-Corrections + [from 1b fixes + 1c human interventions, combined] ## User Comments + [optional — direct quotes or paraphrased human input from Phase 2] ``` @@ -123,13 +129,13 @@ referenced. | Skill name | Frontmatter `name` field of the SKILL.md you loaded | | Package | The npm package the skill lives in (e.g. `@tanstack/query-intent`) | | Skill version | Frontmatter `metadata.version` or `library_version` | -| Task | Summarize the human's original request in one sentence | -| What Worked | List skill sections/patterns that were correct and useful | -| What Failed | From 1b — skill instructions that produced errors | -| Missing | From 1a — gaps where the skill was silent | -| Self-Corrections | From 1b fixes + 1c human interventions, combined | -| Rating | From Phase 2 sentiment analysis or explicit rating | -| User Comments | From Phase 2 answers, keep brief | +| Task | Summarize the human's original request in one sentence | +| What Worked | List skill sections/patterns that were correct and useful | +| What Failed | From 1b — skill instructions that produced errors | +| Missing | From 1a — gaps where the skill was silent | +| Self-Corrections | From 1b fixes + 1c human interventions, combined | +| Rating | From Phase 2 sentiment analysis or explicit rating | +| User Comments | From Phase 2 answers, keep brief | --- From 9998241844b24ebd9cfddbceeda70eb4021a7a37 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:35:36 -0700 Subject: [PATCH 14/19] =?UTF-8?q?rename=20init=20=E2=86=92=20install,=20al?= =?UTF-8?q?ign=20with=20library=20CLI=20prompt=20approach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The maintainer CLI's install command now prints the same guided prompt as the library CLI — the agent scans the project and sets up skill-to-task mappings in agent config files, rather than programmatically injecting blocks. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/cli.ts | 76 ++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index e36ec6a..9f493e2 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -491,7 +491,7 @@ This produces: individual SKILL.md files. 5. Add \`"skills"\`, \`"bin"\` to the \`"files"\` array in each package.json 6. Add \`"!skills/_artifacts"\` to exclude artifacts from publishing 7. Run \`npx @tanstack/intent setup --labels\` to create feedback labels on the GitHub repo -8. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent init\`" +8. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent install\`" ` console.log(prompt) @@ -507,12 +507,11 @@ Usage: intent list [--json] Discover intent-enabled packages intent meta List meta-skills for maintainers intent validate [] Validate skill files (default: skills/) - intent init Set up intent discovery in agent configs + intent install Print a skill that guides your coding agent to set up skill-to-task mappings intent scaffold Print maintainer scaffold prompt intent setup [--workflows] [--shim] [--labels] [--all] Copy CI templates, generate shim, create labels intent stale Check skills for staleness - intent feedback --submit --file Submit skill feedback - intent feedback --meta --submit --file Submit meta-skill feedback` + intent feedback --submit --file Submit skill feedback` const command = process.argv[2] const commandArgs = process.argv.slice(3) @@ -527,26 +526,55 @@ switch (command) { case 'validate': cmdValidate(commandArgs) break - case 'init': { - const { runInit, detectAgentConfigs } = await import('./init.js') - const initRoot = process.cwd() - const result = runInit(initRoot) - - for (const f of result.injected) console.log(`✓ Added intent block to ${f}`) - for (const f of result.skipped) console.log(` Already present in ${f}`) - for (const f of result.created) console.log(`✓ Created ${f}`) - - if (result.injected.length === 0 && result.skipped.length === 0) { - const detected = detectAgentConfigs(initRoot) - if (detected.length === 0) { - console.log( - 'No agent config files found (AGENTS.md, CLAUDE.md, .cursorrules, .github/copilot-instructions.md).', - ) - console.log('Create one of these files and run intent init again.') - } - } - - console.log(`✓ Config: ${result.configPath}`) + case 'install': { + const prompt = `You are an AI assistant helping a developer set up skill-to-task mappings for their project. + +Follow these steps in order: + +1. CHECK FOR EXISTING MAPPINGS + Search the project's agent config files (CLAUDE.md, AGENTS.md, .cursorrules, + .github/copilot-instructions.md) for a block delimited by: + + + - If found: show the user the current mappings and ask "What would you like to update?" + Then skip to step 4 with their requested changes. + - If not found: continue to step 2. + +2. DISCOVER AVAILABLE SKILLS + Run: intent list + This outputs each skill's name, description, and full path — grouped by package. + +3. SCAN THE REPOSITORY + Build a picture of the project's structure and patterns: + - Read package.json for library dependencies + - Survey the directory layout (src/, app/, routes/, components/, api/, etc.) + - Note recurring patterns (routing, data fetching, auth, UI components, etc.) + + Based on this, propose 3–5 skill-to-task mappings. For each one explain: + - The task or code area (in plain language the user would recognise) + - Which skill applies and why + + Then ask: "What other tasks do you commonly use AI coding agents for? + I'll create mappings for those too." + +4. WRITE THE MAPPINGS BLOCK + Once you have the full set of mappings, write or update the agent config file + (prefer CLAUDE.md; create it if none exists) with this exact block: + + +# Skill mappings — when working in these areas, load the linked skill file into context. +skills: + - task: "describe the task or code area here" + load: "node_modules/package-name/skills/skill-name/SKILL.md" + + + Rules: + - Use the user's own words for task descriptions + - Include the exact path from \`intent list\` output so agents can load it directly + - Keep entries concise — this block is read on every agent task + - Preserve all content outside the block tags unchanged` + + console.log(prompt) break } case 'scaffold': { From dfc7cc10cf1e3cdb43a8a53a5c20e7362e362085 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 19:36:46 -0700 Subject: [PATCH 15/19] =?UTF-8?q?remove=20feedback=20CLI=20command=20?= =?UTF-8?q?=E2=80=94=20agents=20submit=20via=20gh=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The feedback-collection SKILL.md now instructs agents to use `gh issue create` directly rather than routing through a CLI command. Removed the `feedback` case from the CLI switch and the `runFeedback` function from feedback.ts. Library utility functions (validate, toMarkdown, submit) remain as public API. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/cli.ts | 8 +- packages/intent/src/feedback.ts | 162 +------------------------------- 2 files changed, 2 insertions(+), 168 deletions(-) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 9f493e2..d221699 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -510,8 +510,7 @@ Usage: intent install Print a skill that guides your coding agent to set up skill-to-task mappings intent scaffold Print maintainer scaffold prompt intent setup [--workflows] [--shim] [--labels] [--all] Copy CI templates, generate shim, create labels - intent stale Check skills for staleness - intent feedback --submit --file Submit skill feedback` + intent stale Check skills for staleness` const command = process.argv[2] const commandArgs = process.argv.slice(3) @@ -632,11 +631,6 @@ skills: } break } - case 'feedback': { - const { runFeedback } = await import('./feedback.js') - runFeedback(commandArgs) - break - } case 'setup': { const { runSetup } = await import('./setup.js') runSetup(process.cwd(), getMetaDir(), commandArgs) diff --git a/packages/intent/src/feedback.ts b/packages/intent/src/feedback.ts index 33c5820..d2196c3 100644 --- a/packages/intent/src/feedback.ts +++ b/packages/intent/src/feedback.ts @@ -1,5 +1,5 @@ import { execSync } from 'node:child_process' -import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import type { FeedbackPayload, @@ -360,163 +360,3 @@ export function submitMetaFeedback( return { method: 'stdout', detail: md } } -// --------------------------------------------------------------------------- -// CLI runner -// --------------------------------------------------------------------------- - -export function runFeedback(args: string[]): void { - const isMeta = args.includes('--meta') - const submitFlag = args.includes('--submit') - const fileIdx = args.indexOf('--file') - const filePath = fileIdx !== -1 ? args[fileIdx + 1] : undefined - - if (!submitFlag || !filePath) { - if (isMeta) { - console.error('Usage: intent feedback --meta --submit --file ') - } else { - console.error('Usage: intent feedback --submit --file ') - } - process.exit(1) - } - - if (!existsSync(filePath)) { - console.error(`File not found: ${filePath}`) - process.exit(1) - } - - const ghAvailable = hasGhCli() - const frequency = resolveFrequency(process.cwd()) - - if (frequency === 'never') { - console.log('Feedback is disabled (frequency: never)') - return - } - - const dateSuffix = new Date().toISOString().slice(0, 10) - const isMarkdown = filePath.endsWith('.md') - - // Markdown mode: submit the file content directly as an issue body - if (isMarkdown) { - const md = readFileSync(filePath, 'utf8') - - if (containsSecrets(md)) { - console.error( - 'Feedback file appears to contain secrets or tokens — submission rejected', - ) - process.exit(1) - } - - // Extract title from first heading, or use a default - const titleMatch = md.match(/^#\s+(.+)/m) - const title = - titleMatch?.[1] ?? (isMeta ? 'Meta-Skill Feedback' : 'Skill Feedback') - const repo = isMeta ? META_FEEDBACK_REPO : undefined - - if (!repo && !isMeta) { - console.error( - 'Markdown feedback for standard skills requires --meta flag or JSON format with a "package" field', - ) - process.exit(1) - } - - if (ghAvailable && repo) { - try { - const labelArg = - isMeta && titleMatch?.[1] - ? (() => { - const skillMatch = titleMatch[1].match(/:\s*(\S+)/) - return skillMatch ? ` --label "feedback:${skillMatch[1]}"` : '' - })() - : '' - execSync( - `gh issue create --repo ${repo} --title "${title.replace(/"/g, '\\"')}"${labelArg} --body -`, - { input: md, stdio: ['pipe', 'pipe', 'pipe'] }, - ) - console.log(`✓ Submitted issue to ${repo}`) - } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - console.error(`GitHub submission failed: ${msg}`) - console.log('--- Feedback markdown (copy/paste to issue) ---') - console.log(md) - } - } else { - console.log('--- Feedback markdown (copy/paste to issue) ---') - console.log(md) - } - return - } - - // JSON mode - let raw: unknown - try { - raw = JSON.parse(readFileSync(filePath, 'utf8')) - } catch { - console.error('Invalid JSON in feedback file') - process.exit(1) - } - - if (isMeta) { - const validation = validateMetaPayload(raw) - if (!validation.valid) { - console.error('Meta-feedback validation failed:') - for (const err of validation.errors) console.error(` - ${err}`) - process.exit(1) - } - - const payload = raw as MetaFeedbackPayload - const fallbackPath = `intent-meta-feedback-${dateSuffix}.md` - - const result = submitMetaFeedback(payload, { - ghAvailable, - outputPath: ghAvailable ? undefined : fallbackPath, - }) - - switch (result.method) { - case 'gh': - console.log(`✓ ${result.detail}`) - break - case 'file': - console.log(`✓ ${result.detail}`) - console.log( - `You can manually open an issue at https://github.com/${META_FEEDBACK_REPO}/issues with this content.`, - ) - break - case 'stdout': - console.log('--- Meta-feedback markdown (copy/paste to issue) ---') - console.log(result.detail) - break - } - return - } - - // Standard skill feedback - const validation = validatePayload(raw) - if (!validation.valid) { - console.error('Feedback validation failed:') - for (const err of validation.errors) console.error(` - ${err}`) - process.exit(1) - } - - const payload = raw as FeedbackPayload - const repo = payload.package.replace(/^@/, '').replace(/\//, '/') - const fallbackPath = `intent-feedback-${dateSuffix}.md` - - const result = submitFeedback(payload, repo, { - ghAvailable, - outputPath: ghAvailable ? undefined : fallbackPath, - }) - - switch (result.method) { - case 'gh': - console.log(`✓ ${result.detail}`) - break - case 'file': - console.log(`✓ ${result.detail}`) - console.log('You can manually open an issue with this content.') - break - case 'stdout': - console.log('--- Feedback markdown (copy/paste to issue) ---') - console.log(result.detail) - break - } -} From 533046c92ac2df45111dcc9f1f58a22768c80f27 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 02:37:39 +0000 Subject: [PATCH 16/19] ci: apply automated fixes --- packages/intent/src/feedback.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/intent/src/feedback.ts b/packages/intent/src/feedback.ts index d2196c3..eb721d7 100644 --- a/packages/intent/src/feedback.ts +++ b/packages/intent/src/feedback.ts @@ -359,4 +359,3 @@ export function submitMetaFeedback( return { method: 'stdout', detail: md } } - From 66b03922dff0c4a9fc8cc6325b94667adbbc54e7 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 20:09:14 -0700 Subject: [PATCH 17/19] apply code review and simplification fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use execFileSync instead of execSync to prevent shell injection - Fix stale phase cross-references in domain-discovery and tree-generator - Remove createSkillLabels and --labels flag (agents create labels directly) - Remove init.ts — replaced by prompt-based install command - Extract shared display helpers into display.ts - Extract collectPackagingWarnings, detectShimExtension, findExistingShim - Surface package.json parse errors in validator warnings - Update README to reflect current CLI surface Co-Authored-By: Claude Opus 4.6 --- packages/intent/README.md | 23 +- .../intent/meta/domain-discovery/SKILL.md | 2 +- packages/intent/meta/tree-generator/SKILL.md | 2 +- packages/intent/src/cli.ts | 228 ++++++------------ packages/intent/src/display.ts | 98 ++++++++ packages/intent/src/feedback.ts | 23 +- packages/intent/src/index.ts | 8 - packages/intent/src/init.ts | 122 ---------- packages/intent/src/intent-library.ts | 101 +------- packages/intent/src/setup.ts | 91 ++----- packages/intent/tests/init.test.ts | 193 --------------- 11 files changed, 230 insertions(+), 661 deletions(-) create mode 100644 packages/intent/src/display.ts delete mode 100644 packages/intent/src/init.ts delete mode 100644 packages/intent/tests/init.test.ts diff --git a/packages/intent/README.md b/packages/intent/README.md index 97408a0..0303ac8 100644 --- a/packages/intent/README.md +++ b/packages/intent/README.md @@ -16,10 +16,10 @@ pnpm add -D @tanstack/intent ### For library consumers -Set up intent discovery in your project's agent config files (CLAUDE.md, .cursorrules, etc.): +Set up skill-to-task mappings in your project's agent config files (CLAUDE.md, .cursorrules, etc.): ```bash -npx intent init +npx intent install ``` List available skills from installed packages: @@ -50,16 +50,15 @@ npx intent setup ## CLI Commands -| Command | Description | -| ----------------------- | ----------------------------------------------- | -| `intent init` | Inject intent discovery into agent config files | -| `intent list [--json]` | Discover intent-enabled packages | -| `intent meta` | List meta-skills for library maintainers | -| `intent scaffold` | Print the guided skill generation prompt | -| `intent validate [dir]` | Validate SKILL.md files | -| `intent setup` | Copy CI workflow templates | -| `intent stale [--json]` | Check skills for version drift | -| `intent feedback` | Submit skill feedback | +| Command | Description | +| ----------------------- | ---------------------------------------------------- | +| `intent install` | Set up skill-to-task mappings in agent config files | +| `intent list [--json]` | Discover intent-enabled packages | +| `intent meta` | List meta-skills for library maintainers | +| `intent scaffold` | Print the guided skill generation prompt | +| `intent validate [dir]` | Validate SKILL.md files | +| `intent setup` | Copy CI templates, generate shim, create labels | +| `intent stale [--json]` | Check skills for version drift | ## License diff --git a/packages/intent/meta/domain-discovery/SKILL.md b/packages/intent/meta/domain-discovery/SKILL.md index 97fec7b..4534c57 100644 --- a/packages/intent/meta/domain-discovery/SKILL.md +++ b/packages/intent/meta/domain-discovery/SKILL.md @@ -428,7 +428,7 @@ Follow up on any corrections. Then: ### 4b — Gap-targeted questions (3–8 questions) -For each gap flagged in Phase 3f, ask a specific question. These are not +For each gap flagged in Phase 3g, ask a specific question. These are not generic — they reference what you found: **Instead of:** "What do developers get wrong?" diff --git a/packages/intent/meta/tree-generator/SKILL.md b/packages/intent/meta/tree-generator/SKILL.md index a64cd18..d207342 100644 --- a/packages/intent/meta/tree-generator/SKILL.md +++ b/packages/intent/meta/tree-generator/SKILL.md @@ -531,7 +531,7 @@ See also: [other-skill]/SKILL.md — [reason] ### Step 6 — Write composition skills (if applicable) Use the `compositions` entries from `domain_map.yaml` (populated during -skill-domain-discovery Phase 2h) to identify which composition skills +skill-domain-discovery Phase 3h) to identify which composition skills to produce. Composition skills cover how two or more libraries work together. These diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index d221699..5198730 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -4,6 +4,11 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs' import { dirname, join, relative, sep } from 'node:path' import { fileURLToPath } from 'node:url' import { parse as parseYaml } from 'yaml' +import { + computeSkillNameWidth, + printSkillTree, + printTable, +} from './display.js' import { scanForIntents } from './scanner.js' import type { ScanResult } from './types.js' import { findSkillFiles, parseFrontmatter } from './utils.js' @@ -13,7 +18,6 @@ import { findSkillFiles, parseFrontmatter } from './utils.js' // --------------------------------------------------------------------------- function getMetaDir(): string { - // Resolve relative to this file's location in dist/ const thisDir = dirname(fileURLToPath(import.meta.url)) return join(thisDir, '..', 'meta') } @@ -22,99 +26,6 @@ function getMetaDir(): string { // Commands // --------------------------------------------------------------------------- -function padColumn(text: string, width: number): string { - return text.length >= width ? text + ' ' : text.padEnd(width) -} - -function printTable(headers: string[], rows: string[][]): void { - const widths = headers.map( - (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)) + 2, - ) - - const headerLine = headers.map((h, i) => padColumn(h, widths[i]!)).join('') - const separator = widths.map((w) => '─'.repeat(w)).join('') - - console.log(headerLine) - console.log(separator) - for (const row of rows) { - console.log(row.map((cell, i) => padColumn(cell, widths[i]!)).join('')) - } -} - -interface SkillDisplay { - name: string - description: string - type?: string - path?: string -} - -function printSkillTree( - skills: SkillDisplay[], - opts: { nameWidth: number; showTypes: boolean }, -): void { - const roots: string[] = [] - const children = new Map() - - for (const skill of skills) { - const slashIdx = skill.name.indexOf('/') - if (slashIdx === -1) { - roots.push(skill.name) - } else { - const parent = skill.name.slice(0, slashIdx) - if (!children.has(parent)) children.set(parent, []) - children.get(parent)!.push(skill) - } - } - - if (roots.length === 0) { - for (const skill of skills) { - if (!roots.includes(skill.name)) roots.push(skill.name) - } - } - - for (const rootName of roots) { - const rootSkill = skills.find((s) => s.name === rootName) - if (!rootSkill) continue - - printSkillLine(rootName, rootSkill, 4, opts) - - for (const sub of children.get(rootName) ?? []) { - const childName = sub.name.slice(sub.name.indexOf('/') + 1) - printSkillLine(childName, sub, 6, opts) - } - } -} - -function printSkillLine( - displayName: string, - skill: SkillDisplay, - indent: number, - opts: { nameWidth: number; showTypes: boolean }, -): void { - const nameStr = ' '.repeat(indent) + displayName - const padding = ' '.repeat(Math.max(2, opts.nameWidth - nameStr.length)) - const typeCol = opts.showTypes - ? (skill.type ? `[${skill.type}]` : '').padEnd(14) - : '' - console.log(`${nameStr}${padding}${typeCol}${skill.description}`) - if (skill.path) { - console.log(`${' '.repeat(indent + 2)}${skill.path}`) - } -} - -function computeSkillNameWidth(allPackageSkills: SkillDisplay[][]): number { - let max = 0 - for (const skills of allPackageSkills) { - for (const s of skills) { - const slashIdx = s.name.indexOf('/') - const displayName = slashIdx === -1 ? s.name : s.name.slice(slashIdx + 1) - const indent = slashIdx === -1 ? 4 : 6 - max = Math.max(max, indent + displayName.length) - } - } - return max + 2 -} - async function cmdList(args: string[]): Promise { const jsonOutput = args.includes('--json') @@ -218,6 +129,62 @@ function cmdMeta(): void { console.log(`Path: node_modules/@tanstack/intent/meta//SKILL.md`) } +function collectPackagingWarnings(root: string): string[] { + const pkgJsonPath = join(root, 'package.json') + if (!existsSync(pkgJsonPath)) return [] + + let pkgJson: Record + try { + pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + return [`Could not parse package.json: ${msg}`] + } + + const warnings: string[] = [] + + const devDeps = pkgJson.devDependencies as + | Record + | undefined + if (!devDeps?.['@tanstack/intent']) { + warnings.push('@tanstack/intent is not in devDependencies') + } + + const bin = pkgJson.bin as Record | undefined + if (!bin?.intent) { + warnings.push('Missing "bin": { "intent": ... } entry in package.json') + } + + const shimJs = join(root, 'bin', 'intent.js') + const shimMjs = join(root, 'bin', 'intent.mjs') + if (!existsSync(shimJs) && !existsSync(shimMjs)) { + warnings.push( + 'No bin/intent.js or bin/intent.mjs shim found (run: npx @tanstack/intent setup --shim)', + ) + } + + const files = pkgJson.files as string[] | undefined + if (Array.isArray(files)) { + if (!files.includes('skills')) { + warnings.push( + '"skills" is not in the "files" array — skills won\'t be published', + ) + } + if (!files.includes('bin')) { + warnings.push( + '"bin" is not in the "files" array — shim won\'t be published', + ) + } + if (!files.includes('!skills/_artifacts')) { + warnings.push( + '"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily', + ) + } + } + + return warnings +} + function cmdValidate(args: string[]): void { const targetDir = args[0] ?? 'skills' const skillsDir = join(process.cwd(), targetDir) @@ -348,61 +315,12 @@ function cmdValidate(args: string[]): void { } } - // Packaging checks — run when package.json exists at cwd - const pkgJsonPath = join(process.cwd(), 'package.json') - const warnings: string[] = [] - if (existsSync(pkgJsonPath)) { - let pkgJson: Record = {} - try { - pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) - } catch { - // skip packaging checks if we can't read package.json - } - - if (Object.keys(pkgJson).length > 0) { - // Check @tanstack/intent in devDependencies - const devDeps = pkgJson.devDependencies as - | Record - | undefined - if (!devDeps?.['@tanstack/intent']) { - warnings.push('@tanstack/intent is not in devDependencies') - } - - // Check bin entry - const bin = pkgJson.bin as Record | undefined - if (!bin?.intent) { - warnings.push('Missing "bin": { "intent": ... } entry in package.json') - } - - // Check shim file exists - const shimJs = join(process.cwd(), 'bin', 'intent.js') - const shimMjs = join(process.cwd(), 'bin', 'intent.mjs') - if (!existsSync(shimJs) && !existsSync(shimMjs)) { - warnings.push( - 'No bin/intent.js or bin/intent.mjs shim found (run: npx @tanstack/intent setup --shim)', - ) - } + const warnings = collectPackagingWarnings(process.cwd()) - // Check files array - const files = pkgJson.files as string[] | undefined - if (Array.isArray(files)) { - if (!files.includes('skills')) { - warnings.push( - '"skills" is not in the "files" array — skills won\'t be published', - ) - } - if (!files.includes('bin')) { - warnings.push( - '"bin" is not in the "files" array — shim won\'t be published', - ) - } - if (!files.includes('!skills/_artifacts')) { - warnings.push( - '"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily', - ) - } - } - } + const printWarnings = (log: (...args: unknown[]) => void): void => { + if (warnings.length === 0) return + log(`\n⚠ Packaging warnings:`) + for (const w of warnings) log(` ${w}`) } if (errors.length > 0) { @@ -410,18 +328,12 @@ function cmdValidate(args: string[]): void { for (const { file, message } of errors) { console.error(` ${file}: ${message}`) } - if (warnings.length > 0) { - console.error(`\n⚠ Packaging warnings:`) - for (const w of warnings) console.error(` ${w}`) - } + printWarnings(console.error) process.exit(1) } console.log(`✅ Validated ${skillFiles.length} skill files — all passed`) - if (warnings.length > 0) { - console.log(`\n⚠ Packaging warnings:`) - for (const w of warnings) console.log(` ${w}`) - } + printWarnings(console.log) } function cmdScaffold(): void { @@ -490,7 +402,7 @@ This produces: individual SKILL.md files. 4. Ensure each package has \`@tanstack/intent\` as a devDependency 5. Add \`"skills"\`, \`"bin"\` to the \`"files"\` array in each package.json 6. Add \`"!skills/_artifacts"\` to exclude artifacts from publishing -7. Run \`npx @tanstack/intent setup --labels\` to create feedback labels on the GitHub repo +7. Create a \`feedback:\` label on the GitHub repo for each skill (use \`gh label create\`) 8. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent install\`" ` @@ -509,7 +421,7 @@ Usage: intent validate [] Validate skill files (default: skills/) intent install Print a skill that guides your coding agent to set up skill-to-task mappings intent scaffold Print maintainer scaffold prompt - intent setup [--workflows] [--shim] [--labels] [--all] Copy CI templates, generate shim, create labels + intent setup [--workflows] [--shim] [--all] Copy CI templates and generate shim intent stale Check skills for staleness` const command = process.argv[2] diff --git a/packages/intent/src/display.ts b/packages/intent/src/display.ts new file mode 100644 index 0000000..3e6823e --- /dev/null +++ b/packages/intent/src/display.ts @@ -0,0 +1,98 @@ +// --------------------------------------------------------------------------- +// Shared display helpers for CLI output +// --------------------------------------------------------------------------- + +export interface SkillDisplay { + name: string + description: string + type?: string + path?: string +} + +export function padColumn(text: string, width: number): string { + return text.length >= width ? text + ' ' : text.padEnd(width) +} + +export function printTable(headers: string[], rows: string[][]): void { + const widths = headers.map( + (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)) + 2, + ) + + const headerLine = headers.map((h, i) => padColumn(h, widths[i]!)).join('') + const separator = widths.map((w) => '─'.repeat(w)).join('') + + console.log(headerLine) + console.log(separator) + for (const row of rows) { + console.log(row.map((cell, i) => padColumn(cell, widths[i]!)).join('')) + } +} + +export function printSkillLine( + displayName: string, + skill: SkillDisplay, + indent: number, + opts: { nameWidth: number; showTypes: boolean }, +): void { + const nameStr = ' '.repeat(indent) + displayName + const padding = ' '.repeat(Math.max(2, opts.nameWidth - nameStr.length)) + const typeCol = opts.showTypes + ? (skill.type ? `[${skill.type}]` : '').padEnd(14) + : '' + console.log(`${nameStr}${padding}${typeCol}${skill.description}`) + if (skill.path) { + console.log(`${' '.repeat(indent + 2)}${skill.path}`) + } +} + +export function printSkillTree( + skills: SkillDisplay[], + opts: { nameWidth: number; showTypes: boolean }, +): void { + const roots: string[] = [] + const children = new Map() + + for (const skill of skills) { + const slashIdx = skill.name.indexOf('/') + if (slashIdx === -1) { + roots.push(skill.name) + } else { + const parent = skill.name.slice(0, slashIdx) + if (!children.has(parent)) children.set(parent, []) + children.get(parent)!.push(skill) + } + } + + if (roots.length === 0) { + for (const skill of skills) { + if (!roots.includes(skill.name)) roots.push(skill.name) + } + } + + for (const rootName of roots) { + const rootSkill = skills.find((s) => s.name === rootName) + if (!rootSkill) continue + + printSkillLine(rootName, rootSkill, 4, opts) + + for (const sub of children.get(rootName) ?? []) { + const childName = sub.name.slice(sub.name.indexOf('/') + 1) + printSkillLine(childName, sub, 6, opts) + } + } +} + +export function computeSkillNameWidth( + allPackageSkills: SkillDisplay[][], +): number { + let max = 0 + for (const skills of allPackageSkills) { + for (const s of skills) { + const slashIdx = s.name.indexOf('/') + const displayName = slashIdx === -1 ? s.name : s.name.slice(slashIdx + 1) + const indent = slashIdx === -1 ? 4 : 6 + max = Math.max(max, indent + displayName.length) + } + } + return max + 2 +} diff --git a/packages/intent/src/feedback.ts b/packages/intent/src/feedback.ts index eb721d7..a0c9d85 100644 --- a/packages/intent/src/feedback.ts +++ b/packages/intent/src/feedback.ts @@ -1,4 +1,4 @@ -import { execSync } from 'node:child_process' +import { execFileSync, execSync } from 'node:child_process' import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import type { @@ -302,8 +302,9 @@ export function submitFeedback( if (opts.ghAvailable) { try { const title = `Skill Feedback: ${payload.skill} (${payload.userRating})` - execSync( - `gh issue create --repo ${repo} --title "${title.replace(/"/g, '\\"')}" --body -`, + execFileSync( + 'gh', + ['issue', 'create', '--repo', repo, '--title', title, '--body', '-'], { input: md, stdio: ['pipe', 'pipe', 'pipe'] }, ) return { method: 'gh', detail: `Submitted issue to ${repo}` } @@ -337,8 +338,20 @@ export function submitMetaFeedback( if (opts.ghAvailable) { try { const title = `Meta-Skill Feedback: ${payload.metaSkill} (${payload.userRating})` - execSync( - `gh issue create --repo ${META_FEEDBACK_REPO} --title "${title.replace(/"/g, '\\"')}" --label "feedback:${payload.metaSkill}" --body -`, + execFileSync( + 'gh', + [ + 'issue', + 'create', + '--repo', + META_FEEDBACK_REPO, + '--title', + title, + '--label', + `feedback:${payload.metaSkill}`, + '--body', + '-', + ], { input: md, stdio: ['pipe', 'pipe', 'pipe'] }, ) return { diff --git a/packages/intent/src/index.ts b/packages/intent/src/index.ts index 3bd4eed..3d22fde 100644 --- a/packages/intent/src/index.ts +++ b/packages/intent/src/index.ts @@ -11,14 +11,6 @@ export { validateMetaPayload, validatePayload, } from './feedback.js' -export { - detectAgentConfigs, - hasIntentBlock, - injectIntentBlock, - readProjectConfig, - runInit, - writeProjectConfig, -} from './init.js' export { findSkillFiles, parseFrontmatter } from './utils.js' export { runSetup } from './setup.js' export type { diff --git a/packages/intent/src/init.ts b/packages/intent/src/init.ts deleted file mode 100644 index 24a4e95..0000000 --- a/packages/intent/src/init.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { existsSync, readFileSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' -import type { IntentProjectConfig } from './types.js' - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const AGENT_CONFIG_FILES = [ - 'AGENTS.md', - 'CLAUDE.md', - '.cursorrules', - '.github/copilot-instructions.md', -] as const - -const INTENT_BLOCK_MARKER = '## Intent Skills' - -const INTENT_BLOCK = `## Intent Skills - -This project uses TanStack Intent. Run \`npx intent list\` to discover -available AI coding skills. Before working with a library that has skills, -read the relevant SKILL.md file at the path shown in the list output. -After completing a task that used skills, collect and submit feedback -following the instructions in the list output. -` - -const DEFAULT_CONFIG: IntentProjectConfig = { - feedback: { - frequency: 'every-5', - }, -} - -// --------------------------------------------------------------------------- -// Detection -// --------------------------------------------------------------------------- - -export function detectAgentConfigs(root: string): string[] { - return AGENT_CONFIG_FILES.map((f) => join(root, f)).filter((f) => - existsSync(f), - ) -} - -// --------------------------------------------------------------------------- -// Injection -// --------------------------------------------------------------------------- - -export function hasIntentBlock(filePath: string): boolean { - try { - const content = readFileSync(filePath, 'utf8') - return content.includes(INTENT_BLOCK_MARKER) - } catch { - return false - } -} - -export function injectIntentBlock(filePath: string): boolean { - if (hasIntentBlock(filePath)) return false - - let content: string - try { - content = readFileSync(filePath, 'utf8') - } catch { - content = '' - } - - const separator = - content.length > 0 && !content.endsWith('\n\n') ? '\n\n' : '' - const updated = content + separator + INTENT_BLOCK - writeFileSync(filePath, updated) - return true -} - -// --------------------------------------------------------------------------- -// Config -// --------------------------------------------------------------------------- - -export function writeProjectConfig(root: string): string { - const configPath = join(root, 'intent.config.json') - if (!existsSync(configPath)) { - writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n') - } - return configPath -} - -export function readProjectConfig(root: string): IntentProjectConfig | null { - const configPath = join(root, 'intent.config.json') - try { - return JSON.parse(readFileSync(configPath, 'utf8')) as IntentProjectConfig - } catch { - return null - } -} - -// --------------------------------------------------------------------------- -// Main init command -// --------------------------------------------------------------------------- - -export interface InitResult { - injected: string[] - skipped: string[] - created: string[] - configPath: string -} - -export function runInit(root: string): InitResult { - const detected = detectAgentConfigs(root) - const injected: string[] = [] - const skipped: string[] = [] - const created: string[] = [] - - for (const filePath of detected) { - if (injectIntentBlock(filePath)) { - injected.push(filePath) - } else { - skipped.push(filePath) - } - } - - const configPath = writeProjectConfig(root) - - return { injected, skipped, created, configPath } -} diff --git a/packages/intent/src/intent-library.ts b/packages/intent/src/intent-library.ts index 6052f43..a7cd48f 100644 --- a/packages/intent/src/intent-library.ts +++ b/packages/intent/src/intent-library.ts @@ -1,103 +1,12 @@ #!/usr/bin/env node +import { + computeSkillNameWidth, + printSkillTree, + printTable, +} from './display.js' import type { LibraryScanResult } from './library-scanner.js' import { scanLibrary } from './library-scanner.js' -// --------------------------------------------------------------------------- -// Display helpers -// --------------------------------------------------------------------------- - -function padColumn(text: string, width: number): string { - return text.length >= width ? text + ' ' : text.padEnd(width) -} - -function printTable(headers: string[], rows: string[][]): void { - const widths = headers.map( - (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)) + 2, - ) - - const headerLine = headers.map((h, i) => padColumn(h, widths[i]!)).join('') - const separator = widths.map((w) => '─'.repeat(w)).join('') - - console.log(headerLine) - console.log(separator) - for (const row of rows) { - console.log(row.map((cell, i) => padColumn(cell, widths[i]!)).join('')) - } -} - -interface SkillDisplay { - name: string - description: string - type?: string - path?: string -} - -function printSkillTree( - skills: SkillDisplay[], - opts: { nameWidth: number; showTypes: boolean }, -): void { - const roots: string[] = [] - const children = new Map() - - for (const skill of skills) { - const slashIdx = skill.name.indexOf('/') - if (slashIdx === -1) { - roots.push(skill.name) - } else { - const parent = skill.name.slice(0, slashIdx) - if (!children.has(parent)) children.set(parent, []) - children.get(parent)!.push(skill) - } - } - - if (roots.length === 0) { - for (const skill of skills) { - if (!roots.includes(skill.name)) roots.push(skill.name) - } - } - - for (const rootName of roots) { - const rootSkill = skills.find((s) => s.name === rootName) - if (!rootSkill) continue - - printSkillLine(rootName, rootSkill, 4, opts) - - for (const sub of children.get(rootName) ?? []) { - const childName = sub.name.slice(sub.name.indexOf('/') + 1) - printSkillLine(childName, sub, 6, opts) - } - } -} - -function printSkillLine( - displayName: string, - skill: SkillDisplay, - indent: number, - opts: { nameWidth: number; showTypes: boolean }, -): void { - const nameStr = ' '.repeat(indent) + displayName - const padding = ' '.repeat(Math.max(2, opts.nameWidth - nameStr.length)) - const typeCol = opts.showTypes - ? (skill.type ? `[${skill.type}]` : '').padEnd(14) - : '' - console.log(`${nameStr}${padding}${typeCol}${skill.description}`) - if (skill.path) { - console.log(`${' '.repeat(indent + 2)}${skill.path}`) - } -} - -function computeSkillNameWidth(allPackageSkills: SkillDisplay[][]): number { - let max = 0 - for (const skills of allPackageSkills) { - for (const s of skills) { - const slashIdx = s.name.indexOf('/') - const displayName = slashIdx === -1 ? s.name : s.name.slice(slashIdx + 1) - const indent = slashIdx === -1 ? 4 : 6 - max = Math.max(max, indent + displayName.length) - } - } - return max + 2 -} // --------------------------------------------------------------------------- // Commands diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index c609137..f1a50e8 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -1,4 +1,3 @@ -import { execSync } from 'node:child_process' import { existsSync, mkdirSync, @@ -7,7 +6,6 @@ import { writeFileSync, } from 'node:fs' import { join } from 'node:path' -import { findSkillFiles, parseFrontmatter } from './utils.js' // --------------------------------------------------------------------------- // Types @@ -17,7 +15,6 @@ export interface SetupResult { workflows: string[] skipped: string[] shim: string | null - labels: string[] } interface TemplateVars { @@ -125,63 +122,41 @@ await import('@tanstack/intent/intent-library') ` } -function generateShim(root: string, result: SetupResult): void { - // Check if either extension already exists +function detectShimExtension(root: string): string { + try { + const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) + if (pkg.type === 'module') return 'js' + } catch { + // default to .mjs when package.json is unreadable + } + return 'mjs' +} + +function findExistingShim(root: string): string | null { const shimJs = join(root, 'bin', 'intent.js') + if (existsSync(shimJs)) return shimJs + const shimMjs = join(root, 'bin', 'intent.mjs') + if (existsSync(shimMjs)) return shimMjs - if (existsSync(shimJs) || existsSync(shimMjs)) { - result.skipped.push(existsSync(shimJs) ? shimJs : shimMjs) - return - } + return null +} - // Use .js if package has "type": "module", otherwise .mjs - let ext = 'mjs' - const pkgPath = join(root, 'package.json') - try { - const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) - if (pkg.type === 'module') ext = 'js' - } catch { - // default to .mjs +function generateShim(root: string, result: SetupResult): void { + const existingShim = findExistingShim(root) + + if (existingShim) { + result.skipped.push(existingShim) + return } + const ext = detectShimExtension(root) const shimPath = join(root, 'bin', `intent.${ext}`) mkdirSync(join(root, 'bin'), { recursive: true }) writeFileSync(shimPath, getShimContent(ext)) result.shim = shimPath } -// --------------------------------------------------------------------------- -// Label creation -// --------------------------------------------------------------------------- - -function createSkillLabels( - root: string, - repo: string, - result: SetupResult, -): void { - const skillsDir = join(root, 'skills') - if (!existsSync(skillsDir)) return - - const skillFiles = findSkillFiles(skillsDir) - for (const filePath of skillFiles) { - const fm = parseFrontmatter(filePath) - const name = typeof fm?.name === 'string' ? fm.name : null - if (!name) continue - - const label = `feedback:${name}` - try { - execSync( - `gh label create "${label}" --repo ${repo} --description "Feedback on the ${name} skill" --color c5def5`, - { stdio: ['pipe', 'pipe', 'pipe'] }, - ) - result.labels.push(label) - } catch { - // Label likely already exists - } - } -} - // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -194,19 +169,16 @@ export function runSetup( const doAll = args.includes('--all') const doWorkflows = doAll || args.includes('--workflows') const doShim = doAll || args.includes('--shim') - const doLabels = doAll || args.includes('--labels') - // If no flags, default to --all - const defaultAll = !doWorkflows && !doShim && !doLabels - const installWorkflows = doWorkflows || defaultAll - const installShim = doShim || defaultAll + const noFlagsGiven = !doWorkflows && !doShim + const installWorkflows = doWorkflows || noFlagsGiven + const installShim = doShim || noFlagsGiven const vars = detectVars(root) const result: SetupResult = { workflows: [], skipped: [], shim: null, - labels: [], } const templatesDir = join(metaDir, 'templates') @@ -223,10 +195,6 @@ export function runSetup( generateShim(root, result) } - if (doLabels) { - createSkillLabels(root, vars.REPO, result) - } - // Print results for (const f of result.workflows) console.log(`✓ Copied workflow: ${f}`) for (const f of result.skipped) console.log(` Already exists: ${f}`) @@ -239,16 +207,9 @@ export function runSetup( console.log(`\n Add "bin" to your package.json "files" array.`) } - if (result.labels.length > 0) { - console.log( - `✓ Created ${result.labels.length} feedback labels on ${vars.REPO}`, - ) - } - if ( result.workflows.length === 0 && result.shim === null && - result.labels.length === 0 && result.skipped.length === 0 ) { console.log('No templates directory found. Is @tanstack/intent installed?') diff --git a/packages/intent/tests/init.test.ts b/packages/intent/tests/init.test.ts deleted file mode 100644 index 4405670..0000000 --- a/packages/intent/tests/init.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { - existsSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} from 'node:fs' -import { join } from 'node:path' -import { tmpdir } from 'node:os' -import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { - detectAgentConfigs, - hasIntentBlock, - injectIntentBlock, - readProjectConfig, - runInit, - writeProjectConfig, -} from '../src/init.js' - -let root: string - -beforeEach(() => { - root = mkdtempSync(join(tmpdir(), 'init-test-')) -}) - -afterEach(() => { - rmSync(root, { recursive: true, force: true }) -}) - -describe('detectAgentConfigs', () => { - it('detects AGENTS.md', () => { - writeFileSync(join(root, 'AGENTS.md'), '# Agents') - const found = detectAgentConfigs(root) - expect(found).toHaveLength(1) - expect(found[0]).toContain('AGENTS.md') - }) - - it('detects CLAUDE.md', () => { - writeFileSync(join(root, 'CLAUDE.md'), '# Claude') - const found = detectAgentConfigs(root) - expect(found).toHaveLength(1) - expect(found[0]).toContain('CLAUDE.md') - }) - - it('detects .cursorrules', () => { - writeFileSync(join(root, '.cursorrules'), 'rules') - const found = detectAgentConfigs(root) - expect(found).toHaveLength(1) - expect(found[0]).toContain('.cursorrules') - }) - - it('detects multiple configs', () => { - writeFileSync(join(root, 'AGENTS.md'), '') - writeFileSync(join(root, 'CLAUDE.md'), '') - const found = detectAgentConfigs(root) - expect(found).toHaveLength(2) - }) - - it('returns empty when no configs found', () => { - const found = detectAgentConfigs(root) - expect(found).toHaveLength(0) - }) -}) - -describe('injectIntentBlock', () => { - it('injects block into existing file', () => { - const filePath = join(root, 'AGENTS.md') - writeFileSync(filePath, '# Agent Instructions\n\nSome existing content.\n') - - const injected = injectIntentBlock(filePath) - expect(injected).toBe(true) - - const content = readFileSync(filePath, 'utf8') - expect(content).toContain('## Intent Skills') - expect(content).toContain('npx intent list') - expect(content).toContain('Some existing content.') - }) - - it('is idempotent — skips if block already present', () => { - const filePath = join(root, 'AGENTS.md') - writeFileSync(filePath, '# Agent\n\n## Intent Skills\n\nAlready here.\n') - - const injected = injectIntentBlock(filePath) - expect(injected).toBe(false) - }) - - it('works on empty file', () => { - const filePath = join(root, 'CLAUDE.md') - writeFileSync(filePath, '') - - const injected = injectIntentBlock(filePath) - expect(injected).toBe(true) - - const content = readFileSync(filePath, 'utf8') - expect(content).toContain('## Intent Skills') - }) -}) - -describe('hasIntentBlock', () => { - it('returns true when block exists', () => { - const filePath = join(root, 'test.md') - writeFileSync(filePath, '## Intent Skills\n\nContent.') - expect(hasIntentBlock(filePath)).toBe(true) - }) - - it('returns false when block missing', () => { - const filePath = join(root, 'test.md') - writeFileSync(filePath, '# No intent here') - expect(hasIntentBlock(filePath)).toBe(false) - }) - - it('returns false for non-existent file', () => { - expect(hasIntentBlock(join(root, 'nope.md'))).toBe(false) - }) -}) - -describe('writeProjectConfig', () => { - it('creates intent.config.json with defaults', () => { - const configPath = writeProjectConfig(root) - expect(existsSync(configPath)).toBe(true) - - const config = JSON.parse(readFileSync(configPath, 'utf8')) - expect(config.feedback.frequency).toBe('every-5') - }) - - it('does not overwrite existing config', () => { - const configPath = join(root, 'intent.config.json') - writeFileSync( - configPath, - JSON.stringify({ feedback: { frequency: 'never' } }), - ) - - writeProjectConfig(root) - const config = JSON.parse(readFileSync(configPath, 'utf8')) - expect(config.feedback.frequency).toBe('never') - }) -}) - -describe('readProjectConfig', () => { - it('reads existing config', () => { - writeFileSync( - join(root, 'intent.config.json'), - JSON.stringify({ feedback: { frequency: 'always' } }), - ) - const config = readProjectConfig(root) - expect(config?.feedback.frequency).toBe('always') - }) - - it('returns null when no config exists', () => { - expect(readProjectConfig(root)).toBeNull() - }) -}) - -describe('runInit', () => { - it('injects into all detected configs and creates project config', () => { - writeFileSync(join(root, 'AGENTS.md'), '# Agents\n') - writeFileSync(join(root, 'CLAUDE.md'), '# Claude\n') - - const result = runInit(root) - - expect(result.injected).toHaveLength(2) - expect(result.skipped).toHaveLength(0) - expect(existsSync(result.configPath)).toBe(true) - - // Verify injection happened - expect(readFileSync(join(root, 'AGENTS.md'), 'utf8')).toContain( - '## Intent Skills', - ) - expect(readFileSync(join(root, 'CLAUDE.md'), 'utf8')).toContain( - '## Intent Skills', - ) - }) - - it('skips already-initialized files', () => { - writeFileSync( - join(root, 'AGENTS.md'), - '## Intent Skills\n\nAlready done.\n', - ) - - const result = runInit(root) - expect(result.injected).toHaveLength(0) - expect(result.skipped).toHaveLength(1) - }) - - it('handles mixed state (some initialized, some not)', () => { - writeFileSync(join(root, 'AGENTS.md'), '## Intent Skills\n\nDone.\n') - writeFileSync(join(root, 'CLAUDE.md'), '# Fresh\n') - - const result = runInit(root) - expect(result.injected).toHaveLength(1) - expect(result.skipped).toHaveLength(1) - }) -}) From b59340a222b7767ab89bf67832f990dfdb73070b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:11:16 +0000 Subject: [PATCH 18/19] ci: apply automated fixes --- docs/intent/functions/detectAgentConfigs.md | 22 --------------------- docs/intent/functions/hasIntentBlock.md | 22 --------------------- docs/intent/functions/injectIntentBlock.md | 22 --------------------- docs/intent/functions/readProjectConfig.md | 22 --------------------- docs/intent/functions/runInit.md | 22 --------------------- docs/intent/functions/runSetup.md | 2 +- docs/intent/functions/submitMetaFeedback.md | 2 +- docs/intent/functions/writeProjectConfig.md | 22 --------------------- docs/intent/index.md | 6 ------ packages/intent/README.md | 18 ++++++++--------- packages/intent/src/cli.ts | 10 ++-------- packages/intent/src/intent-library.ts | 6 +----- 12 files changed, 14 insertions(+), 162 deletions(-) delete mode 100644 docs/intent/functions/detectAgentConfigs.md delete mode 100644 docs/intent/functions/hasIntentBlock.md delete mode 100644 docs/intent/functions/injectIntentBlock.md delete mode 100644 docs/intent/functions/readProjectConfig.md delete mode 100644 docs/intent/functions/runInit.md delete mode 100644 docs/intent/functions/writeProjectConfig.md diff --git a/docs/intent/functions/detectAgentConfigs.md b/docs/intent/functions/detectAgentConfigs.md deleted file mode 100644 index 10d1946..0000000 --- a/docs/intent/functions/detectAgentConfigs.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: detectAgentConfigs -title: detectAgentConfigs ---- - -# Function: detectAgentConfigs() - -```ts -function detectAgentConfigs(root): string[]; -``` - -Defined in: [init.ts:37](https://github.com/TanStack/intent/blob/main/packages/intent/src/init.ts#L37) - -## Parameters - -### root - -`string` - -## Returns - -`string`[] diff --git a/docs/intent/functions/hasIntentBlock.md b/docs/intent/functions/hasIntentBlock.md deleted file mode 100644 index 8b25a0d..0000000 --- a/docs/intent/functions/hasIntentBlock.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: hasIntentBlock -title: hasIntentBlock ---- - -# Function: hasIntentBlock() - -```ts -function hasIntentBlock(filePath): boolean; -``` - -Defined in: [init.ts:47](https://github.com/TanStack/intent/blob/main/packages/intent/src/init.ts#L47) - -## Parameters - -### filePath - -`string` - -## Returns - -`boolean` diff --git a/docs/intent/functions/injectIntentBlock.md b/docs/intent/functions/injectIntentBlock.md deleted file mode 100644 index a25a9fc..0000000 --- a/docs/intent/functions/injectIntentBlock.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: injectIntentBlock -title: injectIntentBlock ---- - -# Function: injectIntentBlock() - -```ts -function injectIntentBlock(filePath): boolean; -``` - -Defined in: [init.ts:56](https://github.com/TanStack/intent/blob/main/packages/intent/src/init.ts#L56) - -## Parameters - -### filePath - -`string` - -## Returns - -`boolean` diff --git a/docs/intent/functions/readProjectConfig.md b/docs/intent/functions/readProjectConfig.md deleted file mode 100644 index a83042c..0000000 --- a/docs/intent/functions/readProjectConfig.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: readProjectConfig -title: readProjectConfig ---- - -# Function: readProjectConfig() - -```ts -function readProjectConfig(root): IntentProjectConfig; -``` - -Defined in: [init.ts:85](https://github.com/TanStack/intent/blob/main/packages/intent/src/init.ts#L85) - -## Parameters - -### root - -`string` - -## Returns - -[`IntentProjectConfig`](../interfaces/IntentProjectConfig.md) diff --git a/docs/intent/functions/runInit.md b/docs/intent/functions/runInit.md deleted file mode 100644 index 9f69c0b..0000000 --- a/docs/intent/functions/runInit.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: runInit -title: runInit ---- - -# Function: runInit() - -```ts -function runInit(root): InitResult; -``` - -Defined in: [init.ts:105](https://github.com/TanStack/intent/blob/main/packages/intent/src/init.ts#L105) - -## Parameters - -### root - -`string` - -## Returns - -`InitResult` diff --git a/docs/intent/functions/runSetup.md b/docs/intent/functions/runSetup.md index dfc3129..bbfe125 100644 --- a/docs/intent/functions/runSetup.md +++ b/docs/intent/functions/runSetup.md @@ -12,7 +12,7 @@ function runSetup( args): SetupResult; ``` -Defined in: [setup.ts:189](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L189) +Defined in: [setup.ts:164](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L164) ## Parameters diff --git a/docs/intent/functions/submitMetaFeedback.md b/docs/intent/functions/submitMetaFeedback.md index d0dc750..4a261fa 100644 --- a/docs/intent/functions/submitMetaFeedback.md +++ b/docs/intent/functions/submitMetaFeedback.md @@ -9,7 +9,7 @@ title: submitMetaFeedback function submitMetaFeedback(payload, opts): SubmitResult; ``` -Defined in: [feedback.ts:331](https://github.com/TanStack/intent/blob/main/packages/intent/src/feedback.ts#L331) +Defined in: [feedback.ts:332](https://github.com/TanStack/intent/blob/main/packages/intent/src/feedback.ts#L332) ## Parameters diff --git a/docs/intent/functions/writeProjectConfig.md b/docs/intent/functions/writeProjectConfig.md deleted file mode 100644 index b95c50d..0000000 --- a/docs/intent/functions/writeProjectConfig.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: writeProjectConfig -title: writeProjectConfig ---- - -# Function: writeProjectConfig() - -```ts -function writeProjectConfig(root): string; -``` - -Defined in: [init.ts:77](https://github.com/TanStack/intent/blob/main/packages/intent/src/init.ts#L77) - -## Parameters - -### root - -`string` - -## Returns - -`string` diff --git a/docs/intent/index.md b/docs/intent/index.md index fd85092..8affefe 100644 --- a/docs/intent/index.md +++ b/docs/intent/index.md @@ -26,16 +26,11 @@ title: "@tanstack/intent" - [checkStaleness](functions/checkStaleness.md) - [containsSecrets](functions/containsSecrets.md) -- [detectAgentConfigs](functions/detectAgentConfigs.md) - [findSkillFiles](functions/findSkillFiles.md) - [hasGhCli](functions/hasGhCli.md) -- [hasIntentBlock](functions/hasIntentBlock.md) -- [injectIntentBlock](functions/injectIntentBlock.md) - [metaToMarkdown](functions/metaToMarkdown.md) - [parseFrontmatter](functions/parseFrontmatter.md) -- [readProjectConfig](functions/readProjectConfig.md) - [resolveFrequency](functions/resolveFrequency.md) -- [runInit](functions/runInit.md) - [runSetup](functions/runSetup.md) - [scanForIntents](functions/scanForIntents.md) - [submitFeedback](functions/submitFeedback.md) @@ -43,4 +38,3 @@ title: "@tanstack/intent" - [toMarkdown](functions/toMarkdown.md) - [validateMetaPayload](functions/validateMetaPayload.md) - [validatePayload](functions/validatePayload.md) -- [writeProjectConfig](functions/writeProjectConfig.md) diff --git a/packages/intent/README.md b/packages/intent/README.md index 0303ac8..153aad4 100644 --- a/packages/intent/README.md +++ b/packages/intent/README.md @@ -50,15 +50,15 @@ npx intent setup ## CLI Commands -| Command | Description | -| ----------------------- | ---------------------------------------------------- | -| `intent install` | Set up skill-to-task mappings in agent config files | -| `intent list [--json]` | Discover intent-enabled packages | -| `intent meta` | List meta-skills for library maintainers | -| `intent scaffold` | Print the guided skill generation prompt | -| `intent validate [dir]` | Validate SKILL.md files | -| `intent setup` | Copy CI templates, generate shim, create labels | -| `intent stale [--json]` | Check skills for version drift | +| Command | Description | +| ----------------------- | --------------------------------------------------- | +| `intent install` | Set up skill-to-task mappings in agent config files | +| `intent list [--json]` | Discover intent-enabled packages | +| `intent meta` | List meta-skills for library maintainers | +| `intent scaffold` | Print the guided skill generation prompt | +| `intent validate [dir]` | Validate SKILL.md files | +| `intent setup` | Copy CI templates, generate shim, create labels | +| `intent stale [--json]` | Check skills for version drift | ## License diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 5198730..5cc66d2 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -4,11 +4,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs' import { dirname, join, relative, sep } from 'node:path' import { fileURLToPath } from 'node:url' import { parse as parseYaml } from 'yaml' -import { - computeSkillNameWidth, - printSkillTree, - printTable, -} from './display.js' +import { computeSkillNameWidth, printSkillTree, printTable } from './display.js' import { scanForIntents } from './scanner.js' import type { ScanResult } from './types.js' import { findSkillFiles, parseFrontmatter } from './utils.js' @@ -143,9 +139,7 @@ function collectPackagingWarnings(root: string): string[] { const warnings: string[] = [] - const devDeps = pkgJson.devDependencies as - | Record - | undefined + const devDeps = pkgJson.devDependencies as Record | undefined if (!devDeps?.['@tanstack/intent']) { warnings.push('@tanstack/intent is not in devDependencies') } diff --git a/packages/intent/src/intent-library.ts b/packages/intent/src/intent-library.ts index a7cd48f..deeafca 100644 --- a/packages/intent/src/intent-library.ts +++ b/packages/intent/src/intent-library.ts @@ -1,10 +1,6 @@ #!/usr/bin/env node -import { - computeSkillNameWidth, - printSkillTree, - printTable, -} from './display.js' +import { computeSkillNameWidth, printSkillTree, printTable } from './display.js' import type { LibraryScanResult } from './library-scanner.js' import { scanLibrary } from './library-scanner.js' From 3507f58f3837aeeeb20d4a906f9bd4ee405d0874 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Tue, 3 Mar 2026 20:12:49 -0700 Subject: [PATCH 19/19] fix: unexport internal display helpers to pass knip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit padColumn and printSkillLine are only used within display.ts — they don't need to be exported. Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/display.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/intent/src/display.ts b/packages/intent/src/display.ts index 3e6823e..1ace419 100644 --- a/packages/intent/src/display.ts +++ b/packages/intent/src/display.ts @@ -9,7 +9,7 @@ export interface SkillDisplay { path?: string } -export function padColumn(text: string, width: number): string { +function padColumn(text: string, width: number): string { return text.length >= width ? text + ' ' : text.padEnd(width) } @@ -28,7 +28,7 @@ export function printTable(headers: string[], rows: string[][]): void { } } -export function printSkillLine( +function printSkillLine( displayName: string, skill: SkillDisplay, indent: number,