diff --git a/.changeset/scaffold-skills-under-src.md b/.changeset/scaffold-skills-under-src.md new file mode 100644 index 000000000..ebacc9bec --- /dev/null +++ b/.changeset/scaffold-skills-under-src.md @@ -0,0 +1,5 @@ +--- +"create-agent-bundle": patch +--- + +Scaffold the minimal template's conventional Skill under `src/skills/`. diff --git a/.changeset/skills-under-src.md b/.changeset/skills-under-src.md new file mode 100644 index 000000000..0354a3802 --- /dev/null +++ b/.changeset/skills-under-src.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": minor +--- + +Move conventional authored documents under `src/`: skills now use `src/skills/`, commands use `src/commands/`, and rules use `src/rules/`. Top-level conventional documents are no longer discovered and report AB4736 unless a legacy skill is claimed by explicit `skills` configuration. Explicit skill paths remain supported anywhere, and published artifact paths are unchanged. diff --git a/README.md b/README.md index 55c6d6118..fa2703dd5 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ import { defineConfig } from 'agent-bundle/config'; export default defineConfig({ plugin: { name: 'my-plugin', version: '0.1.0', description: 'What it does.' }, targets: ['plugin'], - skills: ['skills/*'], + skills: ['src/skills/*'], hooks: { sessionStart: { handler: './src/session-start.ts' } }, mcp: { servers: { tools: { entry: './src/mcp.ts' } } }, }); @@ -55,7 +55,7 @@ Claude Code language servers are declared under `claude.lspServers`; the `claude Claude Code plugin defaults are declared under `claude.settings` and emitted as plugin-root `settings.json`, which Claude Code applies when the plugin is enabled. The pinned contract supports only `agent` and `subagentStatusLine`; Agent Bundle rejects any other key rather than shipping a default Claude Code would silently ignore, and it expands no path tokens here because `settings.json` is absent from the host's placeholder-substitution table. Because the plugin `agents/` component is still deferred, declaring `agent` also raises a warning: the referenced agent has to reach the plugin root some other way, such as a prebuilt payload. -The same config also owns the npm package build — no second bundler config, bin shims, or hand-rolled stdio lifecycles. `bin` and `lib` entries (or the conventions `src/cli.ts`, `src/index.ts`, and `src/mcp/.ts`) emit executable `dist/bin/.js` bundles and a library output alongside the host artifacts; an MCP entry that default-exports a server factory runs under a framework-owned stdio lifecycle; `tools.rsbuild` / `tools.rspack` is the one bundler escape hatch. [Entry conventions](docs/entry-conventions.md) is the full contract, and [Framework mode](docs/framework-mode.md) is the whole authoring model on one screen: structure in config and conventions (`skills//SKILL.md` ships with no declaration at all), JSX only where something is rendered. +The same config also owns the npm package build — no second bundler config, bin shims, or hand-rolled stdio lifecycles. `bin` and `lib` entries (or the conventions `src/cli.ts`, `src/index.ts`, and `src/mcp/.ts`) emit executable `dist/bin/.js` bundles and a library output alongside the host artifacts; an MCP entry that default-exports a server factory runs under a framework-owned stdio lifecycle; `tools.rsbuild` / `tools.rspack` is the one bundler escape hatch. [Entry conventions](docs/entry-conventions.md) is the full contract, and [Framework mode](docs/framework-mode.md) is the whole authoring model on one screen: structure in config and conventions (`src/skills//SKILL.md` ships with no declaration at all), JSX only where something is rendered. ## Commands diff --git a/docs/diagnostics.md b/docs/diagnostics.md index c77093a71..e5adf0d9b 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -108,15 +108,16 @@ development-only fallback can never produce a release artifact, so | `AB4011` | warning | `package.json` is unusable — unparsable, not a JSON object, or symlinked outside the project root. | | `AB4013` | error (build) | `agent-bundle build` refuses a project with no release version: `plugin.version` is omitted and `package.json` declares no valid semantic version. | -## Migration nudges (`AB4730`–`AB4735`) +## Migration nudges (`AB4730`–`AB4736`) The entry conventions and the framework-owned stdio lifecycle shell (RFC #50) replaced patterns consumers previously wrote by hand. When `validate`, `inspect`, `build`, or `dev` prepares project source and finds one of those -pre-convention patterns, it reports an **informational** nudge. Nudges are -never errors and never block anything — migrations stay optional, per the -RFC's additive-first principle. The CLI prints them in human `validate` -output and includes them in every `--json` diagnostics array. +pre-convention patterns, it reports a migration diagnostic. `AB4730`–`AB4735` +are **informational** nudges and never block anything. `AB4736` is an error: +the removed top-level authored-document locations are no longer discovered, +so the compiler refuses to omit them silently. The CLI prints these in human +`validate` output and includes them in every `--json` diagnostics array. ### `AB4730` — self-connecting stdio MCP entry @@ -158,10 +159,10 @@ Silence: remove the shadowed file. ### `AB4734` — conventional skill shadowed by explicit `skills` config -A `skills//SKILL.md` (or rendered `SKILL.tsx`/`SKILL.ts`) directory +A `src/skills//SKILL.md` (or rendered `SKILL.tsx`/`SKILL.ts`) directory exists, but the explicit `skills` configuration does not cover it — the conventional skill is silently shadowed. When config is silent, every -`skills//` directory ships by convention and this nudge never fires. +`src/skills//` directory ships by convention and this nudge never fires. Adopt: remove the explicit `skills` configuration so the convention applies, or add the directory to `skills`. Silence: remove the directory. @@ -175,6 +176,19 @@ document beats a generated one — so the component module never compiles. Adopt: remove `SKILL.md` so the rendered skill compiles at build. Silence: remove the component module. +### `AB4736` — legacy top-level authored document location + +A document still matches a removed top-level convention: +`skills//SKILL.md` (or rendered `SKILL.tsx`/`SKILL.ts`), +`commands/*.md`, or `rules/*.mdc`. These locations are no longer discovered, +and every unignored legacy document is reported as an error. A top-level +skill covered by explicit `skills` configuration is claimed and stays valid; +commands and rules have no equivalent override. + +Recover: move the document under `src/skills/`, `src/commands/`, or +`src/rules/`. Explicit `skills` paths remain valid anywhere. Published +artifact paths remain `skills/`, `commands/`, and `rules/`. + ## Prebuilt payloads (`AB4740`–`AB4750`) The `payload` block and `{ prebuilt: ... }` entries (see diff --git a/docs/framework-mode.md b/docs/framework-mode.md index 274797d04..80b1a8e94 100644 --- a/docs/framework-mode.md +++ b/docs/framework-mode.md @@ -2,10 +2,12 @@ Agent Bundle has one newcomer model: -1. **Files under conventional `src/` roots are the app.** For MCP, put one +1. **Authored source lives under `src/`.** For MCP, put one module at `src/mcp//{tools,resources,prompts}/.tsx`; its path - is its identity. `skills//SKILL.md`, `src/scripts/.ts`, - `src/cli.ts`, and `src/index.ts` keep their existing conventions. + is its identity. Skills, commands, rules, scripts, routes, and state use + their conventional `src/` roots, including `src/skills//SKILL.md`, + `src/commands/*.md`, and `src/rules/*.mdc`. Top-level `assets/` holds + static resources, and `agent-bundle.config.ts` stays at the project root. 2. **One small flat config.** `agent-bundle.config.ts` holds project identity, targets, and policy that no route file can own. 3. **JSX = rendering.** An executable route is one async default Server diff --git a/examples/audiobook-curator/README.md b/examples/audiobook-curator/README.md index 2c1a74c75..e4d37c88d 100644 --- a/examples/audiobook-curator/README.md +++ b/examples/audiobook-curator/README.md @@ -203,7 +203,7 @@ The completion contract and real-volume checklist are in This example is the reference consumer of the framework-owned package build: one `agent-bundle.config.ts` declares the structure, conventional -`src/mcp/**`, `src/cli/**`, `src/providers/**`, and `src/state.ts` modules supply -the application surfaces, and agent-bundle owns the generated package and host -artifacts. See [`docs/entry-conventions.md`](../../docs/entry-conventions.md) -for the contract. +`src/skills/**`, `src/mcp/**`, `src/cli/**`, `src/providers/**`, and +`src/state.ts` modules supply the application surfaces, and agent-bundle owns +the generated package and host artifacts. See +[`docs/entry-conventions.md`](../../docs/entry-conventions.md) for the contract. diff --git a/examples/audiobook-curator/agent-bundle.config.ts b/examples/audiobook-curator/agent-bundle.config.ts index 060020157..4441cf0f9 100644 --- a/examples/audiobook-curator/agent-bundle.config.ts +++ b/examples/audiobook-curator/agent-bundle.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ // No `scripts` or `bin` fields needed: the routed `src/cli/` commands // compile into the package executable (dist/bin/audiobook-curator.js) by // convention (#102 stages 2-3). - // No `skills` field needed: `skills/curate-audiobooks/SKILL.md` is + // No `skills` field needed: `src/skills/curate-audiobooks/SKILL.md` is // discovered by convention. targets: ['claude', 'codex'], }); diff --git a/examples/audiobook-curator/skills/curate-audiobooks/SKILL.md b/examples/audiobook-curator/src/skills/curate-audiobooks/SKILL.md similarity index 100% rename from examples/audiobook-curator/skills/curate-audiobooks/SKILL.md rename to examples/audiobook-curator/src/skills/curate-audiobooks/SKILL.md diff --git a/examples/mcp-app/README.md b/examples/mcp-app/README.md index 8173557cb..f72811d96 100644 --- a/examples/mcp-app/README.md +++ b/examples/mcp-app/README.md @@ -13,7 +13,7 @@ and Claude artifacts; the App resource remains portable. ## What is authored -- `skills/service-readiness` documents the evidence, checks, and report needed +- `src/skills/service-readiness` documents the evidence, checks, and report needed for a service-readiness decision. - `src/hooks/session-start.ts` adds the readiness workflow to compatible host sessions, while `check-service-fixture` validates the checked-in compiler diff --git a/examples/mcp-app/skills/service-readiness/SKILL.md b/examples/mcp-app/src/skills/service-readiness/SKILL.md similarity index 100% rename from examples/mcp-app/skills/service-readiness/SKILL.md rename to examples/mcp-app/src/skills/service-readiness/SKILL.md diff --git a/examples/mcp-app/skills/service-readiness/assets/readiness-report.md b/examples/mcp-app/src/skills/service-readiness/assets/readiness-report.md similarity index 100% rename from examples/mcp-app/skills/service-readiness/assets/readiness-report.md rename to examples/mcp-app/src/skills/service-readiness/assets/readiness-report.md diff --git a/examples/mcp-app/skills/service-readiness/references/status-policy.md b/examples/mcp-app/src/skills/service-readiness/references/status-policy.md similarity index 100% rename from examples/mcp-app/skills/service-readiness/references/status-policy.md rename to examples/mcp-app/src/skills/service-readiness/references/status-policy.md diff --git a/examples/skills-starter/README.md b/examples/skills-starter/README.md index b780389f5..6b6232fac 100644 --- a/examples/skills-starter/README.md +++ b/examples/skills-starter/README.md @@ -13,14 +13,14 @@ required. Both eval suites are deterministic and read only checked-in fixtures. ## What is authored - `agent-bundle.config.ts` declares the plugin and its portable, Codex, and - Claude targets. The Skills are not listed there: every `skills/*/SKILL.md` + Claude targets. The Skills are not listed there: every `src/skills/*/SKILL.md` directory is discovered automatically by convention, the model described in [`docs/framework-mode.md`](../../docs/framework-mode.md). -- `skills/incident-triage/SKILL.md` guides a production incident from first +- `src/skills/incident-triage/SKILL.md` guides a production incident from first signal through containment, evidence collection, and a handoff-ready update. -- `skills/dependency-upgrade/SKILL.md` plans dependency upgrades with API, +- `src/skills/dependency-upgrade/SKILL.md` plans dependency upgrades with API, runtime, rollout, and rollback checks. -- `skills/release-review/SKILL.md` defines the evidence, severity, workflow, +- `src/skills/release-review/SKILL.md` defines the evidence, severity, workflow, and final-report requirements for an explicit release review. - Each Skill links its own `references/` checklist or runbook and reusable `assets/` handoff or planning template. diff --git a/examples/skills-starter/skills/dependency-upgrade/SKILL.md b/examples/skills-starter/src/skills/dependency-upgrade/SKILL.md similarity index 100% rename from examples/skills-starter/skills/dependency-upgrade/SKILL.md rename to examples/skills-starter/src/skills/dependency-upgrade/SKILL.md diff --git a/examples/skills-starter/skills/dependency-upgrade/assets/upgrade-plan.md b/examples/skills-starter/src/skills/dependency-upgrade/assets/upgrade-plan.md similarity index 100% rename from examples/skills-starter/skills/dependency-upgrade/assets/upgrade-plan.md rename to examples/skills-starter/src/skills/dependency-upgrade/assets/upgrade-plan.md diff --git a/examples/skills-starter/skills/dependency-upgrade/references/compatibility-checklist.md b/examples/skills-starter/src/skills/dependency-upgrade/references/compatibility-checklist.md similarity index 100% rename from examples/skills-starter/skills/dependency-upgrade/references/compatibility-checklist.md rename to examples/skills-starter/src/skills/dependency-upgrade/references/compatibility-checklist.md diff --git a/examples/skills-starter/skills/incident-triage/SKILL.md b/examples/skills-starter/src/skills/incident-triage/SKILL.md similarity index 100% rename from examples/skills-starter/skills/incident-triage/SKILL.md rename to examples/skills-starter/src/skills/incident-triage/SKILL.md diff --git a/examples/skills-starter/skills/incident-triage/assets/incident-update.md b/examples/skills-starter/src/skills/incident-triage/assets/incident-update.md similarity index 100% rename from examples/skills-starter/skills/incident-triage/assets/incident-update.md rename to examples/skills-starter/src/skills/incident-triage/assets/incident-update.md diff --git a/examples/skills-starter/skills/incident-triage/references/triage-runbook.md b/examples/skills-starter/src/skills/incident-triage/references/triage-runbook.md similarity index 100% rename from examples/skills-starter/skills/incident-triage/references/triage-runbook.md rename to examples/skills-starter/src/skills/incident-triage/references/triage-runbook.md diff --git a/examples/skills-starter/skills/release-review/SKILL.md b/examples/skills-starter/src/skills/release-review/SKILL.md similarity index 100% rename from examples/skills-starter/skills/release-review/SKILL.md rename to examples/skills-starter/src/skills/release-review/SKILL.md diff --git a/examples/skills-starter/skills/release-review/assets/report-template.md b/examples/skills-starter/src/skills/release-review/assets/report-template.md similarity index 100% rename from examples/skills-starter/skills/release-review/assets/report-template.md rename to examples/skills-starter/src/skills/release-review/assets/report-template.md diff --git a/examples/skills-starter/skills/release-review/references/checklist.md b/examples/skills-starter/src/skills/release-review/references/checklist.md similarity index 100% rename from examples/skills-starter/skills/release-review/references/checklist.md rename to examples/skills-starter/src/skills/release-review/references/checklist.md diff --git a/examples/skills-starter/skills/release-review/references/release-policy.md b/examples/skills-starter/src/skills/release-review/references/release-policy.md similarity index 100% rename from examples/skills-starter/skills/release-review/references/release-policy.md rename to examples/skills-starter/src/skills/release-review/references/release-policy.md diff --git a/fixtures/integration/comprehensive/skills/review/SKILL.md b/fixtures/integration/comprehensive/src/skills/review/SKILL.md similarity index 100% rename from fixtures/integration/comprehensive/skills/review/SKILL.md rename to fixtures/integration/comprehensive/src/skills/review/SKILL.md diff --git a/fixtures/integration/comprehensive/skills/review/assets/binary.bin b/fixtures/integration/comprehensive/src/skills/review/assets/binary.bin similarity index 100% rename from fixtures/integration/comprehensive/skills/review/assets/binary.bin rename to fixtures/integration/comprehensive/src/skills/review/assets/binary.bin diff --git a/fixtures/integration/comprehensive/skills/review/references/guide.txt b/fixtures/integration/comprehensive/src/skills/review/references/guide.txt similarity index 100% rename from fixtures/integration/comprehensive/skills/review/references/guide.txt rename to fixtures/integration/comprehensive/src/skills/review/references/guide.txt diff --git a/fixtures/integration/micro-eval/skills/triage/SKILL.md b/fixtures/integration/micro-eval/src/skills/triage/SKILL.md similarity index 100% rename from fixtures/integration/micro-eval/skills/triage/SKILL.md rename to fixtures/integration/micro-eval/src/skills/triage/SKILL.md diff --git a/fixtures/integration/packed-release/agent-bundle.config.ts b/fixtures/integration/packed-release/agent-bundle.config.ts index 1676b9a19..9e586ab78 100644 --- a/fixtures/integration/packed-release/agent-bundle.config.ts +++ b/fixtures/integration/packed-release/agent-bundle.config.ts @@ -25,6 +25,6 @@ export default { scripts: { review: './src/review.ts', }, - skills: ['skills/review'], + skills: ['src/skills/review'], targets: ['portable', 'claude'], }; diff --git a/fixtures/integration/packed-release/skills/review/SKILL.md b/fixtures/integration/packed-release/src/skills/review/SKILL.md similarity index 100% rename from fixtures/integration/packed-release/skills/review/SKILL.md rename to fixtures/integration/packed-release/src/skills/review/SKILL.md diff --git a/fixtures/integration/skills-only/skills/portable-skill/SKILL.md b/fixtures/integration/skills-only/src/skills/portable-skill/SKILL.md similarity index 100% rename from fixtures/integration/skills-only/skills/portable-skill/SKILL.md rename to fixtures/integration/skills-only/src/skills/portable-skill/SKILL.md diff --git a/fixtures/integration/skills-only/skills/portable-skill/assets/binary.bin b/fixtures/integration/skills-only/src/skills/portable-skill/assets/binary.bin similarity index 100% rename from fixtures/integration/skills-only/skills/portable-skill/assets/binary.bin rename to fixtures/integration/skills-only/src/skills/portable-skill/assets/binary.bin diff --git a/fixtures/integration/skills-only/skills/portable-skill/references/guide.txt b/fixtures/integration/skills-only/src/skills/portable-skill/references/guide.txt similarity index 100% rename from fixtures/integration/skills-only/skills/portable-skill/references/guide.txt rename to fixtures/integration/skills-only/src/skills/portable-skill/references/guide.txt diff --git a/packages/agent-bundle/src/config/discover.ts b/packages/agent-bundle/src/config/discover.ts index 112ac8874..36ad03cd2 100644 --- a/packages/agent-bundle/src/config/discover.ts +++ b/packages/agent-bundle/src/config/discover.ts @@ -44,10 +44,18 @@ export interface DiscoveredPayload { export interface DiscoveredProject { assets?: DiscoveredAsset[]; - /** Conventional flat `commands/*.md` documents; absent when none are discovered. */ + /** Conventional flat `src/commands/*.md` documents; absent when none are discovered. */ commands?: readonly CommandDocument[]; + /** + * Documents using removed top-level conventions. Validation reports AB4736 + * for every unclaimed document; absent when none are discovered. + */ + legacyConventionalDocuments?: readonly { + readonly kind: 'skill' | 'command' | 'rule'; + readonly source: string; + }[]; payloads?: DiscoveredPayload[]; - /** Conventional flat `rules/*.mdc` documents; absent when none are discovered. */ + /** Conventional flat `src/rules/*.mdc` documents; absent when none are discovered. */ rules?: readonly RuleDocument[]; /** * The compiled conventional route graph (#93). Present only when route @@ -56,7 +64,7 @@ export interface DiscoveredProject { */ routeGraph?: CompiledRouteGraph; /** - * Conventional `skills//SKILL.md` documents that explicit `skills` + * Conventional `src/skills//SKILL.md` documents that explicit `skills` * configuration leaves uncovered — the confusable shadowed state surfaced * by the AB4734 migration nudge. Absent when config is silent (the * convention itself applies) or when every conventional skill is covered. @@ -245,7 +253,7 @@ export const discoverProject = async ( const projectRoot = resolve(root); const rules = await readProjectIgnoreRules(projectRoot); const configuredSkills = config.skills; - const conventionalSources = (await fastGlob('skills/*/SKILL.{md,ts,tsx}', { + const conventionalSources = (await fastGlob('src/skills/*/SKILL.{md,ts,tsx}', { absolute: true, cwd: projectRoot, dot: true, @@ -272,9 +280,42 @@ export const discoverProject = async ( } const shadowedConventionalSkills = [...shadowedByDir.values()]; + const legacySkillSources = (await fastGlob('skills/*/SKILL.{md,ts,tsx}', { + absolute: true, + cwd: projectRoot, + dot: true, + followSymbolicLinks: false, + onlyFiles: true, + })) + .filter((source) => + !isProjectPathIgnored(rules, projectRoot, source) && + !coveredDirs.has(dirname(source)) + ); + const legacyCommandSources = (await fastGlob('commands/*.md', { + absolute: true, + cwd: projectRoot, + dot: true, + followSymbolicLinks: false, + onlyFiles: true, + })).filter((source) => !isProjectPathIgnored(rules, projectRoot, source)); + const legacyRuleSources = (await fastGlob('rules/*.mdc', { + absolute: true, + cwd: projectRoot, + dot: true, + followSymbolicLinks: false, + onlyFiles: true, + })).filter((source) => !isProjectPathIgnored(rules, projectRoot, source)); + const legacyConventionalDocuments = [ + ...legacySkillSources.map((source) => ({ kind: 'skill' as const, source })), + ...legacyCommandSources.map((source) => ({ kind: 'command' as const, source })), + ...legacyRuleSources.map((source) => ({ kind: 'rule' as const, source })), + ].sort((left, right) => + left.source.localeCompare(right.source) || left.kind.localeCompare(right.kind) + ); + const payloads = await discoverPayloads(projectRoot, config.payload); const routeGraph = await compileRouteGraph(projectRoot, config, rules); - const commandSources = (await fastGlob('commands/*.md', { + const commandSources = (await fastGlob('src/commands/*.md', { absolute: true, cwd: projectRoot, dot: true, @@ -284,7 +325,7 @@ export const discoverProject = async ( .filter((source) => !isProjectPathIgnored(rules, projectRoot, source)) .sort((left, right) => left.localeCompare(right)); const discoveredCommands = await Promise.all(commandSources.map((source) => parseCommand(source))); - const ruleSources = (await fastGlob('rules/*.mdc', { + const ruleSources = (await fastGlob('src/rules/*.mdc', { absolute: true, cwd: projectRoot, dot: true, @@ -298,6 +339,7 @@ export const discoverProject = async ( return { assets: await discoverAssets(projectRoot, config.assets, rules), ...(discoveredCommands.length === 0 ? {} : { commands: discoveredCommands }), + ...(legacyConventionalDocuments.length === 0 ? {} : { legacyConventionalDocuments }), ...(payloads.length === 0 ? {} : { payloads }), ...(routeGraph === undefined || isEmptyRouteGraph(routeGraph) ? {} : { routeGraph }), ...(discoveredRules.length === 0 ? {} : { rules: discoveredRules }), diff --git a/packages/agent-bundle/src/config/render-markdown.ts b/packages/agent-bundle/src/config/render-markdown.ts index 988c2edcf..2fdb6dbae 100644 --- a/packages/agent-bundle/src/config/render-markdown.ts +++ b/packages/agent-bundle/src/config/render-markdown.ts @@ -1,6 +1,6 @@ /** * The minimal honest React-element-tree → Markdown renderer behind rendered - * skills (`skills//SKILL.tsx`). It walks plain element objects (the + * skills (`src/skills//SKILL.tsx`). It walks plain element objects (the * shape React's automatic JSX runtime produces) without depending on React * itself, resolves function components (sync or async), and hand-emits * Markdown for a documented element subset. Anything outside the subset is a diff --git a/packages/agent-bundle/src/config/rendered-skill.ts b/packages/agent-bundle/src/config/rendered-skill.ts index 90727dabc..1670e4c4f 100644 --- a/packages/agent-bundle/src/config/rendered-skill.ts +++ b/packages/agent-bundle/src/config/rendered-skill.ts @@ -8,7 +8,7 @@ import type { Diagnostic } from '../core/diagnostics.ts'; import { MarkdownRenderError, renderElementToMarkdown } from './render-markdown.ts'; /** - * The rendered-skill convention: `skills//SKILL.tsx` (or `.ts`) + * The rendered-skill convention: `src/skills//SKILL.tsx` (or `.ts`) * default-exports a component and exports a `frontmatter` record; the build * compiles the rendered tree to the `SKILL.md` document every host consumes. * A hand-authored `SKILL.md` in the same directory always wins (config beats diff --git a/packages/agent-bundle/src/config/validate.ts b/packages/agent-bundle/src/config/validate.ts index 19fca5735..5b2eeeee5 100644 --- a/packages/agent-bundle/src/config/validate.ts +++ b/packages/agent-bundle/src/config/validate.ts @@ -60,7 +60,7 @@ const sourceDiagnostic = ( }); /** - * Informational migration nudges (AB473x): they surface pre-convention + * Informational migration nudges (AB4730-AB4735): they surface pre-convention * patterns the entry conventions now replace, and they must never gate a * build — migrations stay optional, so the severity is always `info`. */ @@ -1607,7 +1607,7 @@ const validatePrebuiltReference = ( /** * AB4734: explicit `skills` configuration leaves a conventional - * `skills//SKILL.md` document uncovered, so the convention is silently + * `src/skills//SKILL.md` document uncovered, so the convention is silently * shadowed — the skills-directory analogue of AB4731/AB4732/AB4733. */ const skillConventionShadowNudges = ( @@ -1617,9 +1617,25 @@ const skillConventionShadowNudges = ( 'AB4734', `${relativePosix(loaded.context.projectRoot, source)} is present but explicit skills configuration does not cover it; the conventional skill is shadowed.`, source, - 'Optional: remove the explicit skills configuration to adopt the skills//SKILL.md convention, add the directory to skills, or remove it to silence this nudge.', + 'Optional: remove the explicit skills configuration to adopt the src/skills//SKILL.md convention, add the directory to skills, or remove it to silence this nudge.', )); +const legacyConventionalDocumentErrors = ( + loaded: LoadedConfig, + discovered: DiscoveredProject, +): Diagnostic[] => (discovered.legacyConventionalDocuments ?? []).map(({ kind, source }) => { + const relativePath = relativePosix(loaded.context.projectRoot, source); + const destination = `src/${relativePath}`; + return sourceDiagnostic( + 'AB4736', + `${relativePath} uses the removed top-level ${kind} convention and is no longer discovered.`, + source, + kind === 'skill' + ? `Move the document to ${destination}, or cover its directory with explicit skills configuration.` + : `Move the document to ${destination}.`, + ); +}); + const isRspackHatchValue = (value: unknown): boolean => typeof value === 'function' || isRecord(value); @@ -1905,6 +1921,7 @@ export const validateSource = ( } diagnostics.push(...packageConventionShadowNudges(loaded)); diagnostics.push(...skillConventionShadowNudges(loaded, discovered)); + diagnostics.push(...legacyConventionalDocumentErrors(loaded, discovered)); // Route overrides are validated during discovery; source validation must // still observe the config getter so hostile accessors fail closed as AB7001. void loaded.config['routes']; diff --git a/packages/agent-bundle/src/core/types.ts b/packages/agent-bundle/src/core/types.ts index 830a2ee2b..21c95f2bb 100644 --- a/packages/agent-bundle/src/core/types.ts +++ b/packages/agent-bundle/src/core/types.ts @@ -587,7 +587,7 @@ export interface NormalizedPlugin { */ readonly assets?: readonly NormalizedAsset[]; /** - * Conventional `commands/*.md` documents. Present only when commands are + * Conventional `src/commands/*.md` documents. Present only when commands are * discovered; optional so hand-constructed models predating commands remain valid. */ readonly commands?: readonly NormalizedCommand[]; @@ -624,7 +624,7 @@ export interface NormalizedPlugin { /** Conventional context providers executed for every generated render request. */ readonly providers?: readonly CompiledProvider[]; /** - * Conventional `rules/*.mdc` documents. Present only when rules are + * Conventional `src/rules/*.mdc` documents. Present only when rules are * discovered; optional so hand-constructed models predating rules remain valid. */ readonly rules?: readonly NormalizedRule[]; diff --git a/packages/agent-bundle/tests/api.test.ts b/packages/agent-bundle/tests/api.test.ts index 5c4b5f3f9..a68a51832 100644 --- a/packages/agent-bundle/tests/api.test.ts +++ b/packages/agent-bundle/tests/api.test.ts @@ -26,7 +26,7 @@ import { createMcpPathTokenResolver, standardMcpPathTokens } from '../src/servic const createProject = async (): Promise => { const parent = await mkdtemp(join(tmpdir(), 'agent-bundle-api-parent-')); const root = join(parent, 'project with spaces'); - await mkdir(join(root, 'skills', 'review'), { recursive: true }); + await mkdir(join(root, 'src', 'skills', 'review'), { recursive: true }); await mkdir(join(root, 'src'), { recursive: true }); await Promise.all([ writeFile( @@ -42,7 +42,7 @@ const createProject = async (): Promise => { ].join('\n'), ), writeFile( - join(root, 'skills', 'review', 'SKILL.md'), + join(root, 'src', 'skills', 'review', 'SKILL.md'), '---\nname: review\ndescription: Reviews changes\n---\n# Review\n', ), writeFile(join(root, 'src', 'hook.ts'), 'export default () => undefined;\n'), @@ -652,19 +652,19 @@ it('reports skipped target/component pairs against each target emission surface' const root = await createProject(); try { await Promise.all([ - mkdir(join(root, 'commands')), - mkdir(join(root, 'rules')), + mkdir(join(root, 'src', 'commands'), { recursive: true }), + mkdir(join(root, 'src', 'rules'), { recursive: true }), ]); await Promise.all([ - writeFile(join(root, 'commands', 'shared.md'), '---\ndescription: Shared command\n---\nShared command prompt.\n'), + writeFile(join(root, 'src', 'commands', 'shared.md'), '---\ndescription: Shared command\n---\nShared command prompt.\n'), writeFile( - join(root, 'commands', 'cursor-only.md'), + join(root, 'src', 'commands', 'cursor-only.md'), '---\ndescription: Cursor-only command\ntargets:\n - cursor\n---\nCursor command prompt.\n', ), writeFile(join(root, 'src', 'report.ts'), 'export const report = true;\n'), - writeFile(join(root, 'rules', 'shared.mdc'), '---\ndescription: Shared rule\n---\nShared guidance.\n'), + writeFile(join(root, 'src', 'rules', 'shared.mdc'), '---\ndescription: Shared rule\n---\nShared guidance.\n'), writeFile( - join(root, 'rules', 'cursor-only.mdc'), + join(root, 'src', 'rules', 'cursor-only.mdc'), '---\ndescription: Cursor-only rule\ntargets:\n - cursor\n---\nCursor guidance.\n', ), writeFile(join(root, 'agent-bundle.config.ts'), [ @@ -961,22 +961,22 @@ it('keeps rule and command model digests root-independent and sensitive to conte const sharedCommand = '---\ndescription: Shared command\n---\nShared command body.\n'; try { await Promise.all([ - mkdir(join(leftRoot, 'commands')), - mkdir(join(rightRoot, 'commands')), - mkdir(join(leftRoot, 'rules')), - mkdir(join(rightRoot, 'rules')), + mkdir(join(leftRoot, 'src', 'commands'), { recursive: true }), + mkdir(join(rightRoot, 'src', 'commands'), { recursive: true }), + mkdir(join(leftRoot, 'src', 'rules'), { recursive: true }), + mkdir(join(rightRoot, 'src', 'rules'), { recursive: true }), ]); await Promise.all([ writeFile(join(leftRoot, 'agent-bundle.config.ts'), config), writeFile(join(rightRoot, 'agent-bundle.config.ts'), config), - writeFile(join(leftRoot, 'commands', 'cursor-only.md'), targetedCommand), - writeFile(join(rightRoot, 'commands', 'cursor-only.md'), targetedCommand), - writeFile(join(leftRoot, 'commands', 'shared.md'), sharedCommand), - writeFile(join(rightRoot, 'commands', 'shared.md'), sharedCommand), - writeFile(join(leftRoot, 'rules', 'cursor-only.mdc'), targetedRule), - writeFile(join(rightRoot, 'rules', 'cursor-only.mdc'), targetedRule), - writeFile(join(leftRoot, 'rules', 'shared.mdc'), sharedRule), - writeFile(join(rightRoot, 'rules', 'shared.mdc'), sharedRule), + writeFile(join(leftRoot, 'src', 'commands', 'cursor-only.md'), targetedCommand), + writeFile(join(rightRoot, 'src', 'commands', 'cursor-only.md'), targetedCommand), + writeFile(join(leftRoot, 'src', 'commands', 'shared.md'), sharedCommand), + writeFile(join(rightRoot, 'src', 'commands', 'shared.md'), sharedCommand), + writeFile(join(leftRoot, 'src', 'rules', 'cursor-only.mdc'), targetedRule), + writeFile(join(rightRoot, 'src', 'rules', 'cursor-only.mdc'), targetedRule), + writeFile(join(leftRoot, 'src', 'rules', 'shared.mdc'), sharedRule), + writeFile(join(rightRoot, 'src', 'rules', 'shared.mdc'), sharedRule), ]); const [left, right] = await Promise.all([ @@ -985,20 +985,20 @@ it('keeps rule and command model digests root-independent and sensitive to conte ]); expect(left.projectContext.modelDigest).toBe(right.projectContext.modelDigest); expect(left.projectContext.sourceInputs.map((input) => input.path)).toEqual(expect.arrayContaining([ - 'commands/cursor-only.md', - 'commands/shared.md', - 'rules/cursor-only.mdc', - 'rules/shared.mdc', + 'src/commands/cursor-only.md', + 'src/commands/shared.md', + 'src/rules/cursor-only.mdc', + 'src/rules/shared.mdc', ])); - await writeFile(join(rightRoot, 'rules', 'shared.mdc'), sharedRule.replace('Shared body.', 'Changed body.')); + await writeFile(join(rightRoot, 'src', 'rules', 'shared.mdc'), sharedRule.replace('Shared body.', 'Changed body.')); const changedRule = await readyInspection({ root: rightRoot }); expect(changedRule.projectContext.modelDigest).not.toBe(left.projectContext.modelDigest); await Promise.all([ - writeFile(join(rightRoot, 'rules', 'shared.mdc'), sharedRule), + writeFile(join(rightRoot, 'src', 'rules', 'shared.mdc'), sharedRule), writeFile( - join(rightRoot, 'commands', 'shared.md'), + join(rightRoot, 'src', 'commands', 'shared.md'), sharedCommand.replace('Shared command body.', 'Changed command body.'), ), ]); diff --git a/packages/agent-bundle/tests/build.test.ts b/packages/agent-bundle/tests/build.test.ts index e4646b2de..121d552f9 100644 --- a/packages/agent-bundle/tests/build.test.ts +++ b/packages/agent-bundle/tests/build.test.ts @@ -113,7 +113,7 @@ const treeDigest = async ( const createProject = async (): Promise => { const root = await mkdtemp(join(tmpdir(), 'agent bundle build with spaces ')); - const skillRoot = join(root, 'skills', 'review'); + const skillRoot = join(root, 'src', 'skills', 'review'); const skillScriptsRoot = join(skillRoot, 'scripts'); const scriptPath = join(skillScriptsRoot, 'greeting script.ts'); const localModulePath = join(skillScriptsRoot, 'local greeting module.ts'); @@ -234,26 +234,26 @@ const modelFor = (project: TestProject): NormalizedPlugin => ({ { body: '# Review\n\nSee [guide](references/guide.md).\n', description: 'Review changes', - dir: join(project.root, 'skills', 'review'), + dir: join(project.root, 'src', 'skills', 'review'), frontmatter: { description: 'Review changes', name: 'review' }, id: 'skill:review', name: 'review', - provenance: { kind: 'conventional', sourcePath: join(project.root, 'skills', 'review', 'SKILL.md') }, + provenance: { kind: 'conventional', sourcePath: join(project.root, 'src', 'skills', 'review', 'SKILL.md') }, resources: [ { bytes: Buffer.byteLength(skillFixture.markdown), relativePath: 'SKILL.md', - source: join(project.root, 'skills', 'review', 'SKILL.md'), + source: join(project.root, 'src', 'skills', 'review', 'SKILL.md'), }, { bytes: 4, relativePath: 'assets/icon.bin', - source: join(project.root, 'skills', 'review', 'assets', 'icon.bin'), + source: join(project.root, 'src', 'skills', 'review', 'assets', 'icon.bin'), }, { bytes: 8, relativePath: 'references/guide.md', - source: join(project.root, 'skills', 'review', 'references', 'guide.md'), + source: join(project.root, 'src', 'skills', 'review', 'references', 'guide.md'), }, { bytes: Buffer.byteLength(skillFixture.source), @@ -276,7 +276,7 @@ const modelFor = (project: TestProject): NormalizedPlugin => ({ source: project.pythonScriptPath, }, ], - source: join(project.root, 'skills', 'review', 'SKILL.md'), + source: join(project.root, 'src', 'skills', 'review', 'SKILL.md'), targets: ['portable'], }, ], @@ -354,7 +354,7 @@ it('low-level build writes and returns the exact canonical manifest for a config configPath: 'agent-bundle.config.ts', sourceInputs: expect.arrayContaining([ expect.objectContaining({ path: 'agent-bundle.config.ts' }), - expect.objectContaining({ path: 'skills/review/SKILL.md' }), + expect.objectContaining({ path: 'src/skills/review/SKILL.md' }), ]), }, runtime: { node: '22.12.0' }, @@ -412,8 +412,8 @@ it('low-level build writes and returns the exact canonical manifest for a config path: resource.path, sha256: sha256Hex(contents), sourceInputs: expect.arrayContaining([ - resource.path.replace('portable/', ''), - 'skills/review/SKILL.md', + resource.path.replace('portable/', 'src/'), + 'src/skills/review/SKILL.md', ]), })); } @@ -512,16 +512,16 @@ it('reports complete immutable output provenance for a Skill copy and bundled sc kind: 'bundle', path: 'portable/scripts/greeting.mjs', sourceInputs: [ - 'skills/review/scripts/greeting script.ts', - 'skills/review/scripts/local greeting module.ts', + 'src/skills/review/scripts/greeting script.ts', + 'src/skills/review/scripts/local greeting module.ts', ], }); expect(provenance).toContainEqual({ kind: 'copy', path: 'portable/skills/review/scripts/review helper.sh', sourceInputs: [ - 'skills/review/scripts/review helper.sh', - 'skills/review/SKILL.md', + 'src/skills/review/scripts/review helper.sh', + 'src/skills/review/SKILL.md', ], }); expect(provenance).toContainEqual({ @@ -974,8 +974,8 @@ it('rejects a script name that exits its target scripts directory', async () => it('rejects canonical aliases and adapter/root-script collisions before emission', async () => { const project = await createProject(); - const uppercaseScript = join(project.root, 'skills', 'review', 'scripts', 'upper.SH'); - const lowercaseScript = join(project.root, 'skills', 'review', 'scripts', 'upper.sh'); + const uppercaseScript = join(project.root, 'src', 'skills', 'review', 'scripts', 'upper.SH'); + const lowercaseScript = join(project.root, 'src', 'skills', 'review', 'scripts', 'upper.sh'); const aliasesAdapter: TargetAdapter = { capabilities: {}, metadata: testAdapterMetadata, diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index 39b109dcf..822dceaa6 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -56,7 +56,7 @@ const createCliProject = async (): Promise<{ readonly output: string; readonly r const parent = await mkdtemp(join(tmpdir(), 'agent bundle cli parent-')); const root = join(parent, 'project with spaces'); const output = join(root, 'artifact with spaces'); - await mkdir(join(root, 'skills', 'review'), { recursive: true }); + await mkdir(join(root, 'src', 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile(join(root, 'package.json'), '{"type":"module"}\n'), writeFile( @@ -71,7 +71,7 @@ const createCliProject = async (): Promise<{ readonly output: string; readonly r ].join('\n'), ), writeFile( - join(root, 'skills', 'review', 'SKILL.md'), + join(root, 'src', 'skills', 'review', 'SKILL.md'), '---\nname: review\ndescription: Reviews changes\n---\n# Review\n', ), ]); diff --git a/packages/agent-bundle/tests/command-config.test.ts b/packages/agent-bundle/tests/command-config.test.ts index efc15977c..a149954c2 100644 --- a/packages/agent-bundle/tests/command-config.test.ts +++ b/packages/agent-bundle/tests/command-config.test.ts @@ -50,9 +50,9 @@ const withProject = async ( it('accepts body-only commands and retains exact authored bytes', async () => { await withProject(async (root) => { - const source = join(root, 'commands', 'review.md'); + const source = join(root, 'src', 'commands', 'review.md'); const markdown = '# Review\r\n\r\nCheck the staged diff.'; - await mkdir(join(root, 'commands')); + await mkdir(join(root, 'src', 'commands'), { recursive: true }); await writeFile(source, markdown); const command = await parseCommand(source); @@ -70,8 +70,8 @@ it('accepts body-only commands and retains exact authored bytes', async () => { it('peels targets and accepts only the closed canonical frontmatter schema', async () => { await withProject(async (root) => { - await mkdir(join(root, 'commands')); - const validSource = join(root, 'commands', 'review.md'); + await mkdir(join(root, 'src', 'commands'), { recursive: true }); + const validSource = join(root, 'src', 'commands', 'review.md'); await writeFile( validSource, [ @@ -101,11 +101,11 @@ it('peels targets and accepts only the closed canonical frontmatter schema', asy }); expect(valid.authoredTargets).toEqual(['claude']); - const stringTools = join(root, 'commands', 'string-tools.md'); + const stringTools = join(root, 'src', 'commands', 'string-tools.md'); await writeFile(stringTools, '---\nallowedTools: Read, Grep\n---\nReview.\n'); expect((await parseCommand(stringTools)).frontmatter).toEqual({ allowedTools: 'Read, Grep' }); - const invalidSource = join(root, 'commands', 'invalid.md'); + const invalidSource = join(root, 'src', 'commands', 'invalid.md'); await writeFile( invalidSource, [ @@ -139,13 +139,13 @@ it('peels targets and accepts only the closed canonical frontmatter schema', asy it('reports unreadable files and malformed YAML with fresh command diagnostics', async () => { await withProject(async (root) => { - const missing = join(root, 'commands', 'missing.md'); + const missing = join(root, 'src', 'commands', 'missing.md'); expect((await parseCommand(missing)).diagnostics).toEqual([ expect.objectContaining({ code: 'AB4920', severity: 'error', sourcePath: missing }), ]); - const malformed = join(root, 'commands', 'malformed.md'); - await mkdir(join(root, 'commands')); + const malformed = join(root, 'src', 'commands', 'malformed.md'); + await mkdir(join(root, 'src', 'commands'), { recursive: true }); await writeFile(malformed, '---\nallowedTools: [unterminated\n---\n# Broken\n'); expect((await parseCommand(malformed)).diagnostics).toEqual([ expect.objectContaining({ code: 'AB4921', severity: 'error', sourcePath: malformed }), @@ -155,12 +155,12 @@ it('reports unreadable files and malformed YAML with fresh command diagnostics', it('discovers flat non-ignored commands deterministically and omits the collection when empty', async () => { await withProject(async (root) => { - await mkdir(join(root, 'commands')); + await mkdir(join(root, 'src', 'commands'), { recursive: true }); await Promise.all([ - writeFile(join(root, '.gitignore'), 'commands/ignored.md\n'), - writeFile(join(root, 'commands', 'zeta.md'), '# Zeta\n'), - writeFile(join(root, 'commands', 'alpha.md'), '# Alpha\n'), - writeFile(join(root, 'commands', 'ignored.md'), '# Ignored\n'), + writeFile(join(root, '.gitignore'), 'src/commands/ignored.md\n'), + writeFile(join(root, 'src', 'commands', 'zeta.md'), '# Zeta\n'), + writeFile(join(root, 'src', 'commands', 'alpha.md'), '# Alpha\n'), + writeFile(join(root, 'src', 'commands', 'ignored.md'), '# Ignored\n'), ]); const config: AgentBundleConfig = { plugin: { name: 'command-fixture', version: '1.0.0' }, @@ -168,11 +168,11 @@ it('discovers flat non-ignored commands deterministically and omits the collecti const discovered = await discoverProject(root, config); expect(discovered.commands?.map((command) => command.source)).toEqual([ - join(root, 'commands', 'alpha.md'), - join(root, 'commands', 'zeta.md'), + join(root, 'src', 'commands', 'alpha.md'), + join(root, 'src', 'commands', 'zeta.md'), ]); - await rm(join(root, 'commands'), { recursive: true }); + await rm(join(root, 'src', 'commands'), { recursive: true }); expect(await discoverProject(root, config)).not.toHaveProperty('commands'); }); }); @@ -192,7 +192,7 @@ it('normalizes peeled targets and reports unknown, unavailable, and duplicate co }); const registry = createDefaultRegistry(); const claudeLoaded = loadedProject(root, ['claude']); - const claudeCommand = command(join(root, 'commands', 'review.md'), ['claude']); + const claudeCommand = command(join(root, 'src', 'commands', 'review.md'), ['claude']); const discovered: DiscoveredProject = { commands: [claudeCommand], skills: [] }; const model = await normalizeProject(claudeLoaded, discovered, registry); @@ -207,13 +207,13 @@ it('normalizes peeled targets and reports unknown, unavailable, and duplicate co targets: ['claude'], }]); - const unknown = command(join(root, 'commands', 'unknown.md'), ['cursor']); + const unknown = command(join(root, 'src', 'commands', 'unknown.md'), ['cursor']); expect(validateSource(claudeLoaded, { commands: [unknown], skills: [] }, registry)).toEqual([ expect.objectContaining({ code: 'AB4924', sourcePath: unknown.source }), ]); const codexLoaded = loadedProject(root, ['codex']); - const unavailable = command(join(root, 'commands', 'unavailable.md'), ['codex']); + const unavailable = command(join(root, 'src', 'commands', 'unavailable.md'), ['codex']); expect(validateSource(codexLoaded, { commands: [unavailable], skills: [] }, registry)).toEqual([ expect.objectContaining({ code: 'AB4925', diff --git a/packages/agent-bundle/tests/config.test.ts b/packages/agent-bundle/tests/config.test.ts index 5d5e5ced6..747f2871e 100644 --- a/packages/agent-bundle/tests/config.test.ts +++ b/packages/agent-bundle/tests/config.test.ts @@ -360,7 +360,7 @@ it('expands glob patterns in explicit skills entries and deduplicates overlappin const discovered = await discoverProject(fixture.root, { plugin: { name: 'review', version: '1.0.0' }, - skills: ['skills/*', 'custom/*/SKILL.md', 'custom/selected'], + skills: ['src/skills/*', 'custom/*/SKILL.md', 'custom/selected'], }); expect(discovered.skills.map((skill) => skill.dir).sort()).toEqual( diff --git a/packages/agent-bundle/tests/cursor-adapter.test.ts b/packages/agent-bundle/tests/cursor-adapter.test.ts index e95ecd229..2d8685df5 100644 --- a/packages/agent-bundle/tests/cursor-adapter.test.ts +++ b/packages/agent-bundle/tests/cursor-adapter.test.ts @@ -52,16 +52,16 @@ const plugin = (): NormalizedPlugin => ({ { body: '# Review\n', description: 'Review code and explain findings.', - dir: '/workspace/skills/review', + dir: '/workspace/src/skills/review', frontmatter: { description: 'Review code and explain findings.', name: 'review' }, id: 'skill:review', name: 'review', - provenance: { kind: 'conventional', sourcePath: '/workspace/skills/review/SKILL.md' }, + provenance: { kind: 'conventional', sourcePath: '/workspace/src/skills/review/SKILL.md' }, resources: [ - { bytes: 9, relativePath: 'SKILL.md', source: '/workspace/skills/review/SKILL.md' }, - { bytes: 8, relativePath: 'references/guide.md', source: '/workspace/skills/review/references/guide.md' }, + { bytes: 9, relativePath: 'SKILL.md', source: '/workspace/src/skills/review/SKILL.md' }, + { bytes: 8, relativePath: 'references/guide.md', source: '/workspace/src/skills/review/references/guide.md' }, ], - source: '/workspace/skills/review/SKILL.md', + source: '/workspace/src/skills/review/SKILL.md', targets: ['cursor'], }, ], @@ -255,8 +255,8 @@ it('emits selected rules byte-faithfully and omits the entire surface when rule- id: 'rule:typescript', markdown, name: 'typescript', - provenance: { kind: 'conventional', sourcePath: '/workspace/rules/typescript.mdc' }, - source: '/workspace/rules/typescript.mdc', + provenance: { kind: 'conventional', sourcePath: '/workspace/src/rules/typescript.mdc' }, + source: '/workspace/src/rules/typescript.mdc', targets: ['cursor'], }, { body: 'Keep changes focused.', @@ -265,8 +265,8 @@ it('emits selected rules byte-faithfully and omits the entire surface when rule- id: 'rule:focused', markdown: '---\ndescription: Focus changes\nalwaysApply: true\ntargets:\n - cursor\n---\nKeep changes focused.', name: 'focused', - provenance: { kind: 'conventional', sourcePath: '/workspace/rules/focused.mdc' }, - source: '/workspace/rules/focused.mdc', + provenance: { kind: 'conventional', sourcePath: '/workspace/src/rules/focused.mdc' }, + source: '/workspace/src/rules/focused.mdc', targets: ['cursor'], }], }; @@ -283,7 +283,7 @@ it('emits selected rules byte-faithfully and omits the entire surface when rule- rules: './rules/', }); expect(plan.entries.find((entry) => entry.relativePath === 'rules/typescript.mdc')?.sourceInputs).toEqual([ - '/workspace/rules/typescript.mdc', + '/workspace/src/rules/typescript.mdc', ]); const ruleFree = cursorAdapter.plan(model); @@ -302,8 +302,8 @@ it('emits Cursor command bodies, strips authored frontmatter, and omits the comm id: 'command:review', markdown: '---\r\ndescription: Review changes\r\nargumentHint: "[path]"\r\ntargets:\r\n - cursor\r\n---\r\nReview the staged diff.\r\n', name: 'review', - provenance: { kind: 'conventional', sourcePath: '/workspace/commands/review.md' }, - source: '/workspace/commands/review.md', + provenance: { kind: 'conventional', sourcePath: '/workspace/src/commands/review.md' }, + source: '/workspace/src/commands/review.md', targets: ['cursor'], }, { @@ -312,8 +312,8 @@ it('emits Cursor command bodies, strips authored frontmatter, and omits the comm id: 'command:explain', markdown: '# Explain\n\nExplain this code.', name: 'explain', - provenance: { kind: 'conventional', sourcePath: '/workspace/commands/explain.md' }, - source: '/workspace/commands/explain.md', + provenance: { kind: 'conventional', sourcePath: '/workspace/src/commands/explain.md' }, + source: '/workspace/src/commands/explain.md', targets: ['cursor'], }, ], diff --git a/packages/agent-bundle/tests/dev-artifact-service.test.ts b/packages/agent-bundle/tests/dev-artifact-service.test.ts index f1d63583a..889429ab2 100644 --- a/packages/agent-bundle/tests/dev-artifact-service.test.ts +++ b/packages/agent-bundle/tests/dev-artifact-service.test.ts @@ -20,7 +20,7 @@ const sha256 = (value: string | Uint8Array): string => const createProject = async (): Promise => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-artifact-service-')); - await mkdir(join(root, 'skills', 'review'), { recursive: true }); + await mkdir(join(root, 'src', 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile( join(root, 'agent-bundle.config.ts'), @@ -33,7 +33,7 @@ const createProject = async (): Promise => { ].join('\n'), ), writeFile( - join(root, 'skills', 'review', 'SKILL.md'), + join(root, 'src', 'skills', 'review', 'SKILL.md'), [ '---', 'name: review', @@ -248,7 +248,7 @@ it.each(['added', 'changed', 'removed'] as const)( async (mutation) => { const root = await createProject(); const store = new EpochStore({ projectRoot: root }); - const deletedSource = join(root, 'skills', 'review', 'guide.txt'); + const deletedSource = join(root, 'src', 'skills', 'review', 'guide.txt'); const attempts: string[] = []; try { if (mutation === 'removed') await writeFile(deletedSource, 'Guide before removal.\n'); @@ -261,7 +261,7 @@ it.each(['added', 'changed', 'removed'] as const)( await mkdir(join(root, 'src'), { recursive: true }); await writeFile(join(root, 'src', 'added.ts'), 'export const added = true;\n'); } else if (mutation === 'changed') { - await writeFile(join(root, 'skills', 'review', 'SKILL.md'), [ + await writeFile(join(root, 'src', 'skills', 'review', 'SKILL.md'), [ '---', 'name: review', 'description: Reviews changed source', diff --git a/packages/agent-bundle/tests/dev-coordinator.test.ts b/packages/agent-bundle/tests/dev-coordinator.test.ts index e8df6156c..43807785e 100644 --- a/packages/agent-bundle/tests/dev-coordinator.test.ts +++ b/packages/agent-bundle/tests/dev-coordinator.test.ts @@ -28,7 +28,7 @@ const createProject = async (): Promise => (await createProjectFixture({ '', ].join('\n'), files: { - 'skills/review/SKILL.md': [ + 'src/skills/review/SKILL.md': [ '---', 'name: review', 'description: Reviews changes', diff --git a/packages/agent-bundle/tests/dev-events.contract.test.ts b/packages/agent-bundle/tests/dev-events.contract.test.ts index 2992763e3..a2dea71e6 100644 --- a/packages/agent-bundle/tests/dev-events.contract.test.ts +++ b/packages/agent-bundle/tests/dev-events.contract.test.ts @@ -32,7 +32,7 @@ const epoch = { const sourceChanged: ProjectEventInput = { payload: { occurredAt: '2026-08-14T12:00:00.000Z', - paths: ['skills/review/SKILL.md'], + paths: ['src/skills/review/SKILL.md'], reason: 'source-change', }, type: 'source.changed', @@ -171,7 +171,7 @@ it('rejects non-JSON event payloads before allocating a sequence ID', () => { it('rejects numeric own array keys outside the array length without consuming a sequence', () => { const hub = new ProjectEventHub(); - const paths = ['skills/review/SKILL.md']; + const paths = ['src/skills/review/SKILL.md']; Object.defineProperty(paths, '4294967295', { enumerable: true, value: 'not-an-array-index', diff --git a/packages/agent-bundle/tests/dev-events.test.ts b/packages/agent-bundle/tests/dev-events.test.ts index e8ec3ebb5..e324436fd 100644 --- a/packages/agent-bundle/tests/dev-events.test.ts +++ b/packages/agent-bundle/tests/dev-events.test.ts @@ -82,7 +82,7 @@ it('assigns monotonic sequence IDs and freezes published event payloads', () => const hub = new ProjectEventHub(); const first = hub.publish({ - payload: invalidation(['skills/review/SKILL.md']), + payload: invalidation(['src/skills/review/SKILL.md']), type: 'source.changed', }); const second = hub.publish({ diff --git a/packages/agent-bundle/tests/dev-package-build-service.test.ts b/packages/agent-bundle/tests/dev-package-build-service.test.ts index c40d313d0..067ec7730 100644 --- a/packages/agent-bundle/tests/dev-package-build-service.test.ts +++ b/packages/agent-bundle/tests/dev-package-build-service.test.ts @@ -99,7 +99,7 @@ it('builds initially, skips untracked changes, and rebuilds tracked inputs', asy }); expect(builds).toBe(1); - await expect(service.build(project, invalidation('source-change', ['skills/review/SKILL.md']))) + await expect(service.build(project, invalidation('source-change', ['src/skills/review/SKILL.md']))) .resolves.toEqual({ diagnostics: [], state: 'skipped' }); expect(builds).toBe(1); @@ -143,7 +143,7 @@ it('rebuilds on manual invalidations and on package build identity changes', asy )).resolves.toMatchObject({ state: 'built' }); await expect(service.build( prepared({ packageBuild: packageBuild('renamed') }), - invalidation('source-change', ['skills/review/SKILL.md']), + invalidation('source-change', ['src/skills/review/SKILL.md']), )).resolves.toMatchObject({ state: 'built' }); expect(builds).toBe(3); }); @@ -170,7 +170,7 @@ it('surfaces failures as one AB7103 warning and retries on the next change', asy }); // A failed build never records inputs, so even an untracked change retries. - await expect(service.build(project, invalidation('source-change', ['skills/review/SKILL.md']))) + await expect(service.build(project, invalidation('source-change', ['src/skills/review/SKILL.md']))) .resolves.toMatchObject({ state: 'built' }); expect(attempts).toBe(2); }); @@ -213,21 +213,21 @@ it('includes the tools hatch in the rebuild identity, comparing functions by sou // An identical hatch source with an untracked change skips. await expect(service.build( prepared({ packageBuild: build, tools: unchangedHatch() }), - invalidation('source-change', ['skills/review/SKILL.md']), + invalidation('source-change', ['src/skills/review/SKILL.md']), )).resolves.toMatchObject({ state: 'skipped' }); expect(builds).toBe(1); // A changed hatch rebuilds even when no tracked source input changed. await expect(service.build( prepared({ packageBuild: build, tools: editedHatch }), - invalidation('source-change', ['skills/review/SKILL.md']), + invalidation('source-change', ['src/skills/review/SKILL.md']), )).resolves.toMatchObject({ state: 'built' }); expect(builds).toBe(2); // An rsbuild-fragment change rebuilds too. await expect(service.build( prepared({ packageBuild: build, tools: { rsbuild: { output: { legalComments: 'linked' } } } }), - invalidation('source-change', ['skills/review/SKILL.md']), + invalidation('source-change', ['src/skills/review/SKILL.md']), )).resolves.toMatchObject({ state: 'built' }); expect(builds).toBe(3); }); diff --git a/packages/agent-bundle/tests/dev-server.test.ts b/packages/agent-bundle/tests/dev-server.test.ts index 6761f76cc..663882738 100644 --- a/packages/agent-bundle/tests/dev-server.test.ts +++ b/packages/agent-bundle/tests/dev-server.test.ts @@ -701,7 +701,7 @@ it('requires the same origin and session token before a browser can request a re }); const accepted = await fetch(`${server.url}/api/project/rebuild`, { - body: '{"paths":["skills/review/SKILL.md"]}', + body: '{"paths":["src/skills/review/SKILL.md"]}', headers: { 'content-type': 'application/json', origin: server.url, @@ -711,7 +711,7 @@ it('requires the same origin and session token before a browser can request a re }); expect(accepted.status).toBe(200); expect(coordinator.invalidations).toEqual([expect.objectContaining({ - paths: ['skills/review/SKILL.md'], + paths: ['src/skills/review/SKILL.md'], reason: 'manual', })]); } finally { diff --git a/packages/agent-bundle/tests/dev-services.test.ts b/packages/agent-bundle/tests/dev-services.test.ts index 37adc8d58..d230ce20b 100644 --- a/packages/agent-bundle/tests/dev-services.test.ts +++ b/packages/agent-bundle/tests/dev-services.test.ts @@ -34,7 +34,7 @@ it('rejects a Windows path on a different drive', () => { const createProject = async (skillMarkdown: string): Promise => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-dev-service-')); - await mkdir(join(root, 'skills', 'review'), { recursive: true }); + await mkdir(join(root, 'src', 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile( join(root, 'agent-bundle.config.ts'), @@ -46,7 +46,7 @@ const createProject = async (skillMarkdown: string): Promise => { '', ].join('\n'), ), - writeFile(join(root, 'skills', 'review', 'SKILL.md'), skillMarkdown), + writeFile(join(root, 'src', 'skills', 'review', 'SKILL.md'), skillMarkdown), ]); return root; }; @@ -69,10 +69,10 @@ const createRuntimeProject = async (options: Readonly<{ '{ _meta: ' + (options.appMeta ?? "{ ui: { preferred: 'compact' }, labels: ['one', 'two'] }") + ", entry: './src/app.ts', resourceUri: 'ui://timeline/v1/dashboard', targets: ['portable'], template: './src/shell.html' }" ); - await mkdir(join(root, 'skills', 'review'), { recursive: true }); + await mkdir(join(root, 'src', 'skills', 'review'), { recursive: true }); await mkdir(join(root, 'src', 'dev'), { recursive: true }); await Promise.all([ - writeFile(join(root, 'skills', 'review', 'SKILL.md'), [ + writeFile(join(root, 'src', 'skills', 'review', 'SKILL.md'), [ '---', 'name: review', 'description: Reviews changes', @@ -351,7 +351,7 @@ it('sanitizes top-level MCP App metadata accessors before source validation with it('does not let supplemental metadata sanitization suppress unrelated source diagnostics', async () => { const project = await createRuntimeProject({ appMeta: '{ count: Number.NaN }' }); try { - await writeFile(join(project.root, 'skills', 'review', 'SKILL.md'), '# Missing frontmatter\n'); + await writeFile(join(project.root, 'src', 'skills', 'review', 'SKILL.md'), '# Missing frontmatter\n'); const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root: project.root }).prepare('build'); expect(prepared.source).toMatchObject({ @@ -373,7 +373,7 @@ it('stops the shared project pipeline on source errors with a frozen structured expect(prepared.source.diagnostics).toEqual(expect.arrayContaining([ expect.objectContaining({ code: 'AB3001', - sourcePath: join(root, 'skills', 'review', 'SKILL.md'), + sourcePath: join(root, 'src', 'skills', 'review', 'SKILL.md'), }), ])); expect(prepared.model).toBeUndefined(); @@ -425,7 +425,7 @@ it('retains the resolved configuration path in the prepared project', async () = ); expect(prepared.projectContext?.sourceInputs).toEqual(expect.arrayContaining([ expect.objectContaining({ path: 'agent-bundle.config.ts' }), - expect.objectContaining({ path: 'skills/review/SKILL.md' }), + expect.objectContaining({ path: 'src/skills/review/SKILL.md' }), ])); expect(Object.isFrozen(prepared.projectContext?.sourceInputs)).toBe(true); } finally { @@ -470,7 +470,7 @@ it('creates an exact deeply frozen root-independent project context', async () = expect(left.projectContext?.sourceInputs.map((input) => input.path)).toEqual([ 'a-first.txt', 'agent-bundle.config.ts', - 'skills/review/SKILL.md', + 'src/skills/review/SKILL.md', 'z-last.txt', ]); expect(Object.isFrozen(left.projectContext)).toBe(true); @@ -847,18 +847,18 @@ it('derives source revisions from authored bytes, including resources and invali const invalidRoot = await createProject('# First invalid skill\n'); try { await Promise.all([ - writeFile(join(root, 'skills', 'review', 'guide.txt'), 'one'), - writeFile(join(equivalentRoot, 'skills', 'review', 'guide.txt'), 'one'), + writeFile(join(root, 'src', 'skills', 'review', 'guide.txt'), 'one'), + writeFile(join(equivalentRoot, 'src', 'skills', 'review', 'guide.txt'), 'one'), ]); const initial = await new ProjectService({ root }).prepare('build'); const equivalentInitial = await new ProjectService({ root: equivalentRoot }).prepare('build'); - await writeFile(join(root, 'skills', 'review', 'guide.txt'), 'two'); + await writeFile(join(root, 'src', 'skills', 'review', 'guide.txt'), 'two'); const resourceChanged = await new ProjectService({ root }).prepare('build'); - await writeFile(join(equivalentRoot, 'skills', 'review', 'guide.txt'), 'two'); + await writeFile(join(equivalentRoot, 'src', 'skills', 'review', 'guide.txt'), 'two'); const equivalentChanged = await new ProjectService({ root: equivalentRoot }).prepare('build'); const invalidInitial = await new ProjectService({ root: invalidRoot }).prepare('build'); - await writeFile(join(invalidRoot, 'skills', 'review', 'SKILL.md'), '# Second invalid skill\n'); + await writeFile(join(invalidRoot, 'src', 'skills', 'review', 'SKILL.md'), '# Second invalid skill\n'); const invalidChanged = await new ProjectService({ root: invalidRoot }).prepare('build'); expect(initial.source.revision).toBe(equivalentInitial.source.revision); @@ -888,7 +888,7 @@ it('invalidates cached source hashes after a same-size rewrite with a restored m '[Guide](guide.txt)', '', ].join('\n')); - const guide = join(root, 'skills', 'review', 'guide.txt'); + const guide = join(root, 'src', 'skills', 'review', 'guide.txt'); try { await writeFile(guide, 'one'); diff --git a/packages/agent-bundle/tests/dev-workbench.test.ts b/packages/agent-bundle/tests/dev-workbench.test.ts index 4b7800753..1fa4d3daa 100644 --- a/packages/agent-bundle/tests/dev-workbench.test.ts +++ b/packages/agent-bundle/tests/dev-workbench.test.ts @@ -131,7 +131,7 @@ const writeMcpProject = async (root: string): Promise => { 'export default defineConfig({', " mcp: { servers: { fixture: { entry: './src/server.ts' } } },", " plugin: { name: 'workbench-mcp-fixture', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['portable'],", '});', '', @@ -156,7 +156,7 @@ const writeHookProject = async (root: string): Promise => { 'export default defineConfig({', " hooks: { sessionStart: './src/hooks/session-start.ts' },", " plugin: { name: 'workbench-hook-fixture', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['claude'],", '});', '', @@ -237,7 +237,7 @@ const writeCompilingRuntimeAppProject = async (root: string, stateKey: string): 'export default defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", " plugin: { name: 'compiling-runtime-apps', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['portable'],", '});', '', @@ -363,7 +363,7 @@ it('enables the optional agent API from dev.agentApi when no CLI override is sup 'export default defineConfig({', ' dev: { agentApi: true },', " plugin: { name: 'agent-api-config-fixture', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['portable'],", '});', '', @@ -443,7 +443,7 @@ it('latches a runtime declaration added to an ordinary Workbench session as rest 'export default defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", " plugin: { name: 'review', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", '});', '', ].join('\n')); @@ -486,7 +486,7 @@ it('keeps the ordinary foreground and artifact lane available when provider star 'export default defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", " plugin: { name: 'review', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", '});', '', ].join('\n')), @@ -536,7 +536,7 @@ it('retains Runtime App routes through invalid config updates and reconciles onl ...(includeRuntime ? [" dev: { runtime: { provider: './src/dev/provider.ts' } },"] : []), ` fixtureMarker: ${JSON.stringify(marker)},`, " plugin: { name: 'late-runtime-apps', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", ` targets: ${JSON.stringify(targets)},`, ...(extension === undefined ? [] : [` portable: ${extension},`]), '});', @@ -733,7 +733,7 @@ it('fences a closing foreground before a held valid runtime reconcile can attach 'export default defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", " plugin: { name: 'late-runtime-close', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", ` targets: ${JSON.stringify(targets)},`, '});', '', @@ -833,7 +833,7 @@ it('does not reconcile a valid preparation released after foreground close begin " dev: { runtime: { provider: './src/dev/provider.ts' } },", ` fixtureMarker: ${JSON.stringify(marker)},`, " plugin: { name: 'prepared-runtime-close', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['portable'],", ]; return [ @@ -986,7 +986,7 @@ it('does not publish a prepared runtime topology after foreground close begins', ' return defineConfig({', " dev: { runtime: { provider: './src/dev/provider.ts' } },", " plugin: { name: 'prepared-topology-close', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['portable'],", ' });', '};', @@ -1150,7 +1150,7 @@ it('prepares the optional runtime once with the development config context befor ' return {', " dev: { runtime: { provider: './src/dev/provider.ts' } },", " plugin: { name: 'runtime-fixture', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", ' };', '});', '', diff --git a/packages/agent-bundle/tests/fixtures/host-install-portable/agent-bundle.config.ts b/packages/agent-bundle/tests/fixtures/host-install-portable/agent-bundle.config.ts index 0337d77c4..534c4a8de 100644 --- a/packages/agent-bundle/tests/fixtures/host-install-portable/agent-bundle.config.ts +++ b/packages/agent-bundle/tests/fixtures/host-install-portable/agent-bundle.config.ts @@ -12,6 +12,6 @@ export default { name: 'host-install-portable-proof', version: '1.0.0', }, - skills: ['skills/probe'], + skills: ['src/skills/probe'], targets: ['portable'], }; diff --git a/packages/agent-bundle/tests/fixtures/host-install-portable/skills/probe/SKILL.md b/packages/agent-bundle/tests/fixtures/host-install-portable/src/skills/probe/SKILL.md similarity index 100% rename from packages/agent-bundle/tests/fixtures/host-install-portable/skills/probe/SKILL.md rename to packages/agent-bundle/tests/fixtures/host-install-portable/src/skills/probe/SKILL.md diff --git a/packages/agent-bundle/tests/fixtures/host-install-tokens/agent-bundle.config.ts b/packages/agent-bundle/tests/fixtures/host-install-tokens/agent-bundle.config.ts index 27987c126..48dae91f3 100644 --- a/packages/agent-bundle/tests/fixtures/host-install-tokens/agent-bundle.config.ts +++ b/packages/agent-bundle/tests/fixtures/host-install-tokens/agent-bundle.config.ts @@ -7,6 +7,6 @@ export default { name: 'host-install-token-proof', version: '1.0.0', }, - skills: ['skills/token-probe'], + skills: ['src/skills/token-probe'], targets: ['claude'], }; diff --git a/packages/agent-bundle/tests/fixtures/host-install-tokens/skills/token-probe/SKILL.md b/packages/agent-bundle/tests/fixtures/host-install-tokens/src/skills/token-probe/SKILL.md similarity index 100% rename from packages/agent-bundle/tests/fixtures/host-install-tokens/skills/token-probe/SKILL.md rename to packages/agent-bundle/tests/fixtures/host-install-tokens/src/skills/token-probe/SKILL.md diff --git a/packages/agent-bundle/tests/fixtures/host-install/agent-bundle.config.ts b/packages/agent-bundle/tests/fixtures/host-install/agent-bundle.config.ts index 9f40daa88..439aa5fdf 100644 --- a/packages/agent-bundle/tests/fixtures/host-install/agent-bundle.config.ts +++ b/packages/agent-bundle/tests/fixtures/host-install/agent-bundle.config.ts @@ -17,6 +17,6 @@ export default { routes: { mcpCommands: true, }, - skills: ['skills/probe'], + skills: ['src/skills/probe'], targets: ['claude', 'codex', 'cursor'], }; diff --git a/packages/agent-bundle/tests/fixtures/host-install/skills/probe/SKILL.md b/packages/agent-bundle/tests/fixtures/host-install/src/skills/probe/SKILL.md similarity index 100% rename from packages/agent-bundle/tests/fixtures/host-install/skills/probe/SKILL.md rename to packages/agent-bundle/tests/fixtures/host-install/src/skills/probe/SKILL.md diff --git a/packages/agent-bundle/tests/fixtures/packed-native-smoke/agent-bundle.config.ts b/packages/agent-bundle/tests/fixtures/packed-native-smoke/agent-bundle.config.ts index 9b6e4447a..b61405b13 100644 --- a/packages/agent-bundle/tests/fixtures/packed-native-smoke/agent-bundle.config.ts +++ b/packages/agent-bundle/tests/fixtures/packed-native-smoke/agent-bundle.config.ts @@ -5,6 +5,6 @@ export default { name: 'packed-native-smoke', version: '1.0.0', }, - skills: ['skills/review'], + skills: ['src/skills/review'], targets: ['claude', 'codex'], }; diff --git a/packages/agent-bundle/tests/fixtures/packed-native-smoke/skills/review/SKILL.md b/packages/agent-bundle/tests/fixtures/packed-native-smoke/src/skills/review/SKILL.md similarity index 100% rename from packages/agent-bundle/tests/fixtures/packed-native-smoke/skills/review/SKILL.md rename to packages/agent-bundle/tests/fixtures/packed-native-smoke/src/skills/review/SKILL.md diff --git a/packages/agent-bundle/tests/fixtures/rendered-skill/skills/deploy-checklist/SKILL.tsx b/packages/agent-bundle/tests/fixtures/rendered-skill/src/skills/deploy-checklist/SKILL.tsx similarity index 100% rename from packages/agent-bundle/tests/fixtures/rendered-skill/skills/deploy-checklist/SKILL.tsx rename to packages/agent-bundle/tests/fixtures/rendered-skill/src/skills/deploy-checklist/SKILL.tsx diff --git a/packages/agent-bundle/tests/fixtures/rendered-skill/skills/deploy-checklist/references/playbook.md b/packages/agent-bundle/tests/fixtures/rendered-skill/src/skills/deploy-checklist/references/playbook.md similarity index 100% rename from packages/agent-bundle/tests/fixtures/rendered-skill/skills/deploy-checklist/references/playbook.md rename to packages/agent-bundle/tests/fixtures/rendered-skill/src/skills/deploy-checklist/references/playbook.md diff --git a/packages/agent-bundle/tests/helpers/project-fixture.ts b/packages/agent-bundle/tests/helpers/project-fixture.ts index 4452fdb09..9ab4330cc 100644 --- a/packages/agent-bundle/tests/helpers/project-fixture.ts +++ b/packages/agent-bundle/tests/helpers/project-fixture.ts @@ -35,7 +35,7 @@ export const createProjectFixture = async ( options: ProjectFixtureOptions = {}, ): Promise => { const root = await mkdtemp(join(rstestWorkerRoot(), options.prefix ?? 'agent-bundle-config-')); - const skillDir = join(root, 'skills/review'); + const skillDir = join(root, 'src/skills/review'); const skillSource = join(skillDir, 'SKILL.md'); const imagePath = join(skillDir, 'assets/diagram.png'); const configPath = join(root, 'agent-bundle.config.ts'); @@ -48,7 +48,7 @@ export const createProjectFixture = async ( ...(options.config === undefined ? [] : [writeFile(configPath, options.config)]), ...Object.entries(files).map(([relativePath, contents]) => writeFile(join(root, relativePath), contents)), ]); - const skillMarkdown = files['skills/review/SKILL.md']; + const skillMarkdown = files['src/skills/review/SKILL.md']; return { configPath, imagePath, diff --git a/packages/agent-bundle/tests/host-adapters.test.ts b/packages/agent-bundle/tests/host-adapters.test.ts index 7053cbbfa..252279e5f 100644 --- a/packages/agent-bundle/tests/host-adapters.test.ts +++ b/packages/agent-bundle/tests/host-adapters.test.ts @@ -54,17 +54,17 @@ const plugin = Object.freeze({ Object.freeze({ body: '# Review\n', description: 'Review code and explain findings.', - dir: '/workspace/skills/review', + dir: '/workspace/src/skills/review', frontmatter: Object.freeze({ description: 'Review code and explain findings.', name: 'review' }), id: 'skill:review', name: 'review', - provenance: Object.freeze({ kind: 'conventional' as const, sourcePath: '/workspace/skills/review/SKILL.md' }), + provenance: Object.freeze({ kind: 'conventional' as const, sourcePath: '/workspace/src/skills/review/SKILL.md' }), resources: Object.freeze([ - Object.freeze({ bytes: 9, relativePath: 'SKILL.md', source: '/workspace/skills/review/SKILL.md' }), - Object.freeze({ bytes: 3, relativePath: 'assets/icon.bin', source: '/workspace/skills/review/assets/icon.bin' }), - Object.freeze({ bytes: 8, relativePath: 'references/guide.md', source: '/workspace/skills/review/references/guide.md' }), + Object.freeze({ bytes: 9, relativePath: 'SKILL.md', source: '/workspace/src/skills/review/SKILL.md' }), + Object.freeze({ bytes: 3, relativePath: 'assets/icon.bin', source: '/workspace/src/skills/review/assets/icon.bin' }), + Object.freeze({ bytes: 8, relativePath: 'references/guide.md', source: '/workspace/src/skills/review/references/guide.md' }), ]), - source: '/workspace/skills/review/SKILL.md', + source: '/workspace/src/skills/review/SKILL.md', targets: Object.freeze(['codex', 'claude']), }), ]), @@ -379,8 +379,8 @@ it('lowers Claude commands with documented kebab-case frontmatter and body-only id: 'command:review', markdown: '---\ndescription: Review changes\ntargets:\n - claude\n---\nReview the staged diff.\n', name: 'review', - provenance: { kind: 'conventional', sourcePath: '/workspace/commands/review.md' }, - source: '/workspace/commands/review.md', + provenance: { kind: 'conventional', sourcePath: '/workspace/src/commands/review.md' }, + source: '/workspace/src/commands/review.md', targets: ['claude'], }, { @@ -389,8 +389,8 @@ it('lowers Claude commands with documented kebab-case frontmatter and body-only id: 'command:explain', markdown: '# Explain\n\nExplain this code.', name: 'explain', - provenance: { kind: 'conventional', sourcePath: '/workspace/commands/explain.md' }, - source: '/workspace/commands/explain.md', + provenance: { kind: 'conventional', sourcePath: '/workspace/src/commands/explain.md' }, + source: '/workspace/src/commands/explain.md', targets: ['claude'], }, ], @@ -435,7 +435,7 @@ it('emits validated Claude manifest metadata fields with extension provenance', kind: 'write', sourceInputs: [ '/workspace/agent-bundle.config.ts', - '/workspace/skills/review/SKILL.md', + '/workspace/src/skills/review/SKILL.md', '/workspace/manifest-metadata.config.ts', ], }); @@ -968,9 +968,9 @@ it('plans byte-stable native Codex and Claude plugin trees from the same frozen kind: 'write', relativePath: '.mcp.json', }, - { bytes: 9, kind: 'copy', relativePath: 'skills/review/SKILL.md', source: '/workspace/skills/review/SKILL.md' }, - { bytes: 3, kind: 'copy', relativePath: 'skills/review/assets/icon.bin', source: '/workspace/skills/review/assets/icon.bin' }, - { bytes: 8, kind: 'copy', relativePath: 'skills/review/references/guide.md', source: '/workspace/skills/review/references/guide.md' }, + { bytes: 9, kind: 'copy', relativePath: 'skills/review/SKILL.md', source: '/workspace/src/skills/review/SKILL.md' }, + { bytes: 3, kind: 'copy', relativePath: 'skills/review/assets/icon.bin', source: '/workspace/src/skills/review/assets/icon.bin' }, + { bytes: 8, kind: 'copy', relativePath: 'skills/review/references/guide.md', source: '/workspace/src/skills/review/references/guide.md' }, ]); expect(claudePluginEntries).toMatchObject([ { @@ -988,25 +988,25 @@ it('plans byte-stable native Codex and Claude plugin trees from the same frozen kind: 'write', relativePath: '.mcp.json', }, - { bytes: 9, kind: 'copy', relativePath: 'skills/review/SKILL.md', source: '/workspace/skills/review/SKILL.md' }, - { bytes: 3, kind: 'copy', relativePath: 'skills/review/assets/icon.bin', source: '/workspace/skills/review/assets/icon.bin' }, - { bytes: 8, kind: 'copy', relativePath: 'skills/review/references/guide.md', source: '/workspace/skills/review/references/guide.md' }, + { bytes: 9, kind: 'copy', relativePath: 'skills/review/SKILL.md', source: '/workspace/src/skills/review/SKILL.md' }, + { bytes: 3, kind: 'copy', relativePath: 'skills/review/assets/icon.bin', source: '/workspace/src/skills/review/assets/icon.bin' }, + { bytes: 8, kind: 'copy', relativePath: 'skills/review/references/guide.md', source: '/workspace/src/skills/review/references/guide.md' }, ]); expect(codexPluginEntries.map((entry) => entry.sourceInputs)).toEqual([ ['/workspace/agent-bundle.config.ts'], - ['/workspace/agent-bundle.config.ts', '/workspace/skills/review/SKILL.md'], + ['/workspace/agent-bundle.config.ts', '/workspace/src/skills/review/SKILL.md'], ['/workspace/agent-bundle.config.ts'], - ['/workspace/skills/review/SKILL.md'], - ['/workspace/skills/review/SKILL.md', '/workspace/skills/review/assets/icon.bin'], - ['/workspace/skills/review/SKILL.md', '/workspace/skills/review/references/guide.md'], + ['/workspace/src/skills/review/SKILL.md'], + ['/workspace/src/skills/review/SKILL.md', '/workspace/src/skills/review/assets/icon.bin'], + ['/workspace/src/skills/review/SKILL.md', '/workspace/src/skills/review/references/guide.md'], ]); expect(claudePluginEntries.map((entry) => entry.sourceInputs)).toEqual([ ['/workspace/agent-bundle.config.ts'], - ['/workspace/agent-bundle.config.ts', '/workspace/skills/review/SKILL.md'], + ['/workspace/agent-bundle.config.ts', '/workspace/src/skills/review/SKILL.md'], ['/workspace/agent-bundle.config.ts'], - ['/workspace/skills/review/SKILL.md'], - ['/workspace/skills/review/SKILL.md', '/workspace/skills/review/assets/icon.bin'], - ['/workspace/skills/review/SKILL.md', '/workspace/skills/review/references/guide.md'], + ['/workspace/src/skills/review/SKILL.md'], + ['/workspace/src/skills/review/SKILL.md', '/workspace/src/skills/review/assets/icon.bin'], + ['/workspace/src/skills/review/SKILL.md', '/workspace/src/skills/review/references/guide.md'], ]); await validateDocuments('codex', writeContents(plugin, 'codex')); await validateDocuments('claude', writeContents(plugin, 'claude')); @@ -1213,7 +1213,7 @@ it('emits sorted, allowlisted Claude userConfig declarations with config provena expect(plan.diagnostics).toEqual([]); expect(manifest).toMatchObject({ kind: 'write', - sourceInputs: ['/workspace/agent-bundle.config.ts', '/workspace/skills/review/SKILL.md', '/workspace/claude.config.ts'], + sourceInputs: ['/workspace/agent-bundle.config.ts', '/workspace/src/skills/review/SKILL.md', '/workspace/claude.config.ts'], }); if (manifest?.kind !== 'write') throw new Error('Expected an emitted Claude plugin manifest.'); expect(JSON.parse(manifest.content).userConfig).toEqual({ @@ -2400,8 +2400,8 @@ it('records every selected component provenance for generated host documents', ( }, skills: plugin.skills.map((skill) => ({ ...skill, - provenance: { kind: 'conventional' as const, sourcePath: '/inputs/skills/review/SKILL.md' }, - source: '/inputs/skills/review/SKILL.md', + provenance: { kind: 'conventional' as const, sourcePath: '/inputs/src/skills/review/SKILL.md' }, + source: '/inputs/src/skills/review/SKILL.md', })), targets: plugin.targets.map((target) => ({ ...target, @@ -2416,7 +2416,7 @@ it('records every selected component provenance for generated host documents', ( `/inputs/${target}.target.ts`, '/inputs/mcp.config.ts', '/inputs/hook.config.ts', - '/inputs/skills/review/SKILL.md', + '/inputs/src/skills/review/SKILL.md', ]; expect(byPath[target === 'codex' ? '.codex-plugin/plugin.json' : '.claude-plugin/plugin.json']?.sourceInputs).toEqual(common); expect(byPath['.mcp.json']?.sourceInputs).toEqual([ @@ -2614,7 +2614,7 @@ it('filters host components and builds portable, Codex, and Claude target roots' const root = await mkdtemp(join(tmpdir(), 'agent-bundle-host-adapter-')); const outputRoot = join(root, 'dist'); - const skillRoot = join(root, 'skills', 'review'); + const skillRoot = join(root, 'src', 'skills', 'review'); const skillMarkdown = '---\nname: review\ndescription: Review code and explain findings.\n---\n# Review\n'; await mkdir(join(skillRoot, 'references'), { recursive: true }); await Promise.all([ diff --git a/packages/agent-bundle/tests/integration-matrix.test.ts b/packages/agent-bundle/tests/integration-matrix.test.ts index c7925ffee..487361e89 100644 --- a/packages/agent-bundle/tests/integration-matrix.test.ts +++ b/packages/agent-bundle/tests/integration-matrix.test.ts @@ -71,7 +71,7 @@ it('builds the checked-in fixture matrix from a path with spaces', async () => { 'fixture reference\n', ); await expect(readFile(join(output, 'portable', 'skills', 'review', 'assets', 'binary.bin'))).resolves.toEqual( - await readFile(join(root, 'skills', 'review', 'assets', 'binary.bin')), + await readFile(join(root, 'src', 'skills', 'review', 'assets', 'binary.bin')), ); const [portableMcp, codexMcp, claudeMcp, codexHooks, claudeHooks, codexMarketplace, claudeMarketplace] = @@ -202,7 +202,7 @@ it('builds the checked-in portable skills-only fixture', async () => { 'portable guide\n', ); await expect(readFile(join(output, 'portable', 'skills', 'portable-skill', 'assets', 'binary.bin'))).resolves.toEqual( - await readFile(join(root, 'skills', 'portable-skill', 'assets', 'binary.bin')), + await readFile(join(root, 'src', 'skills', 'portable-skill', 'assets', 'binary.bin')), ); } finally { await rm(parent, { force: true, recursive: true }); diff --git a/packages/agent-bundle/tests/manifest.test.ts b/packages/agent-bundle/tests/manifest.test.ts index 468b503e3..113babc7d 100644 --- a/packages/agent-bundle/tests/manifest.test.ts +++ b/packages/agent-bundle/tests/manifest.test.ts @@ -15,7 +15,7 @@ const hash = (character: string): string => character.repeat(64); const sourceInputs = Object.freeze([ Object.freeze({ path: 'agent-bundle.config.ts', sha256: hash('a') }), - Object.freeze({ path: 'skills/review/SKILL.md', sha256: hash('b') }), + Object.freeze({ path: 'src/skills/review/SKILL.md', sha256: hash('b') }), ]); const validManifest = (): ArtifactManifest => ({ @@ -38,7 +38,7 @@ const validManifest = (): ArtifactManifest => ({ mode: 0o755, path: 'codex/scripts/review.mjs', sha256: hash('d'), - sourceInputs: ['agent-bundle.config.ts', 'skills/review/SKILL.md'], + sourceInputs: ['agent-bundle.config.ts', 'src/skills/review/SKILL.md'], }, ], producer: { diff --git a/packages/agent-bundle/tests/normalization.test.ts b/packages/agent-bundle/tests/normalization.test.ts index 0177e9909..1e9fe535f 100644 --- a/packages/agent-bundle/tests/normalization.test.ts +++ b/packages/agent-bundle/tests/normalization.test.ts @@ -65,7 +65,7 @@ const skill = ( resources?: string[]; } = {}, ): DiscoveredProject['skills'][number] => { - const dir = `${root}/skills/${directory}`; + const dir = `${root}/src/skills/${directory}`; return { body: options.body ?? '# Skill\n', @@ -571,7 +571,7 @@ it('produces root-independent IDs, complete provenance, and deeply immutable out const rightRoot = '/different/right'; const config: AgentBundleConfig = { plugin: { name: 'review-tools', version: '1.0.0' }, - skills: ['skills/review'], + skills: ['src/skills/review'], }; const leftSkill = skill(leftRoot, 'review', 'review', { resources: ['SKILL.md', 'references/checklist.md'], diff --git a/packages/agent-bundle/tests/package-conventions.test.ts b/packages/agent-bundle/tests/package-conventions.test.ts index 1ef6cb868..58e188666 100644 --- a/packages/agent-bundle/tests/package-conventions.test.ts +++ b/packages/agent-bundle/tests/package-conventions.test.ts @@ -482,36 +482,102 @@ describe('migration nudges (AB473x)', () => { return { diagnostics: validateSource(loaded, discovered, registry), root }; }; + it('errors with AB4736 for a skill in the removed top-level conventional location', async () => { + const { diagnostics, root } = await discoveredAndValidated( + {}, + { 'skills/legacy/SKILL.md': skillMarkdown('legacy') }, + ); + + expect(diagnostics).toEqual([{ + code: 'AB4736', + message: expect.stringContaining('skills/legacy/SKILL.md'), + recovery: expect.stringContaining('src/skills/legacy/SKILL.md'), + severity: 'error', + sourcePath: `${root}/skills/legacy/SKILL.md`, + }]); + }); + + it('does not report AB4736 when explicit skills config claims a top-level skill', async () => { + const { diagnostics } = await discoveredAndValidated( + { skills: ['skills/legacy'] }, + { 'skills/legacy/SKILL.md': skillMarkdown('legacy') }, + ); + + expect(diagnostics.filter(({ code }) => code === 'AB4736')).toEqual([]); + }); + + it('errors with AB4736 for top-level command and rule documents', async () => { + const { diagnostics, root } = await discoveredAndValidated( + {}, + { + 'commands/legacy.md': '# Legacy command\n', + 'rules/legacy.mdc': '---\ndescription: Legacy rule\n---\nAlways verify.\n', + }, + ); + + expect(diagnostics).toEqual([ + { + code: 'AB4736', + message: expect.stringContaining('commands/legacy.md'), + recovery: expect.stringContaining('src/commands/legacy.md'), + severity: 'error', + sourcePath: `${root}/commands/legacy.md`, + }, + { + code: 'AB4736', + message: expect.stringContaining('rules/legacy.mdc'), + recovery: expect.stringContaining('src/rules/legacy.mdc'), + severity: 'error', + sourcePath: `${root}/rules/legacy.mdc`, + }, + ]); + }); + + it('discovers skills from the src convention', async () => { + const root = await projectRoot({ + 'src/skills/review/SKILL.md': skillMarkdown('review'), + }); + + const discovered = await discoverProject(root, { + plugin: { name: 'review-tools', version: '1.0.0' }, + }); + + expect(discovered.skills).toMatchObject([{ + dir: `${root}/src/skills/review`, + source: `${root}/src/skills/review/SKILL.md`, + }]); + }); + it('nudges AB4734 when explicit skills configuration shadows a conventional skill', async () => { const { diagnostics, root } = await discoveredAndValidated( - { skills: ['skills/covered'] }, + { skills: ['src/skills/covered'] }, { - 'skills/covered/SKILL.md': skillMarkdown('covered'), - 'skills/shadowed/SKILL.md': skillMarkdown('shadowed'), + 'src/skills/covered/SKILL.md': skillMarkdown('covered'), + 'src/skills/shadowed/SKILL.md': skillMarkdown('shadowed'), }, ); expect(diagnostics).toEqual([{ code: 'AB4734', - message: expect.stringContaining('skills/shadowed/SKILL.md'), + message: expect.stringContaining('src/skills/shadowed/SKILL.md'), recovery: expect.stringContaining('Optional'), severity: 'info', - sourcePath: `${root}/skills/shadowed/SKILL.md`, + sourcePath: `${root}/src/skills/shadowed/SKILL.md`, }]); }); it('stays silent when skills config is absent or covers every conventional skill', async () => { const files = { - 'skills/one/SKILL.md': skillMarkdown('one'), - 'skills/two/SKILL.md': skillMarkdown('two'), + 'src/skills/one/SKILL.md': skillMarkdown('one'), + 'src/skills/two/SKILL.md': skillMarkdown('two'), }; const conventional = await discoveredAndValidated({}, files); expect(conventional.diagnostics).toEqual([]); - const globCovered = await discoveredAndValidated({ skills: ['skills/*'] }, files); + const globCovered = await discoveredAndValidated({ skills: ['src/skills/*'] }, files); expect(globCovered.diagnostics).toEqual([]); const literalCovered = await discoveredAndValidated( - { skills: ['skills/one', 'skills/two/SKILL.md'] }, + { skills: ['src/skills/one', 'src/skills/two/SKILL.md'] }, files, ); expect(literalCovered.diagnostics).toEqual([]); diff --git a/packages/agent-bundle/tests/plugin-bundle.test.ts b/packages/agent-bundle/tests/plugin-bundle.test.ts index a48210971..8900548a0 100644 --- a/packages/agent-bundle/tests/plugin-bundle.test.ts +++ b/packages/agent-bundle/tests/plugin-bundle.test.ts @@ -59,16 +59,16 @@ const bundleModel = Object.freeze({ Object.freeze({ body: '# Review\n', description: 'Review code and explain findings.', - dir: '/workspace/skills/review', + dir: '/workspace/src/skills/review', frontmatter: Object.freeze({ description: 'Review code and explain findings.', name: 'review' }), id: 'skill:review', name: 'review', - provenance: Object.freeze({ kind: 'conventional' as const, sourcePath: '/workspace/skills/review/SKILL.md' }), + provenance: Object.freeze({ kind: 'conventional' as const, sourcePath: '/workspace/src/skills/review/SKILL.md' }), resources: Object.freeze([ - Object.freeze({ bytes: 9, relativePath: 'SKILL.md', source: '/workspace/skills/review/SKILL.md' }), - Object.freeze({ bytes: 8, relativePath: 'references/guide.md', source: '/workspace/skills/review/references/guide.md' }), + Object.freeze({ bytes: 9, relativePath: 'SKILL.md', source: '/workspace/src/skills/review/SKILL.md' }), + Object.freeze({ bytes: 8, relativePath: 'references/guide.md', source: '/workspace/src/skills/review/references/guide.md' }), ]), - source: '/workspace/skills/review/SKILL.md', + source: '/workspace/src/skills/review/SKILL.md', targets: Object.freeze(['plugin']), }), ]), @@ -668,8 +668,8 @@ it('emits Cursor-only rules once at the shared root and documents the honest hos id: 'rule:focused', markdown, name: 'focused', - provenance: { kind: 'conventional', sourcePath: '/workspace/rules/focused.mdc' }, - source: '/workspace/rules/focused.mdc', + provenance: { kind: 'conventional', sourcePath: '/workspace/src/rules/focused.mdc' }, + source: '/workspace/src/rules/focused.mdc', targets: ['plugin'], }], }; @@ -705,8 +705,8 @@ it('emits Claude-format commands without pointing Cursor at the shared directory id: 'command:review', markdown: '---\ndescription: Review changes\nargumentHint: "[path]"\n---\nReview the staged diff.\n', name: 'review', - provenance: { kind: 'conventional', sourcePath: '/workspace/commands/review.md' }, - source: '/workspace/commands/review.md', + provenance: { kind: 'conventional', sourcePath: '/workspace/src/commands/review.md' }, + source: '/workspace/src/commands/review.md', targets: ['plugin'], }], }; @@ -764,7 +764,7 @@ it('reports a bundle-target conflict instead of silently overwriting an entry', it('builds the unified bundle root on disk with a compiled universal hook wrapper', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-plugin-bundle-')); const outputRoot = join(root, 'dist'); - const skillRoot = join(root, 'skills', 'review'); + const skillRoot = join(root, 'src', 'skills', 'review'); const skillMarkdown = '---\nname: review\ndescription: Review code and explain findings.\n---\n\n# Review\n'; await mkdir(join(skillRoot, 'references'), { recursive: true }); await Promise.all([ diff --git a/packages/agent-bundle/tests/portable-adapter.test.ts b/packages/agent-bundle/tests/portable-adapter.test.ts index 575e518ea..314cfe615 100644 --- a/packages/agent-bundle/tests/portable-adapter.test.ts +++ b/packages/agent-bundle/tests/portable-adapter.test.ts @@ -24,24 +24,24 @@ const plugin = (): NormalizedPlugin => ({ { body: 'Use the included resource.\n', description: 'A skill with every discovered file.', - dir: '/workspace/skills/reporter', + dir: '/workspace/src/skills/reporter', frontmatter: { description: 'A skill with every discovered file.', name: 'reporter' }, id: 'skill:reporter', name: 'reporter', - provenance: { kind: 'conventional', sourcePath: '/workspace/skills/reporter/SKILL.md' }, + provenance: { kind: 'conventional', sourcePath: '/workspace/src/skills/reporter/SKILL.md' }, resources: [ { bytes: 23, relativePath: 'SKILL.md', - source: '/workspace/skills/reporter/SKILL.md', + source: '/workspace/src/skills/reporter/SKILL.md', }, { bytes: 12, relativePath: 'references/guide.md', - source: '/workspace/skills/reporter/references/guide.md', + source: '/workspace/src/skills/reporter/references/guide.md', }, ], - source: '/workspace/skills/reporter/SKILL.md', + source: '/workspace/src/skills/reporter/SKILL.md', targets: ['portable'], }, ], @@ -100,19 +100,19 @@ it('plans a schema-valid skills-only plugin with every discovered resource', () bytes: 23, kind: 'copy', relativePath: 'skills/reporter/SKILL.md', - source: '/workspace/skills/reporter/SKILL.md', + source: '/workspace/src/skills/reporter/SKILL.md', }, { bytes: 12, kind: 'copy', relativePath: 'skills/reporter/references/guide.md', - source: '/workspace/skills/reporter/references/guide.md', + source: '/workspace/src/skills/reporter/references/guide.md', }, ]); expect(pluginEntries.map((entry) => entry.sourceInputs)).toEqual([ ['/workspace/agent-bundle.config.ts'], - ['/workspace/skills/reporter/SKILL.md'], - ['/workspace/skills/reporter/SKILL.md', '/workspace/skills/reporter/references/guide.md'], + ['/workspace/src/skills/reporter/SKILL.md'], + ['/workspace/src/skills/reporter/SKILL.md', '/workspace/src/skills/reporter/references/guide.md'], ]); }); diff --git a/packages/agent-bundle/tests/provenance.test.ts b/packages/agent-bundle/tests/provenance.test.ts index 263893534..913eb28ad 100644 --- a/packages/agent-bundle/tests/provenance.test.ts +++ b/packages/agent-bundle/tests/provenance.test.ts @@ -26,7 +26,7 @@ it('canonicalizes artifact outputs and project inputs into deeply frozen stable path: '/tmp/agent-bundle.stage/portable/scripts/greeting.mjs', sourceInputs: [ '/work/project/src/greeting.ts', - '/work/project/skills/review/../review/scripts/greeting.ts', + '/work/project/src/skills/review/../review/scripts/greeting.ts', '/work/project/src/greeting.ts', ], } satisfies ArtifactOutputCandidate], @@ -35,7 +35,7 @@ it('canonicalizes artifact outputs and project inputs into deeply frozen stable expect(records).toEqual([{ kind: 'bundle', path: 'portable/scripts/greeting.mjs', - sourceInputs: ['skills/review/scripts/greeting.ts', 'src/greeting.ts'], + sourceInputs: ['src/greeting.ts', 'src/skills/review/scripts/greeting.ts'], }]); expect(Object.isFrozen(records)).toBe(true); expect(Object.isFrozen(records[0]!)).toBe(true); @@ -47,7 +47,7 @@ it('canonicalizes artifact outputs and project inputs into deeply frozen stable kind: 'bundle', path: '/tmp/another-agent-bundle.stage/portable/scripts/greeting.mjs', sourceInputs: [ - '/another/project/skills/review/scripts/greeting.ts', + '/another/project/src/skills/review/scripts/greeting.ts', '/another/project/src/greeting.ts', ], }], @@ -81,7 +81,7 @@ it('collects nested authored module inputs from public stats without using ident const evidence = collectBundledOutputEvidence({ expectedAssets: [{ path: 'portable/scripts/greeting.mjs', - sourceInputs: ['/work/project/skills/review/scripts/greeting script.ts'], + sourceInputs: ['/work/project/src/skills/review/scripts/greeting script.ts'], }], projectRoot, stats: { @@ -91,9 +91,9 @@ it('collects nested authored module inputs from public stats without using ident identifier: '/outside/must-not-be-read.ts', modules: [{ identifier: '/outside/also-not-read.ts', - nameForCondition: '/work/project/skills/review/scripts/local greeting module.ts', + nameForCondition: '/work/project/src/skills/review/scripts/local greeting module.ts', }], - nameForCondition: '/work/project/skills/review/scripts/greeting script.ts', + nameForCondition: '/work/project/src/skills/review/scripts/greeting script.ts', }], }), }, @@ -102,8 +102,8 @@ it('collects nested authored module inputs from public stats without using ident expect(evidence).toEqual([{ path: 'portable/scripts/greeting.mjs', sourceInputs: [ - '/work/project/skills/review/scripts/greeting script.ts', - '/work/project/skills/review/scripts/local greeting module.ts', + '/work/project/src/skills/review/scripts/greeting script.ts', + '/work/project/src/skills/review/scripts/local greeting module.ts', ], }]); expect(Object.isFrozen(evidence)).toBe(true); diff --git a/packages/agent-bundle/tests/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts index 771c46372..0a3c7ac18 100644 --- a/packages/agent-bundle/tests/public-api-packed.test.ts +++ b/packages/agent-bundle/tests/public-api-packed.test.ts @@ -29,7 +29,7 @@ const readPackageManifest = async (): Promise => const createBuildProject = async (root: string): Promise<{ readonly output: string; readonly project: string }> => { const project = join(root, 'manifest-version-project'); const output = join(project, 'manifest-version-artifact'); - await mkdir(join(project, 'skills', 'review'), { recursive: true }); + await mkdir(join(project, 'src', 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile(join(project, 'package.json'), '{"type":"module"}\n'), writeFile( @@ -37,7 +37,7 @@ const createBuildProject = async (root: string): Promise<{ readonly output: stri "export default { plugin: { name: 'manifest-version-fixture', version: '1.0.0' }, targets: ['portable'] };\n", ), writeFile( - join(project, 'skills', 'review', 'SKILL.md'), + join(project, 'src', 'skills', 'review', 'SKILL.md'), '---\nname: review\ndescription: Reviews changes\n---\n# Review\n', ), ]); diff --git a/packages/agent-bundle/tests/public-api.test.ts b/packages/agent-bundle/tests/public-api.test.ts index fb3e1a69e..25bd4370a 100644 --- a/packages/agent-bundle/tests/public-api.test.ts +++ b/packages/agent-bundle/tests/public-api.test.ts @@ -60,7 +60,7 @@ const readPackageManifest = async (): Promise => const createBuildProject = async (root: string): Promise<{ readonly output: string; readonly project: string }> => { const project = join(root, 'manifest-version-project'); const output = join(project, 'manifest-version-artifact'); - await mkdir(join(project, 'skills', 'review'), { recursive: true }); + await mkdir(join(project, 'src', 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile(join(project, 'package.json'), '{"type":"module"}\n'), writeFile( @@ -68,7 +68,7 @@ const createBuildProject = async (root: string): Promise<{ readonly output: stri "export default { plugin: { name: 'manifest-version-fixture', version: '1.0.0' }, targets: ['portable'] };\n", ), writeFile( - join(project, 'skills', 'review', 'SKILL.md'), + join(project, 'src', 'skills', 'review', 'SKILL.md'), '---\nname: review\ndescription: Reviews changes\n---\n# Review\n', ), ]); diff --git a/packages/agent-bundle/tests/rendered-skills.test.ts b/packages/agent-bundle/tests/rendered-skills.test.ts index 019e0eb83..8d2fbd7e8 100644 --- a/packages/agent-bundle/tests/rendered-skills.test.ts +++ b/packages/agent-bundle/tests/rendered-skills.test.ts @@ -136,7 +136,7 @@ describe('rendered skill compilation', () => { it('compiles the JSX fixture through jiti to the full SKILL.md document', async () => { const compiled = await compileRenderedSkill( - join(fixtureRoot, 'skills', 'deploy-checklist', 'SKILL.tsx'), + join(fixtureRoot, 'src', 'skills', 'deploy-checklist', 'SKILL.tsx'), ); expect(compiled.status).toBe('compiled'); if (compiled.status !== 'compiled') return; @@ -151,7 +151,7 @@ describe('rendered skill compilation', () => { }); it('parses a rendered skill directory: compiled body, module source, no source-file resource', async () => { - const skill = await parseSkill(join(fixtureRoot, 'skills', 'deploy-checklist'), fixtureRoot); + const skill = await parseSkill(join(fixtureRoot, 'src', 'skills', 'deploy-checklist'), fixtureRoot); expect(skill.diagnostics).toEqual([]); expect(skill.rendered).toBe(true); expect(skill.source.endsWith('SKILL.tsx')).toBe(true); @@ -209,14 +209,14 @@ describe('rendered skill compilation', () => { it('loads rendered modules that build plain element objects without react', async () => { const root = await projectRoot({ - 'skills/plain/SKILL.ts': [ + 'src/skills/plain/SKILL.ts': [ "export const frontmatter = { description: 'A plain rendered skill.', name: 'plain' };", "const paragraph = { props: { children: 'Composed without JSX.' }, type: 'p' };", "export default () => [{ props: { children: 'Plain' }, type: 'h1' }, paragraph];", '', ].join('\n'), }); - const skill = await parseSkill(join(root, 'skills', 'plain'), root); + const skill = await parseSkill(join(root, 'src', 'skills', 'plain'), root); expect(skill.diagnostics).toEqual([]); expect(skill.markdown).toBe([ '---', @@ -233,23 +233,23 @@ describe('rendered skill compilation', () => { it('reports AB3003 for a module that fails to load', async () => { const root = await projectRoot({ - 'skills/broken/SKILL.ts': "throw new Error('boom');\nexport default () => null;\n", + 'src/skills/broken/SKILL.ts': "throw new Error('boom');\nexport default () => null;\n", }); - const skill = await parseSkill(join(root, 'skills', 'broken'), root); + const skill = await parseSkill(join(root, 'src', 'skills', 'broken'), root); expect(skill.diagnostics).toEqual([expect.objectContaining({ code: 'AB3003', severity: 'error' })]); }); it('reports AB3004 for a missing default component or frontmatter export', async () => { const root = await projectRoot({ - 'skills/no-component/SKILL.ts': "export const frontmatter = { name: 'no-component' };\n", - 'skills/no-frontmatter/SKILL.ts': "export default () => ({ props: { children: 'x' }, type: 'p' });\n", + 'src/skills/no-component/SKILL.ts': "export const frontmatter = { name: 'no-component' };\n", + 'src/skills/no-frontmatter/SKILL.ts': "export default () => ({ props: { children: 'x' }, type: 'p' });\n", }); - const noComponent = await parseSkill(join(root, 'skills', 'no-component'), root); + const noComponent = await parseSkill(join(root, 'src', 'skills', 'no-component'), root); expect(noComponent.diagnostics).toEqual([expect.objectContaining({ code: 'AB3004', message: expect.stringContaining('default-export a component'), })]); - const noFrontmatter = await parseSkill(join(root, 'skills', 'no-frontmatter'), root); + const noFrontmatter = await parseSkill(join(root, 'src', 'skills', 'no-frontmatter'), root); expect(noFrontmatter.diagnostics).toEqual([expect.objectContaining({ code: 'AB3004', message: expect.stringContaining('frontmatter'), @@ -258,13 +258,13 @@ describe('rendered skill compilation', () => { it('reports AB3005 when the tree renders outside the supported subset', async () => { const root = await projectRoot({ - 'skills/tabular/SKILL.ts': [ + 'src/skills/tabular/SKILL.ts': [ "export const frontmatter = { description: 'Tables are unsupported.', name: 'tabular' };", "export default () => ({ props: {}, type: 'table' });", '', ].join('\n'), }); - const skill = await parseSkill(join(root, 'skills', 'tabular'), root); + const skill = await parseSkill(join(root, 'src', 'skills', 'tabular'), root); expect(skill.diagnostics).toEqual([expect.objectContaining({ code: 'AB3005', message: expect.stringContaining(''), @@ -273,14 +273,14 @@ describe('rendered skill compilation', () => { it('nudges AB4735 when a hand-authored SKILL.md shadows the rendered source', async () => { const root = await projectRoot({ - 'skills/both/SKILL.md': '---\nname: both\ndescription: The authored document wins.\n---\n\n# Both\n\nAuthored.\n', - 'skills/both/SKILL.tsx': [ + 'src/skills/both/SKILL.md': '---\nname: both\ndescription: The authored document wins.\n---\n\n# Both\n\nAuthored.\n', + 'src/skills/both/SKILL.tsx': [ "export const frontmatter = { description: 'Never compiled.', name: 'both' };", "export default () => ({ props: { children: 'Rendered' }, type: 'h1' });", '', ].join('\n'), }); - const skill = await parseSkill(join(root, 'skills', 'both'), root); + const skill = await parseSkill(join(root, 'src', 'skills', 'both'), root); expect(skill.rendered).toBeUndefined(); expect(skill.body).toContain('Authored.'); expect(skill.diagnostics).toEqual([expect.objectContaining({ diff --git a/packages/agent-bundle/tests/route-graph.test.ts b/packages/agent-bundle/tests/route-graph.test.ts index e52584ef0..cdf2799bb 100644 --- a/packages/agent-bundle/tests/route-graph.test.ts +++ b/packages/agent-bundle/tests/route-graph.test.ts @@ -375,7 +375,7 @@ it('errors with AB4804 on invalid routes mode overrides', async () => { it('attaches no routeGraph key to a route-free discovered project', async () => { const root = await createRoot(); await writeTree(root, { - 'skills/review/SKILL.md': '---\nname: review\ndescription: Reviews changes\n---\n# Review\n', + 'src/skills/review/SKILL.md': '---\nname: review\ndescription: Reviews changes\n---\n# Review\n', }); const discovered = await discoverProject(root, fixtureConfig()); diff --git a/packages/agent-bundle/tests/rule-config.test.ts b/packages/agent-bundle/tests/rule-config.test.ts index b9c77f26a..54c09f45b 100644 --- a/packages/agent-bundle/tests/rule-config.test.ts +++ b/packages/agent-bundle/tests/rule-config.test.ts @@ -50,9 +50,9 @@ const withProject = async ( it('accepts body-only rules and retains exact authored bytes', async () => { await withProject(async (root) => { - const source = join(root, 'rules', 'review.mdc'); + const source = join(root, 'src', 'rules', 'review.mdc'); const markdown = '# Review\n\nCheck the staged diff.'; - await mkdir(join(root, 'rules')); + await mkdir(join(root, 'src', 'rules'), { recursive: true }); await writeFile(source, markdown); const rule = await parseRule(source); @@ -71,8 +71,8 @@ it('accepts body-only rules and retains exact authored bytes', async () => { it('peels targets and rejects unknown frontmatter fields and malformed field shapes', async () => { await withProject(async (root) => { - await mkdir(join(root, 'rules')); - const validSource = join(root, 'rules', 'cursor-only.mdc'); + await mkdir(join(root, 'src', 'rules'), { recursive: true }); + const validSource = join(root, 'src', 'rules', 'cursor-only.mdc'); const validMarkdown = [ '---', 'description: Cursor review guidance', @@ -111,7 +111,7 @@ it('peels targets and rejects unknown frontmatter fields and malformed field sha ].join('\n')); expect(valid.emittedMarkdown).not.toContain('targets:'); - const invalidSource = join(root, 'rules', 'invalid.mdc'); + const invalidSource = join(root, 'src', 'rules', 'invalid.mdc'); await writeFile( invalidSource, [ @@ -141,10 +141,10 @@ it('peels targets and rejects unknown frontmatter fields and malformed field sha it('preserves target-free frontmatter bytes and emits target-only rules as body-only', async () => { await withProject(async (root) => { - await mkdir(join(root, 'rules')); - const targetFreeSource = join(root, 'rules', 'target-free.mdc'); + await mkdir(join(root, 'src', 'rules'), { recursive: true }); + const targetFreeSource = join(root, 'src', 'rules', 'target-free.mdc'); const targetFreeMarkdown = '---\r\ndescription: Keep CRLF\r\n---\r\nBody without trailing newline'; - const targetOnlySource = join(root, 'rules', 'target-only.mdc'); + const targetOnlySource = join(root, 'src', 'rules', 'target-only.mdc'); await Promise.all([ writeFile(targetFreeSource, targetFreeMarkdown), writeFile(targetOnlySource, '---\ntargets:\n - cursor\n---\nTarget-only body'), @@ -164,8 +164,8 @@ it('preserves target-free frontmatter bytes and emits target-only rules as body- it('reports malformed YAML frontmatter with a fresh rule diagnostic', async () => { await withProject(async (root) => { - const source = join(root, 'rules', 'malformed.mdc'); - await mkdir(join(root, 'rules')); + const source = join(root, 'src', 'rules', 'malformed.mdc'); + await mkdir(join(root, 'src', 'rules'), { recursive: true }); await writeFile(source, '---\nglobs: [unterminated\n---\n# Broken\n'); expect((await parseRule(source)).diagnostics).toEqual([ @@ -176,12 +176,12 @@ it('reports malformed YAML frontmatter with a fresh rule diagnostic', async () = it('discovers flat non-ignored rules deterministically and omits the collection when empty', async () => { await withProject(async (root) => { - await mkdir(join(root, 'rules')); + await mkdir(join(root, 'src', 'rules'), { recursive: true }); await Promise.all([ - writeFile(join(root, '.gitignore'), 'rules/ignored.mdc\n'), - writeFile(join(root, 'rules', 'zeta.mdc'), '# Zeta\n'), - writeFile(join(root, 'rules', 'alpha.mdc'), '# Alpha\n'), - writeFile(join(root, 'rules', 'ignored.mdc'), '# Ignored\n'), + writeFile(join(root, '.gitignore'), 'src/rules/ignored.mdc\n'), + writeFile(join(root, 'src', 'rules', 'zeta.mdc'), '# Zeta\n'), + writeFile(join(root, 'src', 'rules', 'alpha.mdc'), '# Alpha\n'), + writeFile(join(root, 'src', 'rules', 'ignored.mdc'), '# Ignored\n'), ]); const config: AgentBundleConfig = { plugin: { name: 'rule-fixture', version: '1.0.0' }, @@ -189,11 +189,11 @@ it('discovers flat non-ignored rules deterministically and omits the collection const discovered = await discoverProject(root, config); expect(discovered.rules?.map((rule) => rule.source)).toEqual([ - join(root, 'rules', 'alpha.mdc'), - join(root, 'rules', 'zeta.mdc'), + join(root, 'src', 'rules', 'alpha.mdc'), + join(root, 'src', 'rules', 'zeta.mdc'), ]); - await rm(join(root, 'rules'), { recursive: true }); + await rm(join(root, 'src', 'rules'), { recursive: true }); expect(await discoverProject(root, config)).not.toHaveProperty('rules'); }); }); @@ -214,7 +214,7 @@ it('normalizes peeled rule targets and reports unknown, unavailable, and duplica }); const registry = createDefaultRegistry(); const cursorLoaded = loadedProject(root, ['cursor']); - const cursorRule = rule(join(root, 'rules', 'review.mdc'), ['cursor']); + const cursorRule = rule(join(root, 'src', 'rules', 'review.mdc'), ['cursor']); const cursorDiscovered: DiscoveredProject = { rules: [cursorRule], skills: [] }; const model = await normalizeProject(cursorLoaded, cursorDiscovered, registry); @@ -230,13 +230,13 @@ it('normalizes peeled rule targets and reports unknown, unavailable, and duplica targets: ['cursor'], }]); - const unknown = rule(join(root, 'rules', 'unknown.mdc'), ['claude']); + const unknown = rule(join(root, 'src', 'rules', 'unknown.mdc'), ['claude']); expect(validateSource(cursorLoaded, { rules: [unknown], skills: [] }, registry)).toEqual([ expect.objectContaining({ code: 'AB4904', sourcePath: unknown.source }), ]); const claudeLoaded = loadedProject(root, ['claude']); - const unavailable = rule(join(root, 'rules', 'unavailable.mdc'), ['claude']); + const unavailable = rule(join(root, 'src', 'rules', 'unavailable.mdc'), ['claude']); expect(validateSource(claudeLoaded, { rules: [unavailable], skills: [] }, registry)).toEqual([ expect.objectContaining({ code: 'AB4905', diff --git a/packages/agent-bundle/tests/skill-document-service.test.ts b/packages/agent-bundle/tests/skill-document-service.test.ts index 3e97e0055..597705d3e 100644 --- a/packages/agent-bundle/tests/skill-document-service.test.ts +++ b/packages/agent-bundle/tests/skill-document-service.test.ts @@ -35,7 +35,7 @@ const createProject = async (): Promise => (await createProjectFixture({ '', ].join('\n'), files: { - 'skills/review/SKILL.md': [ + 'src/skills/review/SKILL.md': [ '---', 'name: review', 'description: Reviews changed files', @@ -45,9 +45,9 @@ const createProject = async (): Promise => (await createProjectFixture({ 'Read [the guide](guide.md) and ![the image](assets/pixel.bin).', '', ].join('\n'), - 'skills/review/guide.md': '# Guide\n', - 'skills/review/assets/pixel.bin': new Uint8Array([0, 255, 17, 9]), - 'skills/review/assets/probe.html': '\n', + 'src/skills/review/guide.md': '# Guide\n', + 'src/skills/review/assets/pixel.bin': new Uint8Array([0, 255, 17, 9]), + 'src/skills/review/assets/probe.html': '\n', }, prefix: 'agent-bundle-skill-document-', })).root; @@ -88,7 +88,7 @@ it('serves parsed source documents and exact source resources by a model-owned S }); expect(binary.contentType).toBe('application/octet-stream'); expect(binary.body).toEqual(new Uint8Array([0, 255, 17, 9])); - expect([...await readFile(join(root, 'skills', 'review', 'assets', 'pixel.bin'))]).toEqual([...binary.body]); + expect([...await readFile(join(root, 'src', 'skills', 'review', 'assets', 'pixel.bin'))]).toEqual([...binary.body]); } finally { await rm(root, { force: true, recursive: true }); } @@ -255,7 +255,7 @@ it('rejects traversal and symlink resource mutations after exact model membershi projectService: new ProjectService({ root }), root, }); - const pixel = join(root, 'skills', 'review', 'assets', 'pixel.bin'); + const pixel = join(root, 'src', 'skills', 'review', 'assets', 'pixel.bin'); const outside = join(root, 'outside.bin'); await writeFile(outside, 'outside'); diff --git a/packages/agent-bundle/tests/skill-ir.test.ts b/packages/agent-bundle/tests/skill-ir.test.ts index 99151b578..ebb911094 100644 --- a/packages/agent-bundle/tests/skill-ir.test.ts +++ b/packages/agent-bundle/tests/skill-ir.test.ts @@ -169,8 +169,8 @@ describe('skill token registry', () => { describe('canonical Skill IR', () => { it('keeps a portable SKILL.md byte-stable when no extension or placeholder requires target output', async () => { - const root = await projectRoot({ 'skills/review/SKILL.md': portableMarkdown }); - const document = await parseSkill(join(root, 'skills', 'review'), root); + const root = await projectRoot({ 'src/skills/review/SKILL.md': portableMarkdown }); + const document = await parseSkill(join(root, 'src', 'skills', 'review'), root); const ir = parseSkillIr(document); expect(ir.diagnostics).toEqual([]); expect(ir.passThrough).toBe(true); @@ -207,8 +207,8 @@ describe('canonical Skill IR', () => { 'Review the change.', '', ].join('\n'); - const root = await projectRoot({ 'skills/review/SKILL.md': markdown }); - const document = await parseSkill(join(root, 'skills', 'review'), root); + const root = await projectRoot({ 'src/skills/review/SKILL.md': markdown }); + const document = await parseSkill(join(root, 'src', 'skills', 'review'), root); const ir = parseSkillIr(document); expect(ir.passThrough).toBe(false); expect(ir.extensions.claude).toEqual(expect.objectContaining({ context: 'fork', model: 'sonnet' })); @@ -253,8 +253,8 @@ describe('canonical Skill IR', () => { `Review ${skillTokenSpellings.arguments} in ${skillTokenSpellings.projectRoot}.`, '', ].join('\n'); - const root = await projectRoot({ 'skills/review/SKILL.md': markdown }); - const ir = parseSkillIr(await parseSkill(join(root, 'skills', 'review'), root)); + const root = await projectRoot({ 'src/skills/review/SKILL.md': markdown }); + const ir = parseSkillIr(await parseSkill(join(root, 'src', 'skills', 'review'), root)); expect(ir.passThrough).toBe(false); expect(ir.placeholders.map((placeholder) => placeholder.token)).toEqual(['arguments', 'projectRoot']); @@ -284,8 +284,8 @@ describe('canonical Skill IR', () => { '# Review', '', ].join('\n'); - const root = await projectRoot({ 'skills/review/SKILL.md': markdown }); - const ir = parseSkillIr(await parseSkill(join(root, 'skills', 'review'), root)); + const root = await projectRoot({ 'src/skills/review/SKILL.md': markdown }); + const ir = parseSkillIr(await parseSkill(join(root, 'src', 'skills', 'review'), root)); expect(ir.diagnostics).toEqual([expect.objectContaining({ code: 'AB3006', message: expect.stringContaining('invented-host-field'), @@ -324,8 +324,8 @@ describe('canonical Skill IR', () => { '# Review', '', ].join('\n'); - const root = await projectRoot({ 'skills/review/SKILL.md': markdown }); - const ir = parseSkillIr(await parseSkill(join(root, 'skills', 'review'), root)); + const root = await projectRoot({ 'src/skills/review/SKILL.md': markdown }); + const ir = parseSkillIr(await parseSkill(join(root, 'src', 'skills', 'review'), root)); expect(ir.diagnostics).toEqual([ 'targets.claude.display_nmae', @@ -351,7 +351,7 @@ describe('canonical Skill IR', () => { it('surfaces the shared-vs-per-host skills tree as an inspect-visible evidence decision', async () => { const root = await projectRoot({ - 'skills/review/SKILL.md': [ + 'src/skills/review/SKILL.md': [ '---', 'name: review', 'description: Review a change.', @@ -362,7 +362,7 @@ describe('canonical Skill IR', () => { '', ].join('\n'), }); - const ir = parseSkillIr(await parseSkill(join(root, 'skills', 'review'), root)); + const ir = parseSkillIr(await parseSkill(join(root, 'src', 'skills', 'review'), root)); const inspection = inspectSkillProjection(ir, ['claude', 'codex', 'cursor']); expect(inspection.authoredMarkdown).toContain('model: sonnet'); expect(inspection.skillTreeLayout.decision).toBe('per-host-required'); @@ -393,7 +393,7 @@ describe('static lowering through the rendered-skill path', () => { it('compiles SKILL.tsx at build time and projects per-host Markdown without a Flight client', async () => { const root = await projectRoot({ - 'skills/review/SKILL.ts': [ + 'src/skills/review/SKILL.ts': [ `const argumentsToken = ${JSON.stringify(skillTokenSpellings.arguments)};`, `const projectRootToken = ${JSON.stringify(skillTokenSpellings.projectRoot)};`, "export const frontmatter = { description: 'Review a change and report actionable findings.', name: 'review' };", @@ -408,7 +408,7 @@ describe('static lowering through the rendered-skill path', () => { '', ].join('\n'), }); - const document = await parseSkill(join(root, 'skills', 'review'), root); + const document = await parseSkill(join(root, 'src', 'skills', 'review'), root); expect(document.diagnostics).toEqual([]); expect(document.rendered).toBe(true); const ir = parseSkillIr(document); @@ -422,7 +422,7 @@ describe('static lowering through the rendered-skill path', () => { it('lets the artifact planner own destinations and keeps portable skills as copy pass-through', async () => { const root = await projectRoot({ 'agent-bundle.config.ts': '', - 'skills/review/SKILL.md': portableMarkdown, + 'src/skills/review/SKILL.md': portableMarkdown, }); const loaded = loadedProject(pluginConfig(['claude', 'codex', 'cursor']), root); const discovered = await discoverProject(root, loaded.config); diff --git a/packages/agent-bundle/tests/support/eval-project.ts b/packages/agent-bundle/tests/support/eval-project.ts index fc5c5e873..f465aae07 100644 --- a/packages/agent-bundle/tests/support/eval-project.ts +++ b/packages/agent-bundle/tests/support/eval-project.ts @@ -118,7 +118,7 @@ export const seedEvalProject = async ( : []), ...(options.marketplace === true ? [' marketplace: true,'] : []), " plugin: { name: 'review', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", ` targets: ${JSON.stringify(options.targets ?? ['portable'])},`, '});', '', diff --git a/packages/create-agent-bundle/README.md b/packages/create-agent-bundle/README.md index 8ef34e6ec..1aadbc763 100644 --- a/packages/create-agent-bundle/README.md +++ b/packages/create-agent-bundle/README.md @@ -38,7 +38,7 @@ scripted and asks nothing — the remaining values fall back to their defaults. | Template | What you get | | --- | --- | -| `minimal` | A skills-only plugin: one Skill directory and nothing else. | +| `minimal` | A skills-only plugin: one `src/skills//SKILL.md` directory and nothing else. | | `mcp-server` | A stdio MCP server from one `src/mcp//tools/.tsx` route module plus one artifact script, with the framework test harness wired up. | | `cli-tool` | An installable CLI through the `src/cli.ts` bin convention plus a `src/index.ts` library export with declarations. | diff --git a/packages/create-agent-bundle/templates/minimal/README.md b/packages/create-agent-bundle/templates/minimal/README.md index 0239f8cf9..9bbd3dff8 100644 --- a/packages/create-agent-bundle/templates/minimal/README.md +++ b/packages/create-agent-bundle/templates/minimal/README.md @@ -15,8 +15,8 @@ npm run check # validate + build + typecheck + test ## Layout - `agent-bundle.config.ts` — the one typed config. -- `skills/getting-started/` — a Skill: `SKILL.md` frontmatter plus optional - `references/` and `assets/`. Every `skills//SKILL.md` directory is +- `src/skills/getting-started/` — a Skill: `SKILL.md` frontmatter plus optional + `references/` and `assets/`. Every `src/skills//SKILL.md` directory is discovered automatically; add a folder and it ships. - `tests/` — run with `npm run test`. diff --git a/packages/create-agent-bundle/templates/minimal/agent-bundle.config.ts b/packages/create-agent-bundle/templates/minimal/agent-bundle.config.ts index ac0171b6c..846b3db20 100644 --- a/packages/create-agent-bundle/templates/minimal/agent-bundle.config.ts +++ b/packages/create-agent-bundle/templates/minimal/agent-bundle.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'agent-bundle'; export default defineConfig({ - // No `skills` field needed: every `skills//SKILL.md` directory is + // No `skills` field needed: every `src/skills//SKILL.md` directory is // discovered by convention. Declare `skills:` only to override the layout. plugin: { description: 'A skills-only agent plugin scaffolded from the minimal template.', diff --git a/packages/create-agent-bundle/templates/minimal/skills/getting-started/SKILL.md b/packages/create-agent-bundle/templates/minimal/src/skills/getting-started/SKILL.md similarity index 85% rename from packages/create-agent-bundle/templates/minimal/skills/getting-started/SKILL.md rename to packages/create-agent-bundle/templates/minimal/src/skills/getting-started/SKILL.md index 94729dac3..a4ff516ba 100644 --- a/packages/create-agent-bundle/templates/minimal/skills/getting-started/SKILL.md +++ b/packages/create-agent-bundle/templates/minimal/src/skills/getting-started/SKILL.md @@ -17,11 +17,12 @@ agent-bundle project: one config, one Skill, and a delivery gate. ## How to add a Skill -1. Create `skills//SKILL.md` with `name` and `description` +1. Create `src/skills//SKILL.md` with `name` and `description` frontmatter. The `name` must match the directory name. 2. Add supporting material under `references/` (read-only context) and `assets/` (files the agent fills in or copies). -3. List the new directory in the `skills` array of `agent-bundle.config.ts`. +3. Leave `agent-bundle.config.ts` unchanged unless overriding the conventional + Skill layout. 4. Run the project's `check` script: it validates the config, builds every host artifact, and runs the tests. diff --git a/packages/create-agent-bundle/templates/minimal/tests/skill.test.ts b/packages/create-agent-bundle/templates/minimal/tests/skill.test.ts index 2caedaa5a..0e2dc2301 100644 --- a/packages/create-agent-bundle/templates/minimal/tests/skill.test.ts +++ b/packages/create-agent-bundle/templates/minimal/tests/skill.test.ts @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'; import { expect, it } from '@rstest/core'; -const skillPath = new URL('../skills/getting-started/SKILL.md', import.meta.url); +const skillPath = new URL('../src/skills/getting-started/SKILL.md', import.meta.url); it('keeps the getting-started Skill frontmatter aligned with its directory', async () => { const contents = await readFile(skillPath, 'utf8'); diff --git a/packages/create-agent-bundle/tests/scaffold.test.ts b/packages/create-agent-bundle/tests/scaffold.test.ts index 2c3b447ec..d5c61f8c4 100644 --- a/packages/create-agent-bundle/tests/scaffold.test.ts +++ b/packages/create-agent-bundle/tests/scaffold.test.ts @@ -49,7 +49,7 @@ describe('scaffold', () => { 'README.md', 'agent-bundle.config.ts', 'package.json', - 'skills/getting-started/SKILL.md', + 'src/skills/getting-started/SKILL.md', 'tests/skill.test.ts', 'tsconfig.json', ]); diff --git a/packages/workbench/tests/mcp-app-real.e2e.test.ts b/packages/workbench/tests/mcp-app-real.e2e.test.ts index ba4a54854..b17714745 100644 --- a/packages/workbench/tests/mcp-app-real.e2e.test.ts +++ b/packages/workbench/tests/mcp-app-real.e2e.test.ts @@ -103,7 +103,7 @@ const writeRealAppProject = async (root: string): Promise => { 'export default defineConfig({', " mcp: { servers: { fixture: { entry: './src/server.ts' } } },", " plugin: { name: 'real-app-e2e-fixture', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['portable'],", '});', '', @@ -154,7 +154,7 @@ const writeBundledAppProject = async (root: string): Promise => { " entry: './src/server.ts',", ' } } },', " plugin: { name: 'bundled-app-e2e-fixture', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['portable'],", '});', '', diff --git a/packages/workbench/tests/overview.e2e.test.ts b/packages/workbench/tests/overview.e2e.test.ts index eab06eb58..5dbe1265d 100644 --- a/packages/workbench/tests/overview.e2e.test.ts +++ b/packages/workbench/tests/overview.e2e.test.ts @@ -125,7 +125,7 @@ const writeMcpPlaygroundProject = async (root: string): Promise => { " mcp: { servers: { fixture: { entry: './src/server.ts', env: { NO_COLOR: '1', SECRET_TOKEN: 'fixture-secret' } } } },", " portable: { fixtureMarker: 'artifact-extension-initial' },", " plugin: { name: 'workbench-mcp-fixture', version: '1.0.0' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['portable'],", '});', '', @@ -1505,7 +1505,7 @@ e2e('lists an immutable build Skill tree even after the current source Skill is const project = await createProjectFixture(); const { server } = await startFrozenEpochServer(project.root); try { - const renamed = join(project.root, 'skills', 'revised'); + const renamed = join(project.root, 'src', 'skills', 'revised'); await rename(project.skillDir, renamed); await writeFile(join(renamed, 'SKILL.md'), project.skillMarkdown.replace('name: review', 'name: revised')); diff --git a/packages/workbench/tests/packed-release.e2e.test.ts b/packages/workbench/tests/packed-release.e2e.test.ts index 36a87c163..3d6bd2eec 100644 --- a/packages/workbench/tests/packed-release.e2e.test.ts +++ b/packages/workbench/tests/packed-release.e2e.test.ts @@ -109,7 +109,7 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * }); await expect(access(join(project, 'node_modules'))).rejects.toMatchObject({ code: 'ENOENT' }); const configSource = join(project, 'agent-bundle.config.ts'); - const skillSource = join(project, 'skills', 'review', 'SKILL.md'); + const skillSource = join(project, 'src', 'skills', 'review', 'SKILL.md'); const [originalConfig, originalSkill] = await Promise.all([ readFile(configSource, 'utf8'), readFile(skillSource, 'utf8'), diff --git a/packages/workbench/tests/playground-real.e2e.test.ts b/packages/workbench/tests/playground-real.e2e.test.ts index cbf6af609..09e5641f5 100644 --- a/packages/workbench/tests/playground-real.e2e.test.ts +++ b/packages/workbench/tests/playground-real.e2e.test.ts @@ -132,7 +132,7 @@ const writePlaygroundProject = async (root: string): Promise => { " mcp: { servers: { fixture: { entry: './src/server.ts' } } },", " plugin: { name: 'playground-real-fixture', version: '1.0.0' },", " scripts: { large: './src/large-output.ts', review: './src/review.ts' },", - " skills: ['skills/review'],", + " skills: ['src/skills/review'],", " targets: ['claude'],", '});', '', diff --git a/packages/workbench/tests/project-client.test.ts b/packages/workbench/tests/project-client.test.ts index ce043cc19..e01f28af7 100644 --- a/packages/workbench/tests/project-client.test.ts +++ b/packages/workbench/tests/project-client.test.ts @@ -986,11 +986,11 @@ it('bootstraps a same-session token before posting an explicit rebuild through t }, }); - await expect(client.rebuild(['skills/review/SKILL.md'])).resolves.toMatchObject({ artifact: { state: 'stale' } }); + await expect(client.rebuild(['src/skills/review/SKILL.md'])).resolves.toMatchObject({ artifact: { state: 'stale' } }); expect(requests).toHaveLength(2); expect(requests[0]?.input).toBe('/api/project/session'); - expect(requests[1]).toMatchObject({ body: '{"paths":["skills/review/SKILL.md"]}', input: '/api/project/rebuild' }); + expect(requests[1]).toMatchObject({ body: '{"paths":["src/skills/review/SKILL.md"]}', input: '/api/project/rebuild' }); expect(new Headers(requests[1]?.headers).get('x-agent-bundle-session')).toBe('token-1'); }); diff --git a/packages/workbench/tests/runtime-client.test.ts b/packages/workbench/tests/runtime-client.test.ts index 2530a7f9b..503f3302f 100644 --- a/packages/workbench/tests/runtime-client.test.ts +++ b/packages/workbench/tests/runtime-client.test.ts @@ -265,17 +265,17 @@ it('shares one injected foreground bootstrap across MCP, Runtime, and Project cl const runtime = new RuntimeClient(foreground); const project = new ProjectClient({ foreground }); - await Promise.all([mcp.session('session-a'), runtime.bootstrap(), project.rebuild(['skills/review/SKILL.md'])]); + await Promise.all([mcp.session('session-a'), runtime.bootstrap(), project.rebuild(['src/skills/review/SKILL.md'])]); expect(requests.filter((request) => request.url === '/api/project/session')).toHaveLength(1); mcp.forgetAuthentication(); mcp.forgetAuthentication(); - await project.rebuild(['skills/review/SKILL.md']); + await project.rebuild(['src/skills/review/SKILL.md']); expect(requests.filter((request) => request.url === '/api/project/session')).toHaveLength(2); project.close(); project.close(); - await new ProjectClient({ foreground }).rebuild(['skills/review/SKILL.md']); + await new ProjectClient({ foreground }).rebuild(['src/skills/review/SKILL.md']); expect(requests.filter((request) => request.url === '/api/project/session')).toHaveLength(2); }); @@ -293,7 +293,7 @@ it('fences an in-flight Project rebuild when root shutdown invalidates foregroun }); const client = new ProjectClient({ foreground }); - const rebuilding = client.rebuild(['skills/review/SKILL.md']); + const rebuilding = client.rebuild(['src/skills/review/SKILL.md']); client.close(); foreground.forgetAuthentication(); session.resolve(json(foregroundSession)); diff --git a/packages/workbench/tests/skill-client.test.ts b/packages/workbench/tests/skill-client.test.ts index 7bb3c99a8..1c5af3e36 100644 --- a/packages/workbench/tests/skill-client.test.ts +++ b/packages/workbench/tests/skill-client.test.ts @@ -17,7 +17,7 @@ const sourceDocument = Object.freeze({ message: 'A Skill warning.', recovery: 'Fix the Skill.', severity: 'warning' as const, - sourcePath: 'skills/review/SKILL.md', + sourcePath: 'src/skills/review/SKILL.md', target: 'portable', })]), frontmatter: Object.freeze({