Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9a04a8b
fix(intent): remove session-wide hook edit gate
LadyBluenotes Jul 20, 2026
58a8161
feat(lock): add per-skill lock primitives
LadyBluenotes Jul 20, 2026
543b83a
feat(catalog): add lock-aware cached skill catalogues
LadyBluenotes Jul 20, 2026
2d68b6b
feat(install): add consumer installation planning
LadyBluenotes Jul 20, 2026
de94657
feat(sync): add managed skill link synchronization
LadyBluenotes Jul 20, 2026
010baad
add clack
LadyBluenotes Jul 22, 2026
e16f781
feat(install): add interactive consumer setup
LadyBluenotes Jul 22, 2026
9a13529
feat(install): enhance target selection based on install method
LadyBluenotes Jul 22, 2026
a4f30a7
feat(install): implement grouped skill selection for improved user ex…
LadyBluenotes Jul 22, 2026
b39fff8
feat(sync): enhance output for new dependencies and skills in sync co…
LadyBluenotes Jul 22, 2026
0949902
feat(sync): implement interactive review for new dependencies and skills
LadyBluenotes Jul 22, 2026
3187fb5
fix(load): enforce accepted skill baselines
LadyBluenotes Jul 22, 2026
cd7529b
feat(sync): replace rmSync with unlinkSync and rmdirSync for link rem…
LadyBluenotes Jul 25, 2026
e91b98c
test(benchmarks): exercise lock verification in catalogue benchmarks
LadyBluenotes Jul 25, 2026
1746da4
feat(delivery): add delivery job to PR workflow and implement deliver…
LadyBluenotes Jul 25, 2026
22a63c6
test(scanner): pin current consumer discovery behavior
LadyBluenotes Jul 25, 2026
a8eda95
refactor(exclude): write package.json through the shared JSONC config…
LadyBluenotes Jul 25, 2026
abd20be
fix(lockfile): say when a lockfile was written by a newer Intent
LadyBluenotes Jul 25, 2026
6cb7cb5
fix(install): reject only exclusions that hide an enabled skill
LadyBluenotes Jul 25, 2026
6cc8690
test: pin audience and declined-package policy status
LadyBluenotes Jul 25, 2026
153f1fc
feat(policy): enforce skill-level intent.skills entries on every surface
LadyBluenotes Jul 25, 2026
906d3bb
refactor(policy): compile the skill source policy once per load
LadyBluenotes Jul 25, 2026
ee746c0
perf(load): reuse the already-read config on the full-scan path
LadyBluenotes Jul 25, 2026
32ab9ef
fix(sync): decline new skills without revoking allowed siblings
LadyBluenotes Jul 25, 2026
a983a33
feat(install): record partial skill selections as allow entries
LadyBluenotes Jul 25, 2026
a062988
refactor(hooks): run session hooks through the CLI
LadyBluenotes Jul 25, 2026
df2ecc9
feat(install): deliver skills through lifecycle hooks
LadyBluenotes Jul 25, 2026
eaae768
feat(install): report deltas and detect agent targets
LadyBluenotes Jul 25, 2026
b7c5190
test: pin released config shapes against policy changes
LadyBluenotes Jul 25, 2026
a3e7545
feat(install): encourage the Intent devDependency instead of requirin…
LadyBluenotes Jul 25, 2026
fa7f70f
fix(install): pin the version in generated Intent commands
LadyBluenotes Jul 25, 2026
25b39ee
perf(catalog): load yaml and semver only when they are used
LadyBluenotes Jul 26, 2026
3dfe63f
fix(catalog): keep human-facing notices out of agent context
LadyBluenotes Jul 26, 2026
9ad2f59
fix(list): stop spending agent context on output agents cannot use
LadyBluenotes Jul 26, 2026
deb5a1b
stop sending discovery warnings to agents & remove redundant work fro…
LadyBluenotes Jul 26, 2026
11e9f39
feat(list): add --why to explain why each skill is or is not available
LadyBluenotes Jul 26, 2026
7b54a05
feat(install): support declarative CI bootstrap
LadyBluenotes Jul 26, 2026
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
17 changes: 17 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ jobs:
main-branch-name: main
- name: Run Checks
run: pnpm run test:pr
delivery:
name: Delivery (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Setup Tools
uses: TanStack/config/.github/setup@b313637fa7d314532b98638f6b57b7b9c169d390 # main
- name: Run Managed Link Tests
run: pnpm --filter @tanstack/intent run test:delivery
preview:
name: Preview
runs-on: ubuntu-latest
Expand Down
107 changes: 107 additions & 0 deletions benchmarks/intent/catalog.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { rmSync } from 'node:fs'
import { join } from 'node:path'
import { afterAll, beforeAll, bench, describe } from 'vitest'
import { scanForIntents } from '../../packages/intent/src/discovery/scanner.js'
import { buildCurrentLockfileSources } from '../../packages/intent/src/core/lockfile/lockfile-state.js'
import { writeIntentLockfile } from '../../packages/intent/src/core/lockfile/lockfile.js'
import {
createCliRunner,
createConsoleSilencer,
createTempDir,
writeFile,
writeJson,
writePackage,
} from './helpers.js'

const consoleSilencer = createConsoleSilencer()
const root = createTempDir('catalog')
const runner = createCliRunner({ cwd: root })

const PACKAGES = [
{
name: '@bench/query',
skills: [
'queries',
'mutations',
'invalidation',
'prefetching',
'suspense',
'pagination',
'optimistic-updates',
'ssr-hydration',
],
},
{
name: '@bench/router',
skills: [
'routing',
'loaders',
'search-params',
'navigation',
'code-splitting',
'route-masking',
'not-found',
],
},
{
name: '@bench/table',
skills: ['columns', 'sorting', 'filtering', 'grouping', 'virtualization'],
},
]

let getIntentCatalogContext: (options: {
cwd: string
refresh?: boolean
}) => Promise<unknown>

beforeAll(async () => {
consoleSilencer.silence()
writeJson(join(root, 'package.json'), {
name: 'intent-catalog-benchmark',
private: true,
intent: { skills: ['@bench/*'] },
dependencies: Object.fromEntries(
PACKAGES.map((pkg) => [pkg.name, '1.0.0']),
),
})
writeFile(join(root, 'pnpm-lock.yaml'), 'lockfileVersion: "9.0"\n')
for (const pkg of PACKAGES) {
writePackage(join(root, 'node_modules'), pkg.name, '1.0.0', {
skills: pkg.skills,
})
}

// Without a lockfile the catalogue skips verification entirely, so the warm
// path would measure an empty loop instead of the per-skill hashing it runs
// on every session start.
writeIntentLockfile(join(root, 'intent.lock'), {
lockfileVersion: 1,
sources: buildCurrentLockfileSources(
scanForIntents(root, { scope: 'local' }).packages,
),
})

await runner.setup()
const catalog = await import('../../packages/intent/dist/catalog.mjs')
getIntentCatalogContext = catalog.getIntentCatalogContext
})

afterAll(() => {
runner.teardown()
rmSync(root, { recursive: true, force: true })
consoleSilencer.restore()
})

describe('intent catalog', () => {
bench('cold catalogue generation through API', async () => {
await getIntentCatalogContext({ cwd: root, refresh: true })
})

bench('warm cached catalogue retrieval through API', async () => {
await getIntentCatalogContext({ cwd: root })
})

bench('warm cached catalogue retrieval through CLI', async () => {
await runner.run(['catalog', '--json'])
})
})
47 changes: 47 additions & 0 deletions benchmarks/intent/install-plan.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { bench, describe } from 'vitest'
import { updateIntentConsumerConfigText } from '../../packages/intent/src/commands/install/config.js'
import { buildSkillSelectionPlan } from '../../packages/intent/src/commands/install/plan.js'
import type { IntentPackage } from '../../packages/intent/src/shared/types.js'

const packages: Array<IntentPackage> = Array.from(
{ length: 20 },
(_, packageIndex) => ({
name: `@bench/package-${String(packageIndex).padStart(2, '0')}`,
version: '1.0.0',
kind: 'npm',
source: 'local',
packageRoot: `node_modules/@bench/package-${packageIndex}`,
intent: { version: 1, repo: 'bench/packages', docs: 'docs/' },
skills: Array.from({ length: 5 }, (_, skillIndex) => ({
name: `skill-${skillIndex}`,
path: `skills/skill-${skillIndex}/SKILL.md`,
description: `Skill ${skillIndex}`,
})),
}),
)

const packageJson = `${JSON.stringify(
{
name: 'install-plan-benchmark',
private: true,
intent: { skills: [], exclude: [] },
},
null,
2,
)}\n`

const selection = buildSkillSelectionPlan(packages, { mode: 'all-found' })

describe('installer planning', () => {
bench('plans 100 discovered skills', () => {
buildSkillSelectionPlan(packages, { mode: 'all-found' })
})

bench('updates consumer JSONC configuration', () => {
updateIntentConsumerConfigText(packageJson, {
skills: selection.skills,
exclude: selection.exclude,
install: { method: 'symlink', targets: ['agents'] },
})
})
})
25 changes: 25 additions & 0 deletions benchmarks/intent/lockfile-hash.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { rmSync } from 'node:fs'
import { join } from 'node:path'
import { afterAll, beforeAll, bench, describe } from 'vitest'
import { computeSkillContentHash } from '../../packages/intent/src/core/lockfile/hash.js'
import { createTempDir, writeFile } from './helpers.js'

const root = createTempDir('lockfile-hash')
const skillDir = join(root, 'skills', 'representative')

beforeAll(() => {
writeFile(join(skillDir, 'SKILL.md'), '# Guidance\n'.repeat(200))
writeFile(join(skillDir, 'references', 'api.md'), '# API\n'.repeat(200))
writeFile(join(skillDir, 'assets', 'example.json'), '{"enabled":true}\n')
writeFile(join(skillDir, 'scripts', 'check.mjs'), 'process.exit(0)\n')
})

afterAll(() => {
rmSync(root, { recursive: true, force: true })
})

describe('per-skill lock hashing', () => {
bench('hashes a representative skill folder', () => {
computeSkillContentHash({ packageRoot: root, skillDir })
})
})
77 changes: 77 additions & 0 deletions benchmarks/intent/sync.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { bench, describe } from 'vitest'
import { buildInstallDeltaInventory } from '../../packages/intent/src/commands/install/plan.js'
import { createSyncAliases } from '../../packages/intent/src/commands/sync/targets.js'
import type { IntentLockfileSource } from '../../packages/intent/src/core/lockfile/lockfile.js'
import type { IntentConsumerConfig } from '../../packages/intent/src/commands/install/config.js'
import type { IntentPackage } from '../../packages/intent/src/shared/types.js'

const packages: Array<IntentPackage> = Array.from(
{ length: 20 },
(_, packageIndex) => ({
name: `@bench/package-${String(packageIndex).padStart(2, '0')}`,
version: '1.0.0',
kind: 'npm',
source: 'local',
packageRoot: `node_modules/@bench/package-${packageIndex}`,
intent: { version: 1, repo: 'bench/packages', docs: 'docs/' },
skills: Array.from({ length: 5 }, (_, skillIndex) => ({
name: `skill-${skillIndex}`,
path: `skills/skill-${skillIndex}/SKILL.md`,
description: `Skill ${skillIndex}`,
})),
}),
)

const sources: Array<IntentLockfileSource> = packages.map((pkg) => ({
kind: pkg.kind,
id: pkg.name,
skills: pkg.skills.map((skill) => ({
path: `skills/${skill.name}`,
contentHash: `${pkg.name}-${skill.name}`,
})),
}))

const config: IntentConsumerConfig = {
skills: ['@bench/*'],
exclude: [],
install: { method: 'symlink', targets: ['agents'] },
}

describe('sync planning', () => {
bench('plans unchanged representative sources and aliases', () => {
buildInstallDeltaInventory(
packages,
sources,
{ status: 'found', lockfile: { lockfileVersion: 1, sources } },
config,
)
createSyncAliases(
packages.flatMap((pkg) =>
pkg.skills.map((skill) => ({
kind: pkg.kind,
id: pkg.name,
skill: skill.name,
})),
),
)
})

bench('plans changed and pending sources', () => {
const changed = sources.map((source, index) =>
index === 0
? {
...source,
skills: source.skills.map((skill, skillIndex) =>
skillIndex === 0 ? { ...skill, contentHash: 'changed' } : skill,
),
}
: source,
)
buildInstallDeltaInventory(
packages,
changed,
{ status: 'found', lockfile: { lockfileVersion: 1, sources } },
{ ...config, skills: ['@bench/package-00'] },
)
})
})
1 change: 0 additions & 1 deletion benchmarks/intent/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"noEmit": true
},
"include": ["*.ts"]
Expand Down
8 changes: 7 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
]
},
"packages/intent": {
"entry": ["src/index.ts", "src/cli.ts", "src/core.ts", "src/setup.ts"],
"entry": [
"src/index.ts",
"src/cli.ts",
"src/core.ts",
"src/setup.ts",
"src/catalog.ts"
],
"ignoreDependencies": ["verdaccio"]
}
}
Expand Down
10 changes: 8 additions & 2 deletions packages/intent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"./core": {
"import": "./dist/core.mjs",
"types": "./dist/core.d.mts"
},
"./catalog": {
"import": "./dist/catalog.mjs",
"types": "./dist/catalog.d.mts"
}
},
"bin": {
Expand All @@ -26,6 +30,7 @@
"meta"
],
"dependencies": {
"@clack/prompts": "1.7.0",
"cac": "^6.7.14",
"jsonc-parser": "^3.3.1",
"semver": "^7.8.4",
Expand All @@ -39,9 +44,10 @@
},
"scripts": {
"prepack": "npm run build",
"build": "tsdown src/index.ts src/cli.ts src/setup.ts src/core.ts --format esm --dts",
"test:smoke": "pnpm run build && node dist/cli.mjs --help > /dev/null && node dist/cli.mjs load --help > /dev/null",
"build": "tsdown src/index.ts src/cli.ts src/setup.ts src/core.ts src/catalog.ts --format esm --dts",
"test:smoke": "pnpm run build && node dist/cli.mjs --help && node dist/cli.mjs load --help && node --input-type=module -e \"const api = await import('@tanstack/intent/catalog'); if (typeof api.getIntentCatalogContext !== 'function' || typeof api.runSessionCatalogueHook !== 'function') process.exit(1)\"",
"test:lib": "vitest run --exclude 'tests/integration/**'",
"test:delivery": "vitest run tests/sync.test.ts",
"test:integration": "vitest run tests/integration/",
"test:types": "tsc --noEmit",
"test:eslint": "eslint ."
Expand Down
Loading