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
2 changes: 1 addition & 1 deletion docs/intent/functions/scanForIntents.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: scanForIntents
function scanForIntents(root?): Promise<ScanResult>;
```

Defined in: [scanner.ts:142](https://github.com/TanStack/intent/blob/main/packages/intent/src/scanner.ts#L142)
Defined in: [scanner.ts:190](https://github.com/TanStack/intent/blob/main/packages/intent/src/scanner.ts#L190)

## Parameters

Expand Down
52 changes: 50 additions & 2 deletions packages/intent/meta/domain-discovery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,26 @@ reading exhaustively yet.
guidance, read it. This is high-signal for what the maintainer
considers important

### 1b — Note initial impressions
### 1b — Read peer dependency constraints

Check `package.json` for `peerDependencies` and `peerDependenciesMeta`.
For each major peer dependency (React, Vue, Svelte, Next.js, etc.):

1. Note the version range required
2. Read the peer's docs for integration constraints that affect this
library: SSR/hydration rules, component lifecycle boundaries,
browser-only APIs, singleton patterns, connection limits
3. Log framework-specific failure modes — these are the highest-impact
failure modes and cannot be discovered from the library's own source

Examples of peer-dependency-driven failure modes:

- SSR: calling browser-only APIs during server render
- React: breaking hook rules in library wrapper components
- Connection limits: opening multiple WebSocket connections per tab
- Singleton patterns: creating multiple client instances in dev mode

### 1c — Note initial impressions

Log (but do not group yet):

Expand Down Expand Up @@ -191,7 +210,11 @@ Move concept inventory items into groups. Two items belong together when:
- They share a lifecycle, configuration scope, or architectural tradeoff
- Getting one wrong tends to produce bugs in the other

Target 4–7 domains. These are conceptual groupings, not the final skills.
Let library complexity drive the domain count — a focused library may need
only 2–3 domains, while a large framework may need 7+. Validate by asking:
"Would a developer working on a single feature need to load skills from
multiple domains? If so, merge those domains." These are conceptual
groupings, not the final skills.

Do not create a group for:

Expand All @@ -202,6 +225,14 @@ Do not create a group for:

Name each domain as work being performed, not what the library provides.

**Validation step:** After grouping, check each domain by asking:
"Would a developer working on a single feature need to load skills from
multiple domains?" If yes, merge those domains. Group by developer tasks
(what they're trying to accomplish), not by architecture (how the library
is organized internally). For example, prefer "writing data" over
"producer lifecycle" — the former matches a developer's intent, the latter
matches the codebase structure.

### 3b — Map domains × tasks → skills

Merge your conceptual domains with the maintainer's task list from
Expand Down Expand Up @@ -268,6 +299,14 @@ For each skill, extract failure modes that pass all three tests:

Target 3 failure modes per skill minimum. Complex skills target 5–6.

**Code patterns.** Every failure mode should include `wrong_pattern` and
`correct_pattern` fields with short code snippets (3–10 lines each).
The wrong pattern is what an agent would generate; the correct pattern
is the fix. These feed directly into SKILL.md Common Mistakes sections
as wrong/correct code pairs. If the failure mode is purely conceptual
(e.g. an architectural choice) rather than a code pattern, omit both
fields and explain in `mechanism` instead.

**Cross-skill failure modes.** Some failure modes belong to multiple
skills. A developer doing SSR work and a developer doing state management
both need to know about "stale state during hydration" — they load
Expand Down Expand Up @@ -465,6 +504,11 @@ Update `status: draft` to `status: reviewed`.
If the maintainer uses a custom skills root, replace `skills/` in the paths
below with their chosen directory.

**Monorepo layout:** For monorepos, domain map artifacts go at the REPO ROOT
(e.g. `_artifacts/domain_map.yaml`) since they describe the whole library.
Skills are generated per-package later by the tree-generator and generate-skill
steps.

### 1. skills/\_artifacts/domain_map.yaml

```yaml
Expand Down Expand Up @@ -510,6 +554,10 @@ skills:
failure_modes:
- mistake: '[5-10 word phrase]'
mechanism: '[one sentence]'
wrong_pattern: | # the code an agent would incorrectly generate
[short code snippet showing the mistake]
correct_pattern: | # the code that should be generated instead
[short code snippet showing the fix]
source: '[doc page, source file, issue link, or maintainer interview]'
priority: '[CRITICAL | HIGH | MEDIUM]'
status: '[active | fixed-but-legacy-risk | removed]'
Expand Down
9 changes: 9 additions & 0 deletions packages/intent/meta/generate-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ You will receive:
If the maintainer uses a custom skills root, replace `skills/` in any paths
below with their chosen directory.

**Monorepo:** When the skill tree entry has a `package` field, write the
SKILL.md into that package's skills directory (e.g.
`packages/client/skills/core/SKILL.md`), not a shared root.

1. **Skill name** — format `library-group/skill-name` (e.g. `tanstack-query/core`,
`tanstack-router/loaders`, `db/core/live-queries`)
2. **Skill description** — what the skill covers and when an agent should load it
Expand Down Expand Up @@ -77,6 +81,11 @@ skill-tree-generator for the full spec of each type.

## Step 2 — Extract content from sources

**Line budget:** Each SKILL.md must stay under 500 lines. Before writing,
estimate the content size. If a skill has 5+ failure modes, 3+ primary
patterns, and subsystem details, proactively plan reference files during
extraction — don't wait until the skill exceeds the limit.

Read through the source documentation. Extract only what a coding agent
cannot already know:

Expand Down
8 changes: 7 additions & 1 deletion packages/intent/meta/tree-generator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ discovery. This produces lower-fidelity output than the full
skill-domain-discovery skill — prefer running that when time permits.

1. Build a concept inventory (every export, config key, constraint, warning)
2. Group into 4–7 capability domains using work-oriented names
2. Group into capability domains using work-oriented names (let library complexity drive the count — 2–3 for focused libraries, more for large frameworks)
3. Enumerate 10–20 task-focused skills from the intersection of domains
and developer tasks
4. Extract 3+ failure modes per skill (plausible, silent, grounded)
Expand Down Expand Up @@ -106,6 +106,7 @@ skills:
type: 'core | sub-skill | framework | lifecycle | composition | security'
domain: '[domain slug]'
path: 'skills/[path]/SKILL.md'
package: '[package directory, e.g. packages/client]' # monorepo only — which package this skill belongs to
description: '[1–2 sentence agent-facing routing key]'
requires:
- '[other skill slugs]' # omit if none
Expand All @@ -118,6 +119,11 @@ skills:
- 'references/[file].md' # omit if none
```

**Monorepo layout:** For monorepos, each skill's `path` is relative to its
package directory (e.g. `packages/client/skills/core/SKILL.md`). Set the
`package` field so generate-skill knows where to write the file. The domain
map artifacts stay at the repo root.

### Step 1 — Plan the file tree

From the domain map, each entry in the `skills` list becomes a SKILL.md
Expand Down
50 changes: 32 additions & 18 deletions packages/intent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ function cmdValidate(args: string[]): void {
if (lineCount > 500) {
errors.push({
file: rel,
message: `Exceeds 500 line limit (${lineCount} lines)`,
message: `Exceeds 500 line limit (${lineCount} lines). Rewrite for conciseness: move API tables to references/, trim verbose examples, and remove content an agent already knows. Do not simply raise the limit.`,
})
}
}
Expand Down Expand Up @@ -354,26 +354,39 @@ 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 for their skills root path.
- Default: skills/
- If they choose a different path, replace "skills/" in all output paths below.
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/<pkg>/skills/<domain>/<skill>/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 (exact paths):
- skills/_artifacts/domain_map.yaml
- skills/_artifacts/skill_spec.md
- 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: skills/_artifacts/domain_map.yaml + skills/_artifacts/skill_spec.md
- Output file (exact path):
- skills/_artifacts/skill_tree.yaml
- 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: skills/_artifacts/skill_tree.yaml
- Output files (exact path pattern):
- skills/<domain>/<skill>/SKILL.md
- Input: skill tree
- Output files:
- Single-repo: skills/<domain>/<skill>/SKILL.md
- Monorepo: packages/<pkg>/skills/<domain>/<skill>/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.
Expand All @@ -382,13 +395,14 @@ Guidance for the maintainer:
- 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 intent feedback --meta --submit --file <path>
Ask if the maintainer wants to edit it, then submit it via: npx @tanstack/intent feedback --meta --submit --file <path>

Finish with a short checklist:
- Run npx intent validate
- Commit skills/ and skills/_artifacts/ (artifacts are repo-only)
- Exclude skills/_artifacts/ from package publishing
- Add README snippet: If you use an AI agent, run npx intent init
- 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
`

console.log(prompt)
Expand Down
57 changes: 54 additions & 3 deletions packages/intent/src/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,54 @@ function validateIntentField(
}
}

/**
* Derive an IntentConfig from standard package.json fields when no explicit
* `intent` field is present. A package with a `skills/` directory signals
* intent support; `repo` and `docs` are derived from `repository` and
* `homepage`.
*/
function deriveIntentConfig(
pkgJson: Record<string, unknown>,
): IntentConfig | null {
// Derive repo from repository field
let repo: string | null = null
if (typeof pkgJson.repository === 'string') {
repo = pkgJson.repository
} else if (
pkgJson.repository &&
typeof pkgJson.repository === 'object' &&
typeof (pkgJson.repository as Record<string, unknown>).url === 'string'
) {
repo = (pkgJson.repository as Record<string, unknown>).url as string
// Normalize git+https://github.com/foo/bar.git → foo/bar
repo = repo
.replace(/^git\+/, '')
.replace(/\.git$/, '')
.replace(/^https?:\/\/github\.com\//, '')
}

// Derive docs from homepage field
const docs =
typeof pkgJson.homepage === 'string' ? pkgJson.homepage : undefined

// Need at least a repo to be useful
if (!repo) return null

// Derive requires from intent.requires if partially present
const intentPartial = pkgJson.intent as Record<string, unknown> | undefined
const requires =
intentPartial && Array.isArray(intentPartial.requires)
? intentPartial.requires.filter((r): r is string => typeof r === 'string')
: undefined

return {
version: 1,
repo,
docs: docs ?? '',
requires,
}
}

// ---------------------------------------------------------------------------
// Skill discovery within a package
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -207,11 +255,14 @@ export async function scanForIntents(root?: string): Promise<ScanResult> {
const pkgVersion =
typeof pkgJson.version === 'string' ? pkgJson.version : '0.0.0'

// Validate intent field
const intent = validateIntentField(pkgName, pkgJson.intent)
// Validate intent field — explicit config takes priority, then derive from
// standard package.json fields (repository, homepage)
const intent =
validateIntentField(pkgName, pkgJson.intent) ??
deriveIntentConfig(pkgJson)
if (!intent) {
warnings.push(
`${pkgName} has a skills/ directory but missing or invalid "intent" field in package.json`,
`${pkgName} has a skills/ directory but could not determine repo/docs from package.json (add a "repository" field or explicit "intent" config)`,
)
continue
}
Expand Down
Loading