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

Filter by extension

Filter by extension

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

Generate commands for installed Intent, reject unsafe setup write paths before creating records, and validate distribution selections before writes. Check JavaScript library contracts and JSX examples across supported Markdown fences, and reuse validation results in maintainer reports to avoid compiling the same examples twice.
2 changes: 1 addition & 1 deletion packages/intent/meta/generate-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ For new guidance and updates that change a recommended behavior, follow [task qu

For new or changed descriptions, also follow [discovery checks](references/task-quality.md#check-discovery-separately). Keep activation evidence separate from task correctness.

Run `npx @tanstack/intent@latest validate <skills-root>` with the actual owning package's skill directory (or the repository's installed `intent`). Fix errors without weakening validation. Keep every SKILL.md within the 500-line limit. Review packaging warnings separately; they do not require installing dependencies or changing publishing configuration during authoring.
Run `npm exec --no -- intent validate <skills-root>` with the actual owning package's skill directory (or the repository's installed `intent`). Fix errors without weakening validation. Keep every SKILL.md within the 500-line limit. Review packaging warnings separately; they do not require installing dependencies or changing publishing configuration during authoring.

Check that every reference and prerequisite resolves, every changed claim matches the cited source/version, and examples use actual supported APIs. Exercise the relevant example or package check where available. Intent's structural validation does not prove semantic correctness or agent behavior. If a check cannot run, report it as not verified with the reason.

Expand Down
4 changes: 2 additions & 2 deletions packages/intent/meta/tree-generator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Every skill has a `metadata.type` field in its frontmatter. Valid types:
| `composition` | Integration between two or more libraries | `electric-drizzle` |
| `security` | Audit checklist or security validation | `electric-security-check` |

Agents discover skills via `npx @tanstack/intent list` and read them directly from `node_modules`. Framework skills declare a `requires` dependency on their core skill so agents load them in the right order.
Agents discover skills via `npm exec --no -- intent list` and read them directly from `node_modules`. Framework skills declare a `requires` dependency on their core skill so agents load them in the right order.

There are two workflows. Detect which applies.

Expand Down Expand Up @@ -225,7 +225,7 @@ packages/
│ └── package.json # Add "skills" to files array
```

Publishing configuration is separate from authoring. When the maintainer requests it, `npx @tanstack/intent@latest edit-package-json` prepares the package; review its resulting diff.
Publishing configuration is separate from authoring. When the maintainer requests it, `npm exec --no -- intent edit-package-json` prepares the package; review its resulting diff.

### Steps 2–7 — Write skills

Expand Down
8 changes: 4 additions & 4 deletions packages/intent/src/commands/install/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ Follow these steps in order:
- If not found: continue to step 2.

2. DISCOVER AVAILABLE SKILLS
Run: \`npx @tanstack/intent@latest list\`
Run: \`npm exec --no -- intent list\`
This scans project-local node_modules by default and outputs each package and skill's name,
description, and source.
If the user explicitly wants globally installed skills included, run:
\`npx @tanstack/intent@latest list --global\`
\`npm exec --no -- intent list --global\`
This works best in Node-compatible environments (npm, pnpm, Bun, or Deno npm interop
with node_modules enabled).
If no skills are found, do not create a config file. Report: "No intent-enabled skills found."
Expand All @@ -72,7 +72,7 @@ Follow these steps in order:
- Include slash-named sub-skills when no parent mapping exists, or when they describe distinct user tasks.
- If the proposed block would exceed 12 mappings, show the full discovered list and ask which packages
or skill groups to include before writing.
- Add one fallback note telling the agent to run \`npx @tanstack/intent@latest list\` for less common local skills.
- Add one fallback note telling the agent to run \`npm exec --no -- intent list\` for less common local skills.

Based on the repository scan and the coverage rule, propose the skill-to-task mappings.
For each one explain:
Expand All @@ -97,7 +97,7 @@ Follow these steps in order:
# TanStack Intent - before editing files, run the matching guidance command.
tanstackIntent:
- id: "@scope/package#skill-name"
run: "npx @tanstack/intent@latest load @scope/package#skill-name"
run: "npm exec --no -- intent load @scope/package#skill-name"
for: "describe the task or code area here"
<!-- intent-skills:end -->

Expand Down
90 changes: 61 additions & 29 deletions packages/intent/src/commands/install/guidance.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { parse as parseYaml } from 'yaml'
import { formatIntentCommand } from '../../shared/command-runner.js'
import { repositoryWritePath } from '../../shared/write-path.js'
import { writeChanges } from '../../maintainer/files.js'
import { parseIntentInvocation } from '../../hooks/policy.js'
import { isGeneratedMappingSkill } from '../../skills/categories.js'
import { formatSkillUse, parseSkillUse } from '../../skills/use.js'
import type { FileChange } from '../../maintainer/files.js'
import type { ScanResult, SkillEntry } from '../../shared/types.js'

type GuidanceNamespace = 'intent-skills' | 'intent-maintainer'
Expand Down Expand Up @@ -137,10 +141,8 @@ function containsLocalPathValue(value: string): boolean {
}

function parseLoadedSkillUse(command: string): string | null {
const match = command.match(
/(?:^|&&|\|\||;|\|)\s*(?:bunx\s+@tanstack\/intent(?:@latest)?|pnpm\s+exec\s+intent|pnpm\s+dlx\s+@tanstack\/intent(?:@latest)?|npx\s+@tanstack\/intent(?:@latest)?|yarn\s+dlx\s+@tanstack\/intent(?:@latest)?|intent)\s+load\s+([^\s|;&]+)/i,
)
return match?.[1] ?? null
const invocation = parseIntentInvocation(command)
return invocation?.action === 'load' ? (invocation.skillUse ?? null) : null
}

export function verifyIntentSkillsBlockFile({
Expand Down Expand Up @@ -283,7 +285,7 @@ export function resolveIntentSkillsBlockTargetPath(
root,
namespace === 'intent-skills' ? 'intent-maintainer' : 'intent-skills',
)?.filePath ??
join(root, 'AGENTS.md')
repositoryWritePath(root, join(root, 'AGENTS.md'))
)
}

Expand Down Expand Up @@ -355,6 +357,7 @@ export function buildIntentSkillGuidanceBlock(
INTENT_SKILLS_START,
'## Skill Loading',
'',
'Use the repository’s installed Intent. If it is unavailable, report the missing dependency instead of downloading a replacement.',
'Before editing files for a substantial task:',
`- Run \`${listCommand}\` from the workspace root to see available local skills.`,
`- If a listed skill matches the task, run \`${loadCommand}\` before changing files.`,
Expand All @@ -380,6 +383,7 @@ export function buildMaintainerGuidanceBlock(
'<!-- intent-maintainer:start -->',
'## Library Skill Maintenance',
'',
'Use the repository’s installed Intent. If it is unavailable, report the missing dependency instead of downloading a replacement.',
`Before substantial library source, documentation, examples, tests, or skill work, run \`${command}\` and follow the packaged maintainer procedure.`,
'Use the current request and repository evidence. For initial skills, propose a useful batch and reuse any scope already agreed with the maintainer.',
`Before handing off a skill batch or library change, run \`${reviewCommand}\`. Follow the maintainer procedure to update affected guidance, run task checks, and record completed review outcomes. Report an evidence-backed no-op or missing evidence explicitly.`,
Expand Down Expand Up @@ -410,7 +414,7 @@ function findExistingConfigWithManagedBlock(
managedBlock: ManagedBlock
} | null {
for (const file of SUPPORTED_AGENT_CONFIG_FILES) {
const filePath = join(root, file)
const filePath = repositoryWritePath(root, join(root, file))
if (!existsSync(filePath)) continue

const content = readFileSync(filePath, 'utf8')
Expand Down Expand Up @@ -439,18 +443,23 @@ function replaceManagedBlock(
return `${content.slice(0, managedBlock.start)}${styledBlock}${content.slice(managedBlock.end)}`
}

export function writeIntentSkillsBlock({
export function planIntentSkillsBlock({
block,
mappingCount,
root,
skipWhenEmpty = true,
namespace = 'intent-skills',
}: WriteIntentSkillsBlockOptions): WriteIntentSkillsBlockResult {
}: WriteIntentSkillsBlockOptions): {
result: WriteIntentSkillsBlockResult
change?: FileChange
} {
if (mappingCount === 0 && skipWhenEmpty) {
return {
mappingCount,
status: 'skipped',
targetPath: null,
result: {
mappingCount,
status: 'skipped',
targetPath: null,
},
}
}

Expand All @@ -465,17 +474,25 @@ export function writeIntentSkillsBlock({
)
if (nextContent === existingTarget.content) {
return {
mappingCount,
status: 'unchanged',
targetPath,
result: {
mappingCount,
status: 'unchanged',
targetPath,
},
}
}

writeFileSync(targetPath, nextContent)
return {
mappingCount,
status: 'updated',
targetPath,
change: {
path: targetPath,
source: existingTarget.content,
content: nextContent,
},
result: {
mappingCount,
status: 'updated',
targetPath,
},
}
}

Expand All @@ -485,19 +502,34 @@ export function writeIntentSkillsBlock({
const separator = currentContent === '' ? '' : newline
const nextContent = `${withNewlineStyle(block, newline)}${separator}${currentContent}`

writeFileSync(targetPath, nextContent)
return {
mappingCount,
status: 'updated',
targetPath,
change: {
path: targetPath,
source: currentContent,
content: nextContent,
},
result: {
mappingCount,
status: 'updated',
targetPath,
},
}
}

mkdirSync(dirname(targetPath), { recursive: true })
writeFileSync(targetPath, block)
return {
mappingCount,
status: 'created',
targetPath,
change: { path: targetPath, source: null, content: block },
result: {
mappingCount,
status: 'created',
targetPath,
},
}
}

export function writeIntentSkillsBlock(
options: WriteIntentSkillsBlockOptions,
): WriteIntentSkillsBlockResult {
const plan = planIntentSkillsBlock(options)
if (plan.change) writeChanges(options.root, [plan.change])
return plan.result
}
Loading
Loading