Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
68ee85b
feat(lockfile): implement intent lockfile structure and serialization…
LadyBluenotes Jul 5, 2026
e6a1b4c
feat(hash): add hashing functions and tests for skill folder and sour…
LadyBluenotes Jul 5, 2026
45600e4
feat(hash): optimize file reading with file descriptor management
LadyBluenotes Jul 5, 2026
a1f1309
feat(lockfile): implement lockfile diffing and state management
LadyBluenotes Jul 5, 2026
56ff8af
feat: optimize sorting and comparison functions across multiple files
LadyBluenotes Jul 5, 2026
915b1a8
feat(skills): implement scan and diff commands for skills management
LadyBluenotes Jul 5, 2026
b982fa0
feat(skills): enhance scan and diff commands to handle unlisted skill…
LadyBluenotes Jul 6, 2026
7959986
feat(skills): add 'approve' action to skills command for managing int…
LadyBluenotes Jul 6, 2026
e7269ac
refactor(lockfile): change export to const and update interface visib…
LadyBluenotes Jul 6, 2026
e330d7c
feat: implement frozen mode checks to prevent network calls and subpr…
LadyBluenotes Jul 6, 2026
4dba814
feat(skills): add 'update' action to skills command for managing inte…
LadyBluenotes Jul 6, 2026
8120e0d
refactor(skills): replace parseSourceArg with resolveSourceArg for im…
LadyBluenotes Jul 6, 2026
4c5179c
feat: add skills support to lockfile and related structures
LadyBluenotes Jul 6, 2026
a3bb1f3
feat(skills): enhance update command to report pending added/removed …
LadyBluenotes Jul 9, 2026
50adc0e
fix: update --no-frozen option behavior to override INTENT_FROZEN and…
LadyBluenotes Jul 9, 2026
aee4218
feat: add documentation for intent skills and lockfile management
LadyBluenotes Jul 9, 2026
54321cd
feat: add skills stale command for checking staleness of skills again…
LadyBluenotes Jul 9, 2026
db658f1
feat: implement skills manifest generation and enhance CLI commands
LadyBluenotes Jul 9, 2026
5229dfa
feat: update documentation to include new `stale` and `generate-manif…
LadyBluenotes Jul 9, 2026
014c3cc
feat: enhance hashing logic to include reference files in skill folders
LadyBluenotes Jul 9, 2026
cc5f509
feat: enhance source content hash computation with support file modif…
LadyBluenotes Jul 9, 2026
0946533
feat: refactor file reading logic to use a customizable filesystem in…
LadyBluenotes Jul 9, 2026
cc3e63e
feat: enhance manifest reading and lockfile source building with cust…
LadyBluenotes Jul 9, 2026
4e10b0d
feat: improve package registration logic to handle duplicates and enh…
LadyBluenotes Jul 9, 2026
999deb8
feat: enhance package registrar and skill load path handling with pac…
LadyBluenotes Jul 9, 2026
24eb5de
feat: preserve staleness metadata in lockfile during approve and upda…
LadyBluenotes Jul 9, 2026
813c4af
feat: preserve metadata during source approval and update processes
LadyBluenotes Jul 9, 2026
4529da4
feat: enhance provenance tracking for skills and packages across comm…
LadyBluenotes Jul 9, 2026
13dc1fe
feat: update provenance messaging to indicate when provenance is unknown
LadyBluenotes Jul 9, 2026
1ba0f08
feat: enhance provenance tracking for direct and transitive skill pac…
LadyBluenotes Jul 9, 2026
af042bf
feat: enhance exclude matching to support kind-qualified package patt…
LadyBluenotes Jul 9, 2026
cba03c4
feat: implement validation for canonical package relative paths in lo…
LadyBluenotes Jul 9, 2026
daeea5e
refactor: simplify path validation logic in resolveCanonicalPackagePa…
LadyBluenotes Jul 9, 2026
00d1f37
feat: add canonicalization for manifest serialization and enhance MCP…
LadyBluenotes Jul 10, 2026
b686e87
feat: enhance currentBlobSha function with error handling for directo…
LadyBluenotes Jul 10, 2026
2f13bbe
feat: add validation for hash limits in source content and skill fold…
LadyBluenotes Jul 10, 2026
15f1d86
feat: add readSkillFolderContents function and update generateManifes…
LadyBluenotes Jul 10, 2026
d2d0a83
feat: update lockfile documentation and enhance frozen mode error mes…
LadyBluenotes Jul 10, 2026
d8c62c2
feat: update lockfile documentation to include consumer CI instructio…
LadyBluenotes Jul 10, 2026
5b545d5
feat: add lockfile scan benchmarks and update writeFile function to a…
LadyBluenotes Jul 10, 2026
5637107
feat: export lockfile types and add tests for public lockfile types
LadyBluenotes Jul 10, 2026
e00c502
feat: enhance documentation and error handling for lockfile and skill…
LadyBluenotes Jul 10, 2026
34d4644
feat: enhance skills generate manifest command to handle frozen mode …
LadyBluenotes Jul 10, 2026
570df5f
feat: enhance baseline drift computation to skip installed dependenci…
LadyBluenotes Jul 10, 2026
b43215b
feat: implement frozen mode checks for unlisted skill-bearing sources…
LadyBluenotes Jul 10, 2026
6dfdbb9
feat: enhance intent skills and lockfile management
LadyBluenotes Jul 10, 2026
8d43fd3
changeset
LadyBluenotes Jul 10, 2026
3d59f20
feat: enhance skills update command to require approval for trust-bea…
LadyBluenotes Jul 10, 2026
215e2db
ci: apply automated fixes
autofix-ci[bot] Jul 10, 2026
625e532
refactor: optimize package grouping in resolveSkillUse function and u…
LadyBluenotes Jul 10, 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
14 changes: 14 additions & 0 deletions .changeset/many-forks-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@tanstack/intent': minor
---

Add consumer-managed `intent.lock` files for reviewing and pinning the exact skill content approved for a project.

- Add `intent skills scan`, `diff`, `approve`, and `update` for inspecting drift, reviewing changes, and maintaining approved sources.
- Track sources by `(kind, id)` so same-named workspace and npm packages remain distinct approvals.
- Hash each skill’s `SKILL.md` plus supported `references/`, `assets/`, and `scripts/` files with deterministic SHA-256 content hashes.
- Add frozen-mode enforcement for CI through `--frozen`, `INTENT_FROZEN`, and non-interactive `CI` detection. Frozen mode rejects missing or malformed lockfiles, unapproved source changes, hidden skill-bearing sources, and lockfile mutations.
- Add `intent skills generate-manifest` for package maintainers to create `skills/intent.manifest.json` files containing per-skill hashes, declared capabilities, secret names, and MCP metadata.
- Validate manifests against the installed package identity, skill paths, and content hashes. Manifest metadata changes appear in lockfile diffs and require approval before frozen checks pass.
- Add `intent skills stale` to surface lockfile drift and Git-baseline skill changes for review.
- Export lockfile and manifest metadata types from `@tanstack/intent`.
19 changes: 19 additions & 0 deletions benchmarks/intent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Intent Benchmarks

## Lockfile Scan Baseline

Run from the repository root:

```sh
pnpm --dir benchmarks/intent exec vitest bench --config ./vitest.config.ts ./lockfile-scan.bench.ts
```

Local baseline recorded on 2026-07-09:

| Case | Fixture | Mean |
| ----------------- | --------------------------------------------------------------------- | ---------: |
| Clean lockfile | 8 packages, 3 skills per package, 3 support files per skill at 1 KiB | 9.2180 ms |
| Changed skill | Same fixture, one `SKILL.md` changed after approval | 9.1127 ms |
| Large support set | 24 packages, 4 skills per package, 6 support files per skill at 8 KiB | 50.7923 ms |

The fixture creates `intent.lock` with `intent skills approve --all --yes` before each scan. Re-run these cases after changing lockfile discovery or hashing and compare the same fixture means.
2 changes: 1 addition & 1 deletion benchmarks/intent/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function createTempDir(name: string): string {
return mkdtempSync(join(tmpdir(), `intent-bench-${name}-`))
}

export function writeFile(filePath: string, content: string): void {
export function writeFile(filePath: string, content: string | Buffer): void {
mkdirSync(dirname(filePath), { recursive: true })
writeFileSync(filePath, content)
}
Expand Down
137 changes: 137 additions & 0 deletions benchmarks/intent/lockfile-scan.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { rmSync } from 'node:fs'
import { join } from 'node:path'
import { afterAll, beforeAll, bench, describe } from 'vitest'
import {
createBenchOptions,
createCliRunner,
createConsoleSilencer,
createTempDir,
writeFile,
writeJson,
writePackage,
} from './helpers.js'

type LockfileFixture = {
root: string
runner: ReturnType<typeof createCliRunner>
}

type FixtureOptions = {
changedSkill?: boolean
packageCount: number
skillCount: number
supportFileCount: number
supportFileSize: number
}

function createFixture(name: string, options: FixtureOptions): LockfileFixture {
const root = createTempDir(name)
const packageNames = Array.from(
{ length: options.packageCount },
(_, index) => `@bench/lock-${index}`,
)
writeJson(join(root, 'package.json'), {
name: `intent-lockfile-${name}-benchmark`,
private: true,
intent: { skills: packageNames },
})

for (const packageName of packageNames) {
const skills = Array.from(
{ length: options.skillCount },
(_, index) => `skill-${index}`,
)
writePackage(join(root, 'node_modules'), packageName, '1.0.0', { skills })
const packageRoot = join(root, 'node_modules', ...packageName.split('/'))
for (const skillName of skills) {
const skillDir = join(packageRoot, 'skills', skillName)
for (let index = 0; index < options.supportFileCount; index++) {
const directory = ['references', 'assets', 'scripts'][index % 3]!
const extension = directory === 'scripts' ? 'mjs' : 'dat'
const content =
directory === 'assets'
? Buffer.alloc(options.supportFileSize, index)
: 'x'.repeat(options.supportFileSize)
writeFile(
join(skillDir, directory, `support-${index}.${extension}`),
content,
)
}
}
}

return { root, runner: createCliRunner({ cwd: root }) }
}

function defineScenario(name: string, options: FixtureOptions): void {
const consoleSilencer = createConsoleSilencer()
let fixture: LockfileFixture | null = null

async function setup(): Promise<void> {
if (fixture) return

consoleSilencer.silence()
fixture = createFixture(name, options)
await fixture.runner.setup()
await fixture.runner.run(['skills', 'approve', '--all', '--yes'])
if (options.changedSkill) {
writeFile(
join(
fixture.root,
'node_modules',
'@bench',
'lock-0',
'skills',
'skill-0',
'SKILL.md',
),
'---\nname: skill-0\ndescription: changed benchmark skill\n---\n\nChanged.\n',
)
}
}

function teardown(): void {
if (fixture) {
fixture.runner.teardown()
rmSync(fixture.root, { recursive: true, force: true })
fixture = null
}
consoleSilencer.restore()
}

describe(`intent skills scan --json (${name})`, () => {
beforeAll(setup)
afterAll(teardown)

bench(
'scans lockfile state',
async () => {
if (!fixture) throw new Error('Lockfile fixture was not initialized')
await fixture.runner.run(['skills', 'scan', '--json'])
},
createBenchOptions(setup, teardown),
)
})
}

defineScenario('clean-eight-packages', {
packageCount: 8,
skillCount: 3,
supportFileCount: 3,
supportFileSize: 1024,
})

defineScenario('changed-skill', {
changedSkill: true,
packageCount: 8,
skillCount: 3,
supportFileCount: 3,
supportFileSize: 1024,
})

defineScenario('large-support-set', {
packageCount: 24,
skillCount: 4,
supportFileCount: 6,
supportFileSize: 8 * 1024,
})
6 changes: 4 additions & 2 deletions docs/cli/intent-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ The list as a whole has three special forms:
- **Empty** (`"skills": []`): no package is surfaced, with an info notice printed to stderr.
- **Wildcard** (`"skills": ["*"]`): every discovered package is surfaced, with an acknowledged-risk notice printed to stderr.

A package that ships skills but is not listed is dropped. When packages are dropped this way, Intent prints one summary line naming them so you can opt in. In agent sessions, hidden sources are reported by count only; run `intent list --show-hidden` outside the agent session to review candidates. A listed package that was not discovered is reported as well. Matching is currently by package name. See [Configuration](../concepts/configuration) and [Trust model](../concepts/trust-model).
A package that ships skills but is not listed is dropped. Human-facing output includes bounded dependency provenance when available, otherwise `provenance unknown`. In agent sessions, hidden sources are reported by count only; run `intent list --show-hidden` outside the agent session to review candidates. A listed package that was not discovered is reported as well. Matching uses `(kind, id)`. See [Configuration](../concepts/configuration) and [Trust model](../concepts/trust-model).

When an npm and workspace source share a name and the requested skill, `intent load` rejects the unqualified use as ambiguous.

## Excludes

Expand All @@ -133,7 +135,7 @@ Manage persistent excludes with `intent exclude add|remove|list`.
}
```

A pattern without `#` excludes a whole package. A pattern with `#` excludes a single skill (`@scope/pkg#search-params`), and the skill segment may itself be a glob (`@scope/pkg#experimental-*`). A pattern may cross package boundaries at skill granularity (`*#experimental-*`). The `#*` shortcut (`@scope/pkg#*`) excludes the whole package. Only exact names and `*` wildcards are supported on each segment. Bare package-name patterns keep working unchanged.
A pattern without `#` excludes a whole package. A pattern with `#` excludes a single skill (`@scope/pkg#search-params`), and the skill segment may itself be a glob (`@scope/pkg#experimental-*`). A pattern may cross package boundaries at skill granularity (`*#experimental-*`). The `#*` shortcut (`@scope/pkg#*`) excludes the whole package. Prefix a package segment with `npm:` or `workspace:` to target one source kind. Only exact names and `*` wildcards are supported on each segment. Bare package-name patterns keep working unchanged.

An excluded package never triggers the unlisted-source warning, because an exclude is an explicit decision rather than an oversight.

Expand Down
125 changes: 125 additions & 0 deletions docs/cli/intent-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: intent skills
id: intent-skills
---

`intent skills` manages `intent.lock`, the committed record of which skill-bearing sources you've approved and what their content looked like when you approved it. Six subcommands: `scan`, `diff` (read-only), `approve`, `update` (mutating), `stale` (read-only, checks for skill drift needing re-review), and `generate-manifest` (maintainer-only — writes a package's own `skills/intent.manifest.json`, never touches `intent.lock`).

```bash
npx @tanstack/intent@latest skills <scan|diff|approve|update|stale|generate-manifest> [source] [--json] [--all] [--yes] [--frozen] [--no-frozen] [--baseline <ref>] [--files <path...>]
```

See [Lockfile and frozen mode](../security/lockfile) for what `intent.lock` is and what frozen mode guarantees.

## `intent skills scan`

```bash
npx @tanstack/intent@latest skills scan [--json] [--frozen] [--no-frozen]
```

Read-only. Discovers current skill-bearing sources, computes each source's `contentHash`, and reports drift against `intent.lock`.

- No lock found: prints `No intent.lock found. Run \`intent skills approve --all\` to create one.`
- Lock is clean: prints `intent.lock is up to date.`
- Lock is stale: prints `intent.lock is out of date: N added, N removed, N changed.`
- Discovered sources not in `intent.skills`: prints a count and points at `intent.skills`/`intent.exclude`
- `--json` prints `{ frozen, hiddenSourceCount, hasLockfile, added, removed, changed, isClean }`

## `intent skills diff`

```bash
npx @tanstack/intent@latest skills diff [--json] [--frozen] [--no-frozen]
```

Read-only. Same underlying computation as `scan`, but change-focused: prints only `Added:`/`Removed:`/`Changed:` sections with per-field diffs (`version`, `resolution`, `skills`, `contentHash`, `manifestHash`, `capabilities`). Unchanged sources are omitted.

```
Changed:
~ npm:@acme/query
version: "1.0.0" -> "1.1.0"
resolution: "npm:@acme/query@1.0.0" -> "npm:@acme/query@1.1.0"
contentHash: "sha256-492ac4..." -> "sha256-2631b3..."
```

## `intent skills approve [source]`

```bash
npx @tanstack/intent@latest skills approve [source] [--all] [--yes]
```

Writes `intent.lock`. This is the trust decision — approving means a human reviewed this exact change.

- **No arg, no `--all`/`--yes`:** interactive per-pending-change prompt (approve/skip each). Fails if stdin isn't a TTY.
- **`--all` or `--yes`:** accepts every pending change (added, removed, changed) without prompting. This is the first-run path that creates the initial lock.
- **A single source:** `approve npm:@tanstack/query`, `approve workspace:my-package`, or a bare name (`approve foo`) if it resolves unambiguously against currently-discovered sources. Two sources sharing a bare name across kinds (`npm:foo` and `workspace:foo`) error instead of guessing — pass `kind:id` explicitly.
- Re-serializes the whole file deterministically: identical inputs produce a byte-identical `intent.lock`.
- Only touches the targeted entry (single-source form) or all pending changes (`--all`/`--yes`) — never silently drops an entry you didn't act on.
- Refuses in frozen mode (exit `5`).

## `intent skills update [source]`

```bash
npx @tanstack/intent@latest skills update [source] [--all] [--yes]
```

Writes `intent.lock`. It mechanically re-syncs version and resolution for matching **already-locked** entries. Changes to skills, content hashes, manifests, capabilities, declared secrets, or MCP metadata require `--yes` after reviewing `intent skills diff`.

- Only touches sources present in **both** the lock and the current scan. It never adds a newly-discovered source (that's `approve`'s job) and never drops a source that's no longer discovered (also `approve`'s job — removing a source from the trust boundary is itself a trust decision).
- Reports pending added/removed drift it didn't touch: `N added, M removed source(s) still pending. Run \`intent skills approve\` to review.`
- Makes zero network calls and zero subprocess calls — it only reads what's already on disk.
- Refuses in frozen mode (exit `5`).

## `intent skills stale`

```bash
npx @tanstack/intent@latest skills stale [--json] [--baseline <ref>] [--files <path...>] [--frozen] [--no-frozen]
```

Read-only. Surfaces staleness **candidates** for human/agent review — never a hard verdict on its own (a candidate means "worth checking," not "broken"). Two layers:

- **Self-integrity + version** (Layer 0/1): reuses the same `intent.lock` diff as `scan`/`diff` — a source whose on-disk `contentHash` or `version` no longer matches the lock is reported as a candidate.
- **Baseline drift** (Layer 2): compares each locked source's tracked skill files against a git baseline via blob SHA, independent of the lockfile diff. Baseline resolution order: `--baseline <ref>` flag, then `intent.lock`'s recorded `staleness.baseline`, then the nearest local git tag. No implicit `HEAD~1` — pass `--baseline HEAD~1` explicitly if that's what you want.
- `--files <path...>` restricts Layer 2 to specific repo-relative paths (CI optimization: pass only the files a diff touched).
- No `intent.lock`: prints `No intent.lock found. Run \`intent skills approve --all\` to create one.` (frozen mode fails, exit `4`).
- No baseline resolvable: interactive mode reports `Layer 2 (baseline drift) skipped: <reason>` and continues with Layer 0/1 only; frozen mode fails closed (exit `5`).
- Makes no network calls. Requires git for Layer 2 only — if `cwd` isn't a git repository, Layer 2 fails the same way as an unresolvable baseline.
- Frozen mode: fails (exit `1`) if any candidate — Layer 0/1 or Layer 2 — was found, so CI gates a PR that hasn't refreshed staleness.

## `intent skills generate-manifest`

```bash
npx @tanstack/intent@latest skills generate-manifest [--json]
```

Maintainer-only. Writes `skills/intent.manifest.json` inside each discovered package, never `intent.lock`. It refuses frozen mode. For each skill folder, computes a content hash over `SKILL.md` plus any `references/`, `assets/`, and `scripts/` files. The consumer lockfile aggregate uses the same directory scope. Static heuristics pre-fill `capabilities`: `uses_network` (curl/wget/fetch reference), `runs_install_command` (npm/pnpm/yarn/bun/pip install reference), `ships_scripts` (non-empty `scripts/` dir). Heuristics only ever suggest. Review and edit the generated file before committing.

- **Hard-fails generation** (no partial manifest written) if any hash-included skill file contains what looks like a literal secret *value* (GitHub/Slack tokens, AWS access key IDs, a PEM private key block, a generic `key = "..."` assignment). A secret's *name* belongs in the manifest's `declaredSecrets`, never its value in skill content.
- Once a package ships a manifest, `intent skills scan`/`diff`/`approve` pick up its `manifestHash` and unioned `capabilities` automatically — no separate wiring needed on the consumer side.
- After a package changes manifest metadata, consumers run `intent skills diff` and review it with `intent skills approve`. Frozen scans reject the changed `manifestHash` until that approval updates `intent.lock`.

## Options

- `--json`: with `scan`/`diff`, print the structured diff instead of text
- `--all`: with `approve`/`update`, act on all pending changes without prompting
- `--yes`: with `approve`, accept all pending changes non-interactively; with `update`, accept reviewed trust-bearing changes
- `--frozen`: force frozen mode, regardless of `INTENT_FROZEN`/`CI` auto-detection
- `--no-frozen`: force interactive mode — overrides `INTENT_FROZEN` and the `CI` auto-detect (highest-precedence explicit override)
- `--baseline <ref>`: with `stale`, override the git ref used as the staleness baseline
- `--files <path...>`: with `stale`, restrict Layer 2 baseline drift checks to these repo-relative paths

## Exit codes

| Code | Meaning |
| --- | --- |
| `0` | ok |
| `1` | generic CLI usage/parse error, or (`stale` only) staleness candidates found under frozen mode |
| `2` | drift found under frozen mode |
| `3` | unapproved/unlisted skill-bearing source found under frozen mode |
| `4` | no `intent.lock` found under frozen mode |
| `5` | `approve`/`update` refused — frozen mode disallows mutation; or (`stale` only) no staleness baseline resolvable under frozen mode |
| `6` | `intent.lock` is malformed or from an unsupported (newer) `lockfileVersion` |

## Related

- [Lockfile and frozen mode](../security/lockfile)
- [Trust model](../concepts/trust-model)
8 changes: 6 additions & 2 deletions docs/concepts/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ Each array entry names one source:
| `workspace:@scope/pkg` | workspace | A package in the current workspace. |
| `git:<host>/<repo>#<ref>` | git | Reserved. Not yet supported, and rejected until a future version adds it. |

A malformed entry fails the whole command, and every bad entry is reported at once. Intent currently matches an allowlist entry against a discovered package by name. This matching will tighten in a future version.
A malformed entry fails the whole command, and every bad entry is reported at once. Intent matches a source by `(kind, id)`: `workspace:foo` never authorizes an npm-installed `foo`.

If both `npm:foo` and `workspace:foo` provide the same requested skill, `intent load foo#skill` fails as ambiguous instead of selecting one source by discovery order. Narrow the allowlist to one source before loading it.

### Special forms

Expand All @@ -40,7 +42,7 @@ The list as a whole has three special forms:
- **Empty.** `"skills": []`. No package is surfaced. Intent prints an info notice to stderr.
- **Wildcard.** `"skills": ["*"]`. Every discovered package is surfaced. Intent prints an acknowledged-risk notice to stderr, since unvetted skills may reach your agent.

A package that ships skills but is not listed is dropped. When packages are dropped this way, Intent prints one summary line naming them so you can opt in. A listed package that was not discovered is reported as well.
A package that ships skills but is not listed is dropped. Human-facing output includes bounded dependency provenance when available, otherwise `provenance unknown`. A listed package that was not discovered is reported as well.

### Existing projects

Expand Down Expand Up @@ -85,4 +87,6 @@ Pattern grammar:
- A pattern may cross package boundaries at skill granularity: `*#experimental-*`.
- The `#*` shortcut excludes the whole package: `@scope/pkg#*`.

Prefix a package segment with `npm:` or `workspace:` to target one source kind, for example `workspace:foo` or `npm:foo#experimental-*`. Bare package patterns remain broad and match either kind.

Only exact names and `*` wildcards are supported on each segment. Bare package-name patterns keep working unchanged. An excluded package does not trigger the unlisted-source warning, because an exclude is an explicit decision.
Loading
Loading