From d5fb266f6b7ddb6b201c407a31588751f87cf00d Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 5 Mar 2026 16:22:40 -0700 Subject: [PATCH 1/6] feat: split setup into focused commands and improve meta skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `intent setup` with three focused commands: - `intent add-library-bin` — generates the bin/intent bridge file - `intent edit-package-json` — wires package.json for skill publishing - `intent setup-github-actions` — copies CI workflow templates Improve meta skill SKILL.md files based on real scaffolding feedback: - Soften lightweight path threshold and narrative guide reading - Add companion library and experimental features questions - Add YAML validation step in domain discovery - Add subagent guidance and review cadence preference in generate-skill - Replace inline feedback sections with pointer to feedback-collection - Update tree-generator to reference edit-package-json command Co-Authored-By: Claude Opus 4.6 --- .../intent/meta/domain-discovery/SKILL.md | 70 ++---- .../intent/meta/feedback-collection/SKILL.md | 6 + packages/intent/meta/generate-skill/SKILL.md | 61 ++--- packages/intent/meta/tree-generator/SKILL.md | 48 +--- packages/intent/src/cli.ts | 33 ++- packages/intent/src/index.ts | 11 +- packages/intent/src/setup.ts | 178 ++++++++++---- packages/intent/tests/setup.test.ts | 229 +++++++++++++++--- 8 files changed, 411 insertions(+), 225 deletions(-) diff --git a/packages/intent/meta/domain-discovery/SKILL.md b/packages/intent/meta/domain-discovery/SKILL.md index 35480741..07f85eaf 100644 --- a/packages/intent/meta/domain-discovery/SKILL.md +++ b/packages/intent/meta/domain-discovery/SKILL.md @@ -42,9 +42,12 @@ path applies (see below). ### Lightweight path (small libraries) -After Phase 1, if the library has **fewer than 5 client-facing skill -areas** (e.g. a focused utility library, a single-purpose tool, or a -library with only 2–3 distinct developer tasks), use a compressed flow: +After Phase 1, decide whether the library warrants the full five-phase +flow or the compressed flow below. This is a judgment call — lean toward +full discovery unless the library is obviously small (single-purpose +utility, 2–3 distinct developer tasks max). Use a compressed flow when +the skill surface is small enough that two interview rounds would be +redundant: 1. **Phase 1** — Quick scan (same as full flow) 2. **Phase 2+4 combined** — Single interview round. Combine the @@ -144,13 +147,22 @@ Surface lifecycle/journey skills that cross-cut task areas: > a migrate-from-v4 walkthrough. Which of these exist in your docs > or would be valuable as standalone skills?" -### 2c — Composition and ecosystem (1–2 questions) +### 2c — Composition and ecosystem (1–3 questions) > "Which other libraries does yours compose with most often? Are there > integration patterns important enough to warrant their own skill — > for example, using your library with [framework/ORM/router]?" -### 2d — Confirm initial skill map +> "Are there tasks that developers might expect your library to handle, +> but that are actually handled by a companion library? Which tasks +> should we explicitly exclude from your library's skills?" + +### 2d — Exclude experimental features (1 question) + +> "Are there any features that are experimental, unstable, or not yet +> ready to document for agents? We'll exclude these from the skill set." + +### 2e — Confirm initial skill map Synthesize what you heard into a proposed skill list and present it: @@ -170,7 +182,10 @@ gotchas. Read in this order. Each step builds context for the next. -1. **Every narrative guide** — the how-to content, not API reference tables +1. **Narrative guides** — read as many as needed to build confidence in + your understanding. Prioritize getting-started, migration, and guides + covering the skill areas from Phase 2. Skip exhaustive reading of large + online-only doc sets. 2. **Migration guides** — highest-yield source for failure modes; every breaking change is exactly what agents trained on older versions produce 3. **API reference** — scan for exports, type signatures, option shapes @@ -538,6 +553,10 @@ Merge interview findings into the draft. For each interview answer: 4. If it reveals a new skill — add it 5. If it fills a gap — remove from gaps section +Validate the domain_map.yaml by parsing it with a YAML parser. Check for +duplicate keys, invalid syntax, and structural correctness. Fix any issues +before presenting the final artifact. + Update `status: draft` to `status: reviewed`. --- @@ -743,42 +762,3 @@ Gemini, and open-source models. To achieve this: phase separation - No model-specific features (no XML tags in output, no tool_use assumptions) ---- - -## Meta-skill feedback (alpha default) - -After finalizing artifacts, create or update a single Markdown feedback draft -for all three meta skills used in scaffolding. Use this exact template and -fill only the **Domain Discovery** section now. Leave the other sections in -place for later steps to complete. - -``` -# Intent Meta Skill Feedback - -## Domain Discovery -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Tree Generator -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Generate Skill -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Context (optional) -- Library: -- Repo: -- Docs: -- Notes: -``` - -Do not submit feedback yet. Tell the maintainer to carry this draft forward -to the next meta skill step. diff --git a/packages/intent/meta/feedback-collection/SKILL.md b/packages/intent/meta/feedback-collection/SKILL.md index d3704a74..9494cee9 100644 --- a/packages/intent/meta/feedback-collection/SKILL.md +++ b/packages/intent/meta/feedback-collection/SKILL.md @@ -17,6 +17,12 @@ Run this at the end of any session where you loaded one or more SKILL.md files. The goal is to capture what worked, what didn't, and what was missing — so skill maintainers can improve future versions. +This skill also covers **meta-skill feedback** — feedback about the scaffolding +process itself. When invoked after running domain-discovery, tree-generator, and +generate-skill, treat those three meta skills as the "skills" being evaluated. +Capture what worked and what didn't in each scaffolding phase so the meta skills +can be improved. + --- ## Phase 1 — Automated Signal Collection diff --git a/packages/intent/meta/generate-skill/SKILL.md b/packages/intent/meta/generate-skill/SKILL.md index fb69eaae..3ee32fd5 100644 --- a/packages/intent/meta/generate-skill/SKILL.md +++ b/packages/intent/meta/generate-skill/SKILL.md @@ -79,6 +79,15 @@ skill-tree-generator for the full spec of each type. --- +### Subagent guidance for batch generation + +When generating multiple skills, spawn a separate subagent for each skill +(or per-package group). Each subagent receives the domain_map.yaml, +skill_tree.yaml, and the source docs relevant to its skill. This prevents +context bleed between skills and allows parallel generation. + +--- + ## Step 2 — Extract content from sources **Line budget:** Each SKILL.md must stay under 500 lines. Before writing, @@ -319,6 +328,10 @@ Run every check before outputting. Fix any failures. ## Step 6 — Output +Before generating, ask the maintainer: "Would you like to review each skill +individually before I generate the next one, or should I generate all skills +and you review them together?" Respect their preference. + Output the complete SKILL.md file content. If reference files are needed, output those as well with their relative paths. @@ -381,48 +394,8 @@ Output is consumed by all major AI coding agents. To ensure consistency: --- -## Meta-skill feedback (alpha default) - -After generating the first iteration of skills, complete a single Markdown -feedback draft for all three meta skills used in scaffolding. If a draft -exists from previous steps, keep it and fill the **Generate Skill** section. -If no draft exists, create it using this exact template. - -``` - -# Intent Meta Skill Feedback - -## Domain Discovery +## Meta-skill feedback -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Tree Generator - -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Generate Skill - -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Context (optional) - -- Library: -- Repo: -- Docs: -- Notes: - -``` - -Ask the maintainer if they want to edit the feedback. Then ask if you should -send it as a GitHub issue to TanStack/intent. Use the issue title: -`[meta-feedback] intent meta skill`. Only submit if they confirm. -``` +After generating all skills, run the `skill-feedback-collection` skill to +capture feedback about the scaffolding process (domain-discovery, +tree-generator, and generate-skill). diff --git a/packages/intent/meta/tree-generator/SKILL.md b/packages/intent/meta/tree-generator/SKILL.md index 1954a705..c6a6e285 100644 --- a/packages/intent/meta/tree-generator/SKILL.md +++ b/packages/intent/meta/tree-generator/SKILL.md @@ -274,14 +274,9 @@ packages/ │ └── package.json # Add "skills" to files array ``` -Add `"skills"` to each package's `files` array in `package.json` so -skill files are included in the published npm tarball: - -```json -{ - "files": ["dist", "src", "skills"] -} -``` +Run `intent edit-package-json` to wire each package's `package.json` +automatically (adds `"skills"`, `"bin"`, and `"!skills/_artifacts"` to the +`files` array, and adds the `bin` entry if missing). ### Step 2 — Write the core skill @@ -853,40 +848,3 @@ When updating: --- -## Meta-skill feedback (alpha default) - -After producing the skill tree artifact, update a single Markdown feedback -draft for all three meta skills used in scaffolding. If a draft exists from -Domain Discovery, keep it and fill only the **Tree Generator** section. If -no draft exists, create it using this exact template. - -``` -# Intent Meta Skill Feedback - -## Domain Discovery -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Tree Generator -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Generate Skill -- What worked well: -- What was confusing or missing: -- Suggestions for improvement: -- Overall rating: good | mixed | bad - -## Context (optional) -- Library: -- Repo: -- Docs: -- Notes: -``` - -Do not submit feedback yet. Carry this draft forward to the generate-skill -step. diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 3a5fbe87..4285cb03 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -155,7 +155,7 @@ function collectPackagingWarnings(root: string): string[] { 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)', + 'No bin/intent.js or bin/intent.mjs shim found (run: npx @tanstack/intent add-library-bin)', ) } @@ -394,12 +394,11 @@ This produces: individual SKILL.md files. 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. 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\`" +3. For each publishable package, run: \`npx @tanstack/intent add-library-bin\` +4. For each publishable package, run: \`npx @tanstack/intent edit-package-json\` +5. Ensure each package has \`@tanstack/intent\` as a devDependency +6. Create a \`feedback:\` label on the GitHub repo for each skill (use \`gh label create\`) +7. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent install\`" ` console.log(prompt) @@ -417,7 +416,9 @@ 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] [--all] Copy CI templates and generate shim + intent add-library-bin Generate bin/intent.{js,mjs} bridge file + intent edit-package-json Wire package.json (files, bin) for skill publishing + intent setup-github-actions Copy CI workflow templates to .github/workflows/ intent stale Check skills for staleness` const command = process.argv[2] @@ -539,9 +540,19 @@ skills: } break } - case 'setup': { - const { runSetup } = await import('./setup.js') - runSetup(process.cwd(), getMetaDir(), commandArgs) + case 'add-library-bin': { + const { runAddLibraryBin } = await import('./setup.js') + runAddLibraryBin(process.cwd()) + break + } + case 'edit-package-json': { + const { runEditPackageJson } = await import('./setup.js') + runEditPackageJson(process.cwd()) + break + } + case 'setup-github-actions': { + const { runSetupGithubActions } = await import('./setup.js') + runSetupGithubActions(process.cwd(), getMetaDir()) break } default: diff --git a/packages/intent/src/index.ts b/packages/intent/src/index.ts index 3d22fde8..7a46e778 100644 --- a/packages/intent/src/index.ts +++ b/packages/intent/src/index.ts @@ -12,7 +12,16 @@ export { validatePayload, } from './feedback.js' export { findSkillFiles, parseFrontmatter } from './utils.js' -export { runSetup } from './setup.js' +export { + runAddLibraryBin, + runEditPackageJson, + runSetupGithubActions, +} from './setup.js' +export type { + AddLibraryBinResult, + EditPackageJsonResult, + SetupGithubActionsResult, +} from './setup.js' export type { AgentName, FeedbackPayload, diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index f1a50e83..cb5884af 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -11,10 +11,19 @@ import { join } from 'node:path' // Types // --------------------------------------------------------------------------- -export interface SetupResult { +export interface AddLibraryBinResult { + shim: string | null + skipped: string | null +} + +export interface EditPackageJsonResult { + added: string[] + alreadyPresent: string[] +} + +export interface SetupGithubActionsResult { workflows: string[] skipped: string[] - shim: string | null } interface TemplateVars { @@ -33,8 +42,12 @@ function detectVars(root: string): TemplateVars { let pkgJson: Record = {} try { pkgJson = JSON.parse(readFileSync(pkgPath, 'utf8')) - } catch { - /* fallback to defaults */ + } catch (err: unknown) { + const isNotFound = + err && typeof err === 'object' && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT' + if (!isNotFound) { + console.error(`Warning: could not read ${pkgPath}: ${err instanceof Error ? err.message : err}`) + } } const name = typeof pkgJson.name === 'string' ? pkgJson.name : 'unknown' @@ -109,7 +122,7 @@ function copyTemplates( } // --------------------------------------------------------------------------- -// Shim generation +// Shim generation helpers // --------------------------------------------------------------------------- function getShimContent(ext: string): string { @@ -126,8 +139,12 @@ 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 + } catch (err: unknown) { + const isNotFound = + err && typeof err === 'object' && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT' + if (!isNotFound) { + console.error(`Warning: could not read package.json: ${err instanceof Error ? err.message : err}`) + } } return 'mjs' } @@ -142,12 +159,18 @@ function findExistingShim(root: string): string | null { return null } -function generateShim(root: string, result: SetupResult): void { - const existingShim = findExistingShim(root) +// --------------------------------------------------------------------------- +// Command: add-library-bin +// --------------------------------------------------------------------------- +export function runAddLibraryBin(root: string): AddLibraryBinResult { + const result: AddLibraryBinResult = { shim: null, skipped: null } + + const existingShim = findExistingShim(root) if (existingShim) { - result.skipped.push(existingShim) - return + result.skipped = existingShim + console.log(` Already exists: ${existingShim}`) + return result } const ext = detectShimExtension(root) @@ -155,63 +178,118 @@ function generateShim(root: string, result: SetupResult): void { mkdirSync(join(root, 'bin'), { recursive: true }) writeFileSync(shimPath, getShimContent(ext)) result.shim = shimPath + + console.log(`✓ Generated intent shim: ${shimPath}`) + console.log(`\n Run \`npx @tanstack/intent edit-package-json\` to wire package.json.`) + + return result } // --------------------------------------------------------------------------- -// Main +// Command: edit-package-json // --------------------------------------------------------------------------- -export function runSetup( - root: string, - metaDir: string, - args: string[], -): SetupResult { - const doAll = args.includes('--all') - const doWorkflows = doAll || args.includes('--workflows') - const doShim = doAll || args.includes('--shim') +export function runEditPackageJson(root: string): EditPackageJsonResult { + const result: EditPackageJsonResult = { added: [], alreadyPresent: [] } + const pkgPath = join(root, 'package.json') - const noFlagsGiven = !doWorkflows && !doShim - const installWorkflows = doWorkflows || noFlagsGiven - const installShim = doShim || noFlagsGiven + if (!existsSync(pkgPath)) { + console.error('No package.json found in ' + root) + process.exitCode = 1 + return result + } - const vars = detectVars(root) - const result: SetupResult = { - workflows: [], - skipped: [], - shim: null, + const raw = readFileSync(pkgPath, 'utf8') + let pkg: Record + try { + pkg = JSON.parse(raw) as Record + } catch (err) { + const detail = err instanceof SyntaxError ? err.message : String(err) + console.error(`Failed to parse ${pkgPath}: ${detail}`) + return result } - const templatesDir = join(metaDir, 'templates') + // Detect indent size from existing file + const indentMatch = raw.match(/^(\s+)"/m) + const indentSize = indentMatch ? indentMatch[1].length : 2 - if (installWorkflows) { - const srcDir = join(templatesDir, 'workflows') - const destDir = join(root, '.github', 'workflows') - const { copied, skipped } = copyTemplates(srcDir, destDir, vars) - result.workflows = copied - result.skipped.push(...skipped) + // --- files array --- + if (!Array.isArray(pkg.files)) { + pkg.files = [] + } + const files = pkg.files as string[] + const requiredFiles = ['skills', 'bin', '!skills/_artifacts'] + + for (const entry of requiredFiles) { + if (files.includes(entry)) { + result.alreadyPresent.push(`files: "${entry}"`) + } else { + files.push(entry) + result.added.push(`files: "${entry}"`) + } } - if (installShim) { - generateShim(root, result) + // --- bin field --- + const existingShim = findExistingShim(root) + let ext: string + if (existingShim) { + ext = existingShim.endsWith('.mjs') ? 'mjs' : 'js' + } else { + ext = pkg.type === 'module' ? 'js' : 'mjs' + } + const shimRelative = `./bin/intent.${ext}` + + if (typeof pkg.bin === 'object' && pkg.bin !== null) { + const binObj = pkg.bin as Record + if (binObj.intent) { + result.alreadyPresent.push(`bin.intent`) + } else { + binObj.intent = shimRelative + result.added.push(`bin.intent: "${shimRelative}"`) + } + } else if (!pkg.bin) { + pkg.bin = { intent: shimRelative } + result.added.push(`bin.intent: "${shimRelative}"`) + } else if (typeof pkg.bin === 'string') { + // npm string shorthand: "bin": "./cli.js" means { "": "./cli.js" } + const pkgName = + typeof pkg.name === 'string' + ? pkg.name.replace(/^@[^/]+\//, '') + : 'unknown' + pkg.bin = { [pkgName]: pkg.bin, intent: shimRelative } + result.added.push(`bin.intent: "${shimRelative}" (converted bin from string to object)`) } + writeFileSync(pkgPath, JSON.stringify(pkg, null, indentSize) + '\n') + // Print results + for (const a of result.added) console.log(`✓ Added ${a}`) + for (const a of result.alreadyPresent) console.log(` Already present: ${a}`) + + return result +} + +// --------------------------------------------------------------------------- +// Command: setup-github-actions +// --------------------------------------------------------------------------- + +export function runSetupGithubActions( + root: string, + metaDir: string, +): SetupGithubActionsResult { + const vars = detectVars(root) + const result: SetupGithubActionsResult = { workflows: [], skipped: [] } + + const srcDir = join(metaDir, 'templates', 'workflows') + const destDir = join(root, '.github', 'workflows') + const { copied, skipped } = copyTemplates(srcDir, destDir, vars) + result.workflows = copied + result.skipped = skipped + for (const f of result.workflows) console.log(`✓ Copied workflow: ${f}`) 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": "${shimRelative}" }`) - console.log(`\n Add "bin" to your package.json "files" array.`) - } - - if ( - result.workflows.length === 0 && - result.shim === null && - result.skipped.length === 0 - ) { + if (result.workflows.length === 0 && result.skipped.length === 0) { console.log('No templates directory found. Is @tanstack/intent installed?') } else if (result.workflows.length > 0) { console.log(`\nTemplate variables applied:`) diff --git a/packages/intent/tests/setup.test.ts b/packages/intent/tests/setup.test.ts index 2144ea20..19686e1d 100644 --- a/packages/intent/tests/setup.test.ts +++ b/packages/intent/tests/setup.test.ts @@ -9,11 +9,23 @@ import { } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' -import { runSetup } from '../src/setup.js' +import { + runAddLibraryBin, + runEditPackageJson, + runSetupGithubActions, +} from '../src/setup.js' let root: string let metaDir: string +function writePkg(data: Record, indent?: number): void { + writeFileSync(join(root, 'package.json'), JSON.stringify(data, null, indent)) +} + +function readPkg(): Record { + return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) +} + beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'setup-test-')) metaDir = join(root, 'meta') @@ -31,28 +43,189 @@ afterEach(() => { rmSync(root, { recursive: true, force: true }) }) -describe('runSetup', () => { - it('copies workflow templates with defaults', () => { - const result = runSetup(root, metaDir, []) - expect(result.workflows).toHaveLength(1) - expect(result.skipped).toHaveLength(0) +describe('runAddLibraryBin', () => { + it('generates bin/intent.js for type:module packages', () => { + writePkg({ name: 'test-pkg', type: 'module' }) - const wfPath = join(root, '.github', 'workflows', 'notify-intent.yml') - expect(existsSync(wfPath)).toBe(true) - const content = readFileSync(wfPath, 'utf8') - expect(content).toContain('package: unknown') + const result = runAddLibraryBin(root) + expect(result.shim).toBe(join(root, 'bin', 'intent.js')) + expect(result.skipped).toBeNull() + expect(existsSync(join(root, 'bin', 'intent.js'))).toBe(true) + }) + + it('generates bin/intent.mjs for non-module packages', () => { + writePkg({ name: 'test-pkg' }) + + const result = runAddLibraryBin(root) + expect(result.shim).toBe(join(root, 'bin', 'intent.mjs')) + expect(existsSync(join(root, 'bin', 'intent.mjs'))).toBe(true) + }) + + it('skips if bin/intent.js already exists', () => { + mkdirSync(join(root, 'bin'), { recursive: true }) + writeFileSync(join(root, 'bin', 'intent.js'), 'existing') + + const result = runAddLibraryBin(root) + expect(result.shim).toBeNull() + expect(result.skipped).toBe(join(root, 'bin', 'intent.js')) + expect(readFileSync(join(root, 'bin', 'intent.js'), 'utf8')).toBe( + 'existing', + ) + }) + + it('skips if bin/intent.mjs already exists', () => { + mkdirSync(join(root, 'bin'), { recursive: true }) + writeFileSync(join(root, 'bin', 'intent.mjs'), 'existing') + + const result = runAddLibraryBin(root) + expect(result.shim).toBeNull() + expect(result.skipped).toBe(join(root, 'bin', 'intent.mjs')) }) - it('substitutes variables from package.json intent field', () => { + it('generates shim with correct content', () => { + writePkg({ name: 'test-pkg', type: 'module' }) + + runAddLibraryBin(root) + + const content = readFileSync(join(root, 'bin', 'intent.js'), 'utf8') + expect(content).toContain('#!/usr/bin/env node') + expect(content).toContain('@tanstack/intent/intent-library') + }) +}) + +describe('runEditPackageJson', () => { + it('adds skills, bin, and !skills/_artifacts to files array', () => { + writePkg({ name: 'test-pkg', files: ['dist', 'src'] }, 2) + + const result = runEditPackageJson(root) + expect(result.added).toContain('files: "skills"') + expect(result.added).toContain('files: "bin"') + expect(result.added).toContain('files: "!skills/_artifacts"') + + const pkg = readPkg() + expect(pkg.files).toContain('skills') + expect(pkg.files).toContain('bin') + expect(pkg.files).toContain('!skills/_artifacts') + expect(pkg.files).toContain('dist') + expect(pkg.files).toContain('src') + }) + + it('adds bin field when missing', () => { + writePkg({ name: 'test-pkg', files: [] }, 2) + + const result = runEditPackageJson(root) + expect(result.added).toEqual( + expect.arrayContaining([expect.stringMatching(/^bin\.intent/)]), + ) + + const pkg = readPkg() + expect(pkg.bin.intent).toMatch(/\.\/bin\/intent\.(js|mjs)/) + }) + + it('is idempotent — re-running does not duplicate entries', () => { + writePkg({ name: 'test-pkg', files: ['dist'] }, 2) + + runEditPackageJson(root) + const result = runEditPackageJson(root) + + expect(result.added).toHaveLength(0) + expect(result.alreadyPresent.length).toBeGreaterThan(0) + + const pkg = readPkg() + const skillsCount = pkg.files.filter((f: string) => f === 'skills').length + expect(skillsCount).toBe(1) + }) + + it('preserves existing package.json content', () => { + writePkg( + { + name: 'test-pkg', + version: '1.0.0', + description: 'A test package', + files: ['dist'], + }, + 2, + ) + + runEditPackageJson(root) + + const pkg = readPkg() + expect(pkg.name).toBe('test-pkg') + expect(pkg.version).toBe('1.0.0') + expect(pkg.description).toBe('A test package') + expect(pkg.files).toContain('dist') + }) + + it('preserves existing bin entries when adding intent', () => { + writePkg( + { name: 'test-pkg', files: [], bin: { 'my-cli': './bin/cli.js' } }, + 2, + ) + + runEditPackageJson(root) + + const pkg = readPkg() as Record + const bin = pkg.bin as Record + expect(bin['my-cli']).toBe('./bin/cli.js') + expect(bin.intent).toMatch(/\.\/bin\/intent\.(js|mjs)/) + }) + + it('converts string bin to object preserving existing entry', () => { + writePkg({ name: '@scope/my-tool', files: [], bin: './dist/cli.js' }, 2) + + const result = runEditPackageJson(root) + + const pkg = readPkg() as Record + const bin = pkg.bin as Record + expect(bin['my-tool']).toBe('./dist/cli.js') + expect(bin.intent).toMatch(/\.\/bin\/intent\.(js|mjs)/) + expect(result.added).toEqual( + expect.arrayContaining([expect.stringContaining('converted bin from string')]), + ) + }) + + it('creates files array if missing', () => { + writePkg({ name: 'test-pkg' }, 2) + + runEditPackageJson(root) + + const pkg = readPkg() + expect(pkg.files).toContain('skills') + expect(pkg.files).toContain('bin') + expect(pkg.files).toContain('!skills/_artifacts') + }) + + it('returns empty result when no package.json exists', () => { + const result = runEditPackageJson(root) + expect(result.added).toHaveLength(0) + expect(result.alreadyPresent).toHaveLength(0) + }) + + it('preserves 4-space indentation', () => { writeFileSync( join(root, 'package.json'), - JSON.stringify({ - name: '@tanstack/query', - intent: { repo: 'TanStack/query', docs: 'docs/' }, - }), + JSON.stringify({ name: 'test-pkg', files: ['dist'] }, null, 4), ) - const result = runSetup(root, metaDir, []) + runEditPackageJson(root) + + const raw = readFileSync(join(root, 'package.json'), 'utf8') + expect(raw).toContain(' "name"') + expect(raw).not.toMatch(/^ {2}"name"/m) + }) +}) + +describe('runSetupGithubActions', () => { + it('copies workflow templates with variable substitution', () => { + writePkg({ + name: '@tanstack/query', + intent: { repo: 'TanStack/query', docs: 'docs/' }, + }) + + const result = runSetupGithubActions(root, metaDir) + expect(result.workflows).toHaveLength(1) + expect(result.skipped).toHaveLength(0) + const wfContent = readFileSync( join(root, '.github', 'workflows', 'notify-intent.yml'), 'utf8', @@ -62,29 +235,27 @@ describe('runSetup', () => { expect(wfContent).toContain('docs: docs/**') }) - it('only copies workflows with --workflows flag', () => { - const result = runSetup(root, metaDir, ['--workflows']) + it('copies templates with defaults when no package.json', () => { + const result = runSetupGithubActions(root, metaDir) expect(result.workflows).toHaveLength(1) - }) - it('copies all with --all flag', () => { - const result = runSetup(root, metaDir, ['--all']) - expect(result.workflows).toHaveLength(1) + const wfPath = join(root, '.github', 'workflows', 'notify-intent.yml') + expect(existsSync(wfPath)).toBe(true) + const content = readFileSync(wfPath, 'utf8') + expect(content).toContain('package: unknown') }) - it('skips existing files', () => { - // First run - runSetup(root, metaDir, []) - // Second run - const result = runSetup(root, metaDir, []) + it('skips existing workflow files', () => { + runSetupGithubActions(root, metaDir) + const result = runSetupGithubActions(root, metaDir) expect(result.workflows).toHaveLength(0) - expect(result.skipped).toHaveLength(2) + expect(result.skipped).toHaveLength(1) }) it('handles missing templates directory gracefully', () => { const emptyMeta = join(root, 'empty-meta') mkdirSync(emptyMeta) - const result = runSetup(root, emptyMeta, []) + const result = runSetupGithubActions(root, emptyMeta) expect(result.workflows).toHaveLength(0) }) }) From b0400e307aaa47a3a4820878a81d0d3ddb68f3cf Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 5 Mar 2026 16:23:04 -0700 Subject: [PATCH 2/6] chore: add changeset Co-Authored-By: Claude Opus 4.6 --- .changeset/split-setup-improve-meta-skills.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/split-setup-improve-meta-skills.md diff --git a/.changeset/split-setup-improve-meta-skills.md b/.changeset/split-setup-improve-meta-skills.md new file mode 100644 index 00000000..2024c758 --- /dev/null +++ b/.changeset/split-setup-improve-meta-skills.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Replace `intent setup` with three focused commands: `intent add-library-bin`, `intent edit-package-json`, and `intent setup-github-actions`. The new `edit-package-json` command automatically wires the `files` array and `bin` field, handling existing CLIs (both object and string shorthand forms) without clobbering them. Improve meta skill SKILL.md files based on real scaffolding feedback: soften lightweight path threshold, add companion library and experimental features questions, add YAML validation step, add subagent guidance for parallel generation, and replace inline feedback sections with a pointer to the feedback-collection skill. From e2ab114f91aab995d2088e85002f85529d1969b2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:24:18 +0000 Subject: [PATCH 3/6] ci: apply automated fixes --- docs/intent/functions/runAddLibraryBin.md | 22 +++++++++++++ docs/intent/functions/runEditPackageJson.md | 22 +++++++++++++ docs/intent/functions/runSetup.md | 33 ------------------- .../intent/functions/runSetupGithubActions.md | 26 +++++++++++++++ docs/intent/index.md | 7 +++- docs/intent/interfaces/AddLibraryBinResult.md | 28 ++++++++++++++++ .../interfaces/EditPackageJsonResult.md | 28 ++++++++++++++++ .../interfaces/SetupGithubActionsResult.md | 28 ++++++++++++++++ .../intent/meta/domain-discovery/SKILL.md | 1 - packages/intent/meta/generate-skill/SKILL.md | 1 + packages/intent/meta/tree-generator/SKILL.md | 1 - packages/intent/src/setup.ts | 26 +++++++++++---- packages/intent/tests/setup.test.ts | 4 ++- 13 files changed, 184 insertions(+), 43 deletions(-) create mode 100644 docs/intent/functions/runAddLibraryBin.md create mode 100644 docs/intent/functions/runEditPackageJson.md delete mode 100644 docs/intent/functions/runSetup.md create mode 100644 docs/intent/functions/runSetupGithubActions.md create mode 100644 docs/intent/interfaces/AddLibraryBinResult.md create mode 100644 docs/intent/interfaces/EditPackageJsonResult.md create mode 100644 docs/intent/interfaces/SetupGithubActionsResult.md diff --git a/docs/intent/functions/runAddLibraryBin.md b/docs/intent/functions/runAddLibraryBin.md new file mode 100644 index 00000000..c582fac0 --- /dev/null +++ b/docs/intent/functions/runAddLibraryBin.md @@ -0,0 +1,22 @@ +--- +id: runAddLibraryBin +title: runAddLibraryBin +--- + +# Function: runAddLibraryBin() + +```ts +function runAddLibraryBin(root): AddLibraryBinResult; +``` + +Defined in: [setup.ts:176](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L176) + +## Parameters + +### root + +`string` + +## Returns + +[`AddLibraryBinResult`](../interfaces/AddLibraryBinResult.md) diff --git a/docs/intent/functions/runEditPackageJson.md b/docs/intent/functions/runEditPackageJson.md new file mode 100644 index 00000000..07ac0382 --- /dev/null +++ b/docs/intent/functions/runEditPackageJson.md @@ -0,0 +1,22 @@ +--- +id: runEditPackageJson +title: runEditPackageJson +--- + +# Function: runEditPackageJson() + +```ts +function runEditPackageJson(root): EditPackageJsonResult; +``` + +Defined in: [setup.ts:204](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L204) + +## Parameters + +### root + +`string` + +## Returns + +[`EditPackageJsonResult`](../interfaces/EditPackageJsonResult.md) diff --git a/docs/intent/functions/runSetup.md b/docs/intent/functions/runSetup.md deleted file mode 100644 index bbfe125b..00000000 --- a/docs/intent/functions/runSetup.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -id: runSetup -title: runSetup ---- - -# Function: runSetup() - -```ts -function runSetup( - root, - metaDir, - args): SetupResult; -``` - -Defined in: [setup.ts:164](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L164) - -## Parameters - -### root - -`string` - -### metaDir - -`string` - -### args - -`string`[] - -## Returns - -`SetupResult` diff --git a/docs/intent/functions/runSetupGithubActions.md b/docs/intent/functions/runSetupGithubActions.md new file mode 100644 index 00000000..379a54ea --- /dev/null +++ b/docs/intent/functions/runSetupGithubActions.md @@ -0,0 +1,26 @@ +--- +id: runSetupGithubActions +title: runSetupGithubActions +--- + +# Function: runSetupGithubActions() + +```ts +function runSetupGithubActions(root, metaDir): SetupGithubActionsResult; +``` + +Defined in: [setup.ts:290](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L290) + +## Parameters + +### root + +`string` + +### metaDir + +`string` + +## Returns + +[`SetupGithubActionsResult`](../interfaces/SetupGithubActionsResult.md) diff --git a/docs/intent/index.md b/docs/intent/index.md index 8affefe1..ad5b0321 100644 --- a/docs/intent/index.md +++ b/docs/intent/index.md @@ -7,12 +7,15 @@ title: "@tanstack/intent" ## Interfaces +- [AddLibraryBinResult](interfaces/AddLibraryBinResult.md) +- [EditPackageJsonResult](interfaces/EditPackageJsonResult.md) - [FeedbackPayload](interfaces/FeedbackPayload.md) - [IntentConfig](interfaces/IntentConfig.md) - [IntentPackage](interfaces/IntentPackage.md) - [IntentProjectConfig](interfaces/IntentProjectConfig.md) - [MetaFeedbackPayload](interfaces/MetaFeedbackPayload.md) - [ScanResult](interfaces/ScanResult.md) +- [SetupGithubActionsResult](interfaces/SetupGithubActionsResult.md) - [SkillEntry](interfaces/SkillEntry.md) - [SkillStaleness](interfaces/SkillStaleness.md) - [StalenessReport](interfaces/StalenessReport.md) @@ -31,7 +34,9 @@ title: "@tanstack/intent" - [metaToMarkdown](functions/metaToMarkdown.md) - [parseFrontmatter](functions/parseFrontmatter.md) - [resolveFrequency](functions/resolveFrequency.md) -- [runSetup](functions/runSetup.md) +- [runAddLibraryBin](functions/runAddLibraryBin.md) +- [runEditPackageJson](functions/runEditPackageJson.md) +- [runSetupGithubActions](functions/runSetupGithubActions.md) - [scanForIntents](functions/scanForIntents.md) - [submitFeedback](functions/submitFeedback.md) - [submitMetaFeedback](functions/submitMetaFeedback.md) diff --git a/docs/intent/interfaces/AddLibraryBinResult.md b/docs/intent/interfaces/AddLibraryBinResult.md new file mode 100644 index 00000000..a1f61784 --- /dev/null +++ b/docs/intent/interfaces/AddLibraryBinResult.md @@ -0,0 +1,28 @@ +--- +id: AddLibraryBinResult +title: AddLibraryBinResult +--- + +# Interface: AddLibraryBinResult + +Defined in: [setup.ts:14](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L14) + +## Properties + +### shim + +```ts +shim: string; +``` + +Defined in: [setup.ts:15](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L15) + +*** + +### skipped + +```ts +skipped: string; +``` + +Defined in: [setup.ts:16](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L16) diff --git a/docs/intent/interfaces/EditPackageJsonResult.md b/docs/intent/interfaces/EditPackageJsonResult.md new file mode 100644 index 00000000..bafef7fd --- /dev/null +++ b/docs/intent/interfaces/EditPackageJsonResult.md @@ -0,0 +1,28 @@ +--- +id: EditPackageJsonResult +title: EditPackageJsonResult +--- + +# Interface: EditPackageJsonResult + +Defined in: [setup.ts:19](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L19) + +## Properties + +### added + +```ts +added: string[]; +``` + +Defined in: [setup.ts:20](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L20) + +*** + +### alreadyPresent + +```ts +alreadyPresent: string[]; +``` + +Defined in: [setup.ts:21](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L21) diff --git a/docs/intent/interfaces/SetupGithubActionsResult.md b/docs/intent/interfaces/SetupGithubActionsResult.md new file mode 100644 index 00000000..e6cd37f1 --- /dev/null +++ b/docs/intent/interfaces/SetupGithubActionsResult.md @@ -0,0 +1,28 @@ +--- +id: SetupGithubActionsResult +title: SetupGithubActionsResult +--- + +# Interface: SetupGithubActionsResult + +Defined in: [setup.ts:24](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L24) + +## Properties + +### skipped + +```ts +skipped: string[]; +``` + +Defined in: [setup.ts:26](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L26) + +*** + +### workflows + +```ts +workflows: string[]; +``` + +Defined in: [setup.ts:25](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L25) diff --git a/packages/intent/meta/domain-discovery/SKILL.md b/packages/intent/meta/domain-discovery/SKILL.md index 07f85eaf..3cad176c 100644 --- a/packages/intent/meta/domain-discovery/SKILL.md +++ b/packages/intent/meta/domain-discovery/SKILL.md @@ -761,4 +761,3 @@ Gemini, and open-source models. To achieve this: - Section boundaries use Markdown headers (##) for navigation and --- for phase separation - No model-specific features (no XML tags in output, no tool_use assumptions) - diff --git a/packages/intent/meta/generate-skill/SKILL.md b/packages/intent/meta/generate-skill/SKILL.md index 3ee32fd5..71b53ea4 100644 --- a/packages/intent/meta/generate-skill/SKILL.md +++ b/packages/intent/meta/generate-skill/SKILL.md @@ -399,3 +399,4 @@ Output is consumed by all major AI coding agents. To ensure consistency: After generating all skills, run the `skill-feedback-collection` skill to capture feedback about the scaffolding process (domain-discovery, tree-generator, and generate-skill). +``` diff --git a/packages/intent/meta/tree-generator/SKILL.md b/packages/intent/meta/tree-generator/SKILL.md index c6a6e285..d84841e8 100644 --- a/packages/intent/meta/tree-generator/SKILL.md +++ b/packages/intent/meta/tree-generator/SKILL.md @@ -847,4 +847,3 @@ When updating: 3. CHANGELOG.md entry --- - diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index cb5884af..f71cd2bf 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -44,9 +44,14 @@ function detectVars(root: string): TemplateVars { pkgJson = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch (err: unknown) { const isNotFound = - err && typeof err === 'object' && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT' + err && + typeof err === 'object' && + 'code' in err && + (err as NodeJS.ErrnoException).code === 'ENOENT' if (!isNotFound) { - console.error(`Warning: could not read ${pkgPath}: ${err instanceof Error ? err.message : err}`) + console.error( + `Warning: could not read ${pkgPath}: ${err instanceof Error ? err.message : err}`, + ) } } @@ -141,9 +146,14 @@ function detectShimExtension(root: string): string { if (pkg.type === 'module') return 'js' } catch (err: unknown) { const isNotFound = - err && typeof err === 'object' && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT' + err && + typeof err === 'object' && + 'code' in err && + (err as NodeJS.ErrnoException).code === 'ENOENT' if (!isNotFound) { - console.error(`Warning: could not read package.json: ${err instanceof Error ? err.message : err}`) + console.error( + `Warning: could not read package.json: ${err instanceof Error ? err.message : err}`, + ) } } return 'mjs' @@ -180,7 +190,9 @@ export function runAddLibraryBin(root: string): AddLibraryBinResult { result.shim = shimPath console.log(`✓ Generated intent shim: ${shimPath}`) - console.log(`\n Run \`npx @tanstack/intent edit-package-json\` to wire package.json.`) + console.log( + `\n Run \`npx @tanstack/intent edit-package-json\` to wire package.json.`, + ) return result } @@ -257,7 +269,9 @@ export function runEditPackageJson(root: string): EditPackageJsonResult { ? pkg.name.replace(/^@[^/]+\//, '') : 'unknown' pkg.bin = { [pkgName]: pkg.bin, intent: shimRelative } - result.added.push(`bin.intent: "${shimRelative}" (converted bin from string to object)`) + result.added.push( + `bin.intent: "${shimRelative}" (converted bin from string to object)`, + ) } writeFileSync(pkgPath, JSON.stringify(pkg, null, indentSize) + '\n') diff --git a/packages/intent/tests/setup.test.ts b/packages/intent/tests/setup.test.ts index 19686e1d..0eaa70d1 100644 --- a/packages/intent/tests/setup.test.ts +++ b/packages/intent/tests/setup.test.ts @@ -180,7 +180,9 @@ describe('runEditPackageJson', () => { expect(bin['my-tool']).toBe('./dist/cli.js') expect(bin.intent).toMatch(/\.\/bin\/intent\.(js|mjs)/) expect(result.added).toEqual( - expect.arrayContaining([expect.stringContaining('converted bin from string')]), + expect.arrayContaining([ + expect.stringContaining('converted bin from string'), + ]), ) }) From 7944da7b520e1cc10ec0a87e3d723a2440dc8aa3 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 5 Mar 2026 16:27:21 -0700 Subject: [PATCH 4/6] fix: resolve TS2532 in indentMatch optional group access Co-Authored-By: Claude Opus 4.6 --- packages/intent/src/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index f71cd2bf..a08f8701 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -223,7 +223,7 @@ export function runEditPackageJson(root: string): EditPackageJsonResult { // Detect indent size from existing file const indentMatch = raw.match(/^(\s+)"/m) - const indentSize = indentMatch ? indentMatch[1].length : 2 + const indentSize = indentMatch?.[1] ? indentMatch[1].length : 2 // --- files array --- if (!Array.isArray(pkg.files)) { From 38d0926971a390947fd68628f826e95cb17d176e Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 5 Mar 2026 16:42:36 -0700 Subject: [PATCH 5/6] fix: restore hard rules and STOP gates in domain-discovery, skip !skills/_artifacts in monorepos Restore content accidentally removed in PR #34's merge conflict resolution: - Hard rules section enforcing mandatory interactive interviews - STOP gates after Phase 1c, 2e, 3i - Interactive phase headings and preambles for Phases 2 and 4 - Expanded rules for Phases 2 and 4 (open-ended questions, wait for response) - Monorepo cross-package question in Phase 2a - Phase 3 docs file listing preamble - packages field in domain_map.yaml schema - Additional constraints table rows (interview interactivity, STOP gates, etc.) - Cross-model compatibility notes about STOP gates and hard rules Also fix !skills/_artifacts to only be added in non-monorepo packages, since in monorepos _artifacts lives at repo root. Co-Authored-By: Claude Opus 4.6 --- .../intent/meta/domain-discovery/SKILL.md | 113 ++++++++++++++++-- packages/intent/src/setup.ts | 27 ++++- packages/intent/tests/setup.test.ts | 27 +++++ 3 files changed, 159 insertions(+), 8 deletions(-) diff --git a/packages/intent/meta/domain-discovery/SKILL.md b/packages/intent/meta/domain-discovery/SKILL.md index 3cad176c..e9fe3eb3 100644 --- a/packages/intent/meta/domain-discovery/SKILL.md +++ b/packages/intent/meta/domain-discovery/SKILL.md @@ -62,6 +62,42 @@ The lightweight path produces identical output artifacts (domain_map.yaml and skill_spec.md). It just avoids two separate interview rounds when the library is small enough that one round covers everything. +### Hard rules — interview phases are mandatory and interactive + +These rules override any other reasoning. No exceptions. + +1. **Phases 2 and 4 are interactive interviews conducted with the + maintainer.** You must ask the questions specified in each sub-section + and wait for the maintainer's response before continuing. Documentation, + source code, and other automated analysis are NOT substitutes for the + maintainer's answers. +2. **Every question in Phases 2 and 4 must be asked as an open-ended + question and sent as a message to the maintainer.** You must then + STOP and WAIT for their reply. Do not answer your own questions. Do + not infer answers from documentation. Do not skip questions because + you believe you already know the answer. +3. **Do not convert open-ended questions into multiple-choice, + yes/no, or confirmation prompts.** The question templates in each + sub-section are open-ended by design. Present them as open-ended + questions. The maintainer's unprompted answers surface knowledge that + pre-structured options suppress. +4. **Minimum question counts are enforced.** Each sub-section specifies + a question count range (e.g. "2–4 questions"). You must ask at least + the minimum number. Asking zero questions in any sub-section is a + protocol violation. +5. **STOP gates are mandatory.** At the boundaries marked `── STOP ──` + below, you must halt execution and wait for the maintainer's response + or acknowledgment before proceeding. Do not continue past a STOP gate + in the same message. +6. **If the maintainer asks to skip an interview phase**, explain the + value of the phase and what will be lost. Proceed with skipping only + if they confirm a second time. +7. **Rich documentation makes interviews MORE valuable, not less.** + When docs are comprehensive, the interview surfaces what docs miss: + implicit knowledge, AI-specific failure modes, undocumented tradeoffs, + and the maintainer's prioritization of what matters most. Never + rationalize skipping interviews because documentation is thorough. + --- ## Phase 1 — Quick scan (autonomous, ~10 minutes) @@ -108,20 +144,37 @@ Log (but do not group yet): - Which frameworks it supports - Any existing skill files, agent configs, or intents - Whether the library is a monorepo and which packages matter +- Peer dependency constraints — read `peerDependencies` and + `peerDependenciesMeta` from each client-facing package.json to + understand version ranges and optional integrations early + +Present your initial impressions to the maintainer as a brief summary +(3–5 bullets). This orients them on what you found and primes them for +the interview. + +**── STOP ── Do not proceed to Phase 2 until the maintainer has +acknowledged your summary or responded.** --- -## Phase 2 — High-level interview +## Phase 2 — High-level interview (interactive — requires maintainer) The maintainer's mental model of developer tasks IS the skill map. Your job in this phase is to extract it — not to propose your own structure. +You must ask the questions below to the maintainer and wait for their +responses. Do not infer answers from documentation or source code. + ### Rules for Phase 2 1. One topic per message for open-ended questions. You may batch 2–3 yes/no or short-confirmation questions together. -2. Take notes silently. Do not summarize back unless asked. -3. If the maintainer gives a short answer, probe deeper before moving on. +2. Ask each question as written (you may adapt phrasing to context, but + keep questions open-ended — never convert to multiple-choice). +3. Wait for the maintainer's response after each question before asking + the next. +4. Take notes silently. Do not summarize back unless asked. +5. If the maintainer gives a short answer, probe deeper before moving on. ### 2a — Developer tasks (2–4 questions) @@ -138,6 +191,13 @@ Follow up to enumerate distinct tasks: > things like 'set up the client', 'implement auth', 'debug sync issues' > — each one a separate moment where they'd want focused guidance." +For monorepo libraries, also ask about cross-package tasks: + +> "Are there tasks that touch multiple packages in your monorepo? For +> example, a getting-started flow that requires imports from both the +> client and server packages? I want to make sure skills that span +> package boundaries are captured correctly." + ### 2b — Developer journeys (1–2 questions) Surface lifecycle/journey skills that cross-cut task areas: @@ -170,6 +230,9 @@ Synthesize what you heard into a proposed skill list and present it: > [enumerate skills with one-line descriptions]. Does this match how > you think about your library? What would you add, remove, or rename?" +**── STOP ── Do not proceed to Phase 3 until the maintainer has +reviewed and confirmed (or corrected) the skill list.** + --- ## Phase 3 — Deep read (autonomous) @@ -182,6 +245,10 @@ gotchas. Read in this order. Each step builds context for the next. +Before starting, list every file in the docs directory (and subdirectories). +Use this list as a checklist — every narrative file must be read. Do not +sample a subset and extrapolate. + 1. **Narrative guides** — read as many as needed to build confidence in your understanding. Prioritize getting-started, migration, and guides covering the skill areas from Phase 2. Skip exhaustive reading of large @@ -432,20 +499,35 @@ Present the draft to the maintainer before starting Phase 4: > [N] skills and [M] failure modes. I've flagged [K] specific gaps where > I need your input." +Include the full draft domain_map.yaml in your message so the maintainer +can review it. Also include a checklist of all docs files you read. + +**── STOP ── Do not proceed to Phase 4 until the maintainer has +reviewed the draft and responded. Their feedback on the draft informs +the detail interview questions.** + --- -## Phase 4 — Detail interview (builds on Phase 1–3) +## Phase 4 — Detail interview (interactive — requires maintainer) You have the maintainer's task map and a deep read. The interview now fills gaps, validates your understanding, and surfaces implicit knowledge. +You must ask the questions below to the maintainer and wait for their +responses. Do not infer answers from documentation or source code — +even for gaps you think you can answer from your reading. + ### Rules for Phase 4 1. One topic per message for open-ended questions. You may batch 2–3 yes/no or short-confirmation questions together. -2. Each question must reference something specific from your reading. -3. If the maintainer gives a short answer, probe deeper before moving on. -4. Take notes silently. Do not summarize back unless asked. +2. Ask each question as written (you may adapt phrasing to context, but + keep questions open-ended — never convert to multiple-choice). +3. Each question must reference something specific from your reading. +4. Wait for the maintainer's response after each question before asking + the next. +5. If the maintainer gives a short answer, probe deeper before moving on. +6. Take notes silently. Do not summarize back unless asked. ### 4a — Draft review (2–3 questions) @@ -599,6 +681,9 @@ skills: domain: '[parent domain slug]' description: '[what a developer is doing — matches a specific task/moment]' type: '[core | framework | lifecycle | composition]' + packages: # required for monorepo; omit for single-package libraries + - '[primary package name]' + - '[secondary package name, if skill spans multiple packages]' covers: - '[API/hook/concept 1]' - '[API/hook/concept 2]' @@ -729,6 +814,11 @@ not promotional.] | ------------------------------------- | -------------------------------------------------------------------------- | | Quick scan before interview | Never interview without at least reading README and package structure | | High-level interview before deep read | The maintainer's task map informs what you read deeply | +| **Interview phases are interactive** | Phases 2 and 4 require sending questions to the maintainer and waiting | +| **Docs are not a substitute** | Documentation cannot replace maintainer answers — even comprehensive docs | +| **Open-ended questions stay open** | Never convert interview questions to multiple-choice or yes/no | +| **Minimum question counts enforced** | Each sub-section's minimum count must be met; zero questions = violation | +| **STOP gates are mandatory** | Do not proceed past a STOP gate without maintainer response | | Batch only confirmations | Yes/no questions may batch 2–3; open-ended questions get their own message | | Questions reference findings | No generic questions — cite what you found | | Skills are task-focused | Each skill matches a developer moment, not a conceptual area | @@ -738,6 +828,7 @@ not promotional.] | No marketing prose | Library description is factual, not promotional | | domain_map.yaml is valid YAML | Parseable by any YAML parser | | Draft before detail interview | Present draft for review before Phase 4 | +| **Draft reviewed before Phase 4** | Maintainer must acknowledge or respond to draft before detail interview | | Agent-specific failures probed | Always ask AI-agent-specific questions in Phase 4c | | Compositions discovered from code | Scan peer deps and examples before asking composition questions | | Cross-skill failure modes tagged | Failure modes spanning skills list all relevant slugs | @@ -746,6 +837,7 @@ not promotional.] | 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 | +| **All docs files read** | List docs directory contents and read every narrative file — no sampling | --- @@ -755,6 +847,13 @@ This skill is designed to produce consistent results across Claude, GPT-4+, Gemini, and open-source models. To achieve this: - All instructions use imperative sentences, not suggestions +- Interview phases use explicit STOP gates to prevent models from + continuing autonomously past interactive checkpoints +- Hard rules at the top override any model tendency to rationalize + skipping interactive phases when documentation is available +- Open-ended questions are explicitly protected from conversion to + multiple-choice or confirmation prompts, which models default to + when they have enough context to pre-populate answers - Output formats use YAML (universally parsed) and Markdown tables (universally rendered) - Examples use concrete values, not placeholders like "[your value here]" diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index a08f8701..24b9e3b9 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -230,7 +230,32 @@ export function runEditPackageJson(root: string): EditPackageJsonResult { pkg.files = [] } const files = pkg.files as string[] - const requiredFiles = ['skills', 'bin', '!skills/_artifacts'] + + // In monorepos, _artifacts lives at repo root, not under packages — + // the negation pattern is a no-op and shouldn't be added. + // Detect monorepo by walking up to find a parent package.json with workspaces. + const isMonorepo = (() => { + let dir = join(root, '..') + for (let i = 0; i < 5; i++) { + const parentPkg = join(dir, 'package.json') + if (existsSync(parentPkg)) { + try { + const parent = JSON.parse(readFileSync(parentPkg, 'utf8')) + if (Array.isArray(parent.workspaces) || parent.workspaces?.packages) { + return true + } + } catch {} + return false + } + const next = join(dir, '..') + if (next === dir) break + dir = next + } + return false + })() + const requiredFiles = isMonorepo + ? ['skills', 'bin'] + : ['skills', 'bin', '!skills/_artifacts'] for (const entry of requiredFiles) { if (files.includes(entry)) { diff --git a/packages/intent/tests/setup.test.ts b/packages/intent/tests/setup.test.ts index 0eaa70d1..f5cd7a38 100644 --- a/packages/intent/tests/setup.test.ts +++ b/packages/intent/tests/setup.test.ts @@ -203,6 +203,33 @@ describe('runEditPackageJson', () => { expect(result.alreadyPresent).toHaveLength(0) }) + it('skips !skills/_artifacts in monorepo packages', () => { + // Simulate monorepo by creating a parent package.json + const monoRoot = mkdtempSync(join(tmpdir(), 'mono-root-')) + const pkgDir = join(monoRoot, 'packages', 'my-lib') + mkdirSync(pkgDir, { recursive: true }) + writeFileSync( + join(monoRoot, 'package.json'), + JSON.stringify({ workspaces: ['packages/*'] }), + ) + writeFileSync( + join(pkgDir, 'package.json'), + JSON.stringify({ name: '@scope/my-lib', files: ['dist'] }, null, 2), + ) + + const result = runEditPackageJson(pkgDir) + expect(result.added).toContain('files: "skills"') + expect(result.added).toContain('files: "bin"') + expect(result.added).not.toEqual( + expect.arrayContaining([expect.stringContaining('!skills/_artifacts')]), + ) + + const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')) + expect(pkg.files).not.toContain('!skills/_artifacts') + + rmSync(monoRoot, { recursive: true, force: true }) + }) + it('preserves 4-space indentation', () => { writeFileSync( join(root, 'package.json'), From b996d2ed3dd06c7337fb130d2b61b38cd7e6e909 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:43:14 +0000 Subject: [PATCH 6/6] ci: apply automated fixes --- docs/intent/functions/runSetupGithubActions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intent/functions/runSetupGithubActions.md b/docs/intent/functions/runSetupGithubActions.md index 379a54ea..7580b7e0 100644 --- a/docs/intent/functions/runSetupGithubActions.md +++ b/docs/intent/functions/runSetupGithubActions.md @@ -9,7 +9,7 @@ title: runSetupGithubActions function runSetupGithubActions(root, metaDir): SetupGithubActionsResult; ``` -Defined in: [setup.ts:290](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L290) +Defined in: [setup.ts:315](https://github.com/TanStack/intent/blob/main/packages/intent/src/setup.ts#L315) ## Parameters