From 802f07108501a237fb24731fc5d1df6600a15cf8 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Sun, 26 Jul 2026 23:55:47 -0700 Subject: [PATCH 1/4] agents|refactor: Rename the authoring rulebook and mark enforced rules Renames the `consult-authoring-guidance` skill to `consult-codeassembly-content-specification`. The former name read both as guidance about authoring and as the authoring of guidance; the new one names the declaration contract the rulebook specifies. The rulebook now states which of its rules are validated when content is loaded and which are convention, so a reader can tell what fails the build from what is house style. Its naming section records when a `codeassembly-` prefix applies: an artifact governing this repository's own mechanics takes it, and content that would still apply in another project does not. --- .agents/PROJECT.md | 2 +- .agents/codeassembly.yaml | 2 +- packages/agents/README.md | 2 +- packages/agents/content/_partials/README.md | 2 +- .../guidance/rulebooks/authoring-guidance.md | 68 ---------------- .../codeassembly-content-specification.md | 78 +++++++++++++++++++ .../action-item-reinforcement.test.ts | 4 +- 7 files changed, 84 insertions(+), 74 deletions(-) delete mode 100644 packages/agents/content/guidance/rulebooks/authoring-guidance.md create mode 100644 packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md diff --git a/.agents/PROJECT.md b/.agents/PROJECT.md index 9f7057c2..fb18c815 100644 --- a/.agents/PROJECT.md +++ b/.agents/PROJECT.md @@ -117,7 +117,7 @@ content/ #### Content authoring -When authoring a skill, subagent, rulebook, or collection, consult `packages/agents/content/guidance/rulebooks/authoring-guidance.md` (the `consult-authoring-guidance` skill) for `dependencies:`/`members:`, frontmatter, and naming conventions. +When authoring a skill, subagent, rulebook, or collection, consult `packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md` (the `consult-codeassembly-content-specification` skill) for `dependencies:`/`members:`, frontmatter, and naming conventions. The `packages/agents/content/` tree supports **partials** — reusable Markdown fragments inlined at install time. The expander produces byte-identical installed output, so partials are the correct DRY mechanism even when "verbatim execution context" is a requirement. See `packages/agents/content/_partials/README.md` for the canonical reference. diff --git a/.agents/codeassembly.yaml b/.agents/codeassembly.yaml index eacc9f8a..bf1f3bb7 100644 --- a/.agents/codeassembly.yaml +++ b/.agents/codeassembly.yaml @@ -1,6 +1,6 @@ rulebooks: use: - - authoring-guidance + - codeassembly-content-specification collections: use: - recommended diff --git a/packages/agents/README.md b/packages/agents/README.md index ec5b493a..7aea3e79 100644 --- a/packages/agents/README.md +++ b/packages/agents/README.md @@ -127,7 +127,7 @@ Two things to know about Rovo: A project opts into shared artifacts through `.agents/codeassembly.yaml`. Run `codeassembly-agents init` to scaffold one, declare the artifacts you want, then run `codeassembly-agents sync` to materialize them. The same declaration format resolves in two independent domains — the repo (via `sync`) and the user-global home (via `sync --global`). For the home domain, `codeassembly-agents init --global` scaffolds `~/.agents/codeassembly.yaml`, seeded with the `all` collection. See [Scopes](#scopes). -Authoring conventions for the artifacts you declare — frontmatter fields, the `dependencies:` and `members:` blocks, and naming — live in the `authoring-guidance` rulebook (`content/guidance/rulebooks/authoring-guidance.md`). This section documents the declaration mechanism itself. +Authoring conventions for the artifacts you declare — frontmatter fields, the `dependencies:` and `members:` blocks, and naming — live in the `codeassembly-content-specification` rulebook (`content/guidance/rulebooks/codeassembly-content-specification.md`). This section documents the declaration mechanism itself. ### Format diff --git a/packages/agents/content/_partials/README.md b/packages/agents/content/_partials/README.md index 43f62c40..698b9726 100644 --- a/packages/agents/content/_partials/README.md +++ b/packages/agents/content/_partials/README.md @@ -21,7 +21,7 @@ Inline a spec **once per skill, as a section**, and point every use site at it w ### Skill-local pointers are load-bearing -Several skill bodies — `collaborate`, `design-and-plan`, and `refine-plan` among them — carry a pointer to the option-format rules at their question-asking steps, duplicating the universal rule in `AGENTS.md`. That duplication is intentional, and a DRY-driven refactor must not strip it. The rule and its rationale live in the authoring rulebook, under "Skill-local reinforcement". +Several skill bodies — `collaborate`, `design-and-plan`, and `refine-plan` among them — carry a pointer to the option-format rules at their question-asking steps, duplicating the universal rule in `AGENTS.md`. That duplication is intentional, and a DRY-driven refactor must not strip it. The rule and its rationale live in the `codeassembly-content-specification` rulebook, under "Skill-local reinforcement". ## Directive grammar diff --git a/packages/agents/content/guidance/rulebooks/authoring-guidance.md b/packages/agents/content/guidance/rulebooks/authoring-guidance.md deleted file mode 100644 index f597194f..00000000 --- a/packages/agents/content/guidance/rulebooks/authoring-guidance.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -slug: authoring-guidance -description: Conventions for authoring CodeAssembly skills, subagents, rulebooks, and collections. -delivery: skill -version: 3 ---- - -# Authoring guidance - -Conventions for authoring CodeAssembly artifacts — skills, subagents, rulebooks, and collections. (Here "artifact" means an authored library item, not a generated output like a review or devlog.) - -## Declaring dependencies - -When a rulebook, skill, or subagent relies on another — a skill that invokes another skill, a subagent that calls a skill it does not inject — declare the edge in its frontmatter `dependencies:` block, grouped by type: - -```yaml -dependencies: - rulebooks: - - shell-conventions - skills: - - capture-event -``` - -`sync` resolves these edges transitively, so declaring one artifact pulls in its whole closure. Prefer a declared dependency over a prose note that another artifact "must be present." - -## Invocation tokens - -When a skill or subagent invocation appears inline in a skill's or subagent's body, write it as a token rather than a hardcoded harness-specific form: - -- `{skill:}` renders to the harness skill sigil plus the slug — `/` on Claude, `!` on Rovo. -- `{subagent:}` renders to the harness subagent sigil plus the slug. That sigil is empty on both current harnesses, so it renders to the bare slug, which is how a subagent is dispatched on each. - -Slugs are kebab-case and letter-led (`[a-z][a-z0-9-]*`). The sigils are a typed property of each harness in `HarnessConfig`, so a new harness must declare its own rendering or the build fails. - -A token is also a dependency edge: `sync` extracts the tokens from a skill's or subagent's include-expanded body and pulls each target into the deploy closure. An inline invocation is therefore expressed once, as the token — it needs no duplicate `dependencies:` entry, and a token naming a non-existent artifact fails the run just as a missing `dependencies:` edge does. Because extraction runs on the include-expanded body, a token inside a shared `_partials` file becomes an edge for every skill that includes it. - -Tokens are honored only in skills and subagents — the types whose bodies pass through the render pass. Rulebooks (embedded without that pass) and collections keep `dependencies:` / `members:`. Reserve a `dependencies:` entry for a non-inline edge; use a token for any invocation that appears in the body. - -## Collections - -A collection's only payload is a `members:` block — the constituents it pulls into the deployed closure. List them per type (the same shape `dependencies:` uses), or use the computed token `'@library'` for every rulebook, skill, and subagent in the content root the collection belongs to — the built-in library, or the owning source for a source collection: - -```yaml -members: - skills: - - capture-feedback - subagents: - - canary -``` - -`members:` is collections-only; rulebooks, skills, and subagents use `dependencies:` instead. Declaring `dependencies:` on a collection, or `members:` on any other type, is an error. The resolver follows both keys identically — the split is semantic: a collection contains members, an artifact depends on prerequisites. - -## Frontmatter fields - -- **Rulebooks:** `slug`, `description`, `delivery` (`ambient`, `skill`, or both), optional `skill-name`, optional `version`. -- **Skills:** `name`, `description`, optional `user-invocable` (defaults to `true`), optional `harnesses` (a harness id or list restricting deployment to those harnesses; absent deploys to all). -- **Subagents:** `name`, `description`, `tools`, optional `maxTurns`, optional `skills` (skills injected into the subagent's context; `sync` pulls them into the deploy closure automatically). -- **Collections:** `name`, `description`, and a `members:` block — the collection's only payload. - -## Naming - -A `delivery: skill` rulebook ships as `consult-`. Skill names are verb-led. Order list members and frontmatter lists alphabetically unless there is a reason to group otherwise. - -## Skill-local reinforcement - -Behavioural rules that govern an agent's output — the recommendation gradient, the action-items block — are stated once in `AGENTS.md` and the shared `_data` specs, then restated at the step that produces the output: as a pointer in the skill body, or as a rendered example inlined from `_partials/`. An agent follows a rule more reliably when it sits beside the action it governs than when it was read once at session start, and it imitates a nearby concrete example more reliably still than it follows a directive. - -Treat that restatement as load-bearing redundancy, not duplication. A DRY-driven refactor that strips the skill-local pointers and leaves only the global rule removes the mechanism by which the global rule takes effect. diff --git a/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md b/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md new file mode 100644 index 00000000..c77bb173 --- /dev/null +++ b/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md @@ -0,0 +1,78 @@ +--- +slug: codeassembly-content-specification +description: The declaration contract for CodeAssembly skills, subagents, rulebooks, and collections -- frontmatter fields, dependency blocks, and invocation tokens. +delivery: skill +version: 4 +--- + +# CodeAssembly content specification + +The declaration contract for CodeAssembly artifacts -- skills, subagents, rulebooks, and collections. (Here "artifact" means an authored library item, not a generated output like a review or devlog.) + +## Enforcement + +Most of what follows is validated at parse time. A malformed `slug` or `skill-name`, a `delivery` value outside the permitted set, an unknown artifact-type key, a non-list value under one, a `members:` block on anything but a collection, a token naming an artifact that does not exist, or a harness that has not declared its sigil each fail the run with an error naming the source file. + +The remainder is convention, marked as such where it appears. Nothing checks it. + +## Declaring dependencies + +When a rulebook, skill, or subagent relies on another -- a skill that invokes another skill, a subagent that calls a skill it does not inject -- declare the edge in its frontmatter `dependencies:` block, grouped by type: + +```yaml +dependencies: + rulebooks: + - shell-conventions + skills: + - capture-event +``` + +`sync` resolves these edges transitively, so declaring one artifact pulls in its whole closure. Prefer a declared dependency over a prose note that another artifact "must be present." _(Convention; not enforced.)_ + +## Invocation tokens + +When a skill or subagent invocation appears inline in a skill's or subagent's body, write it as a token rather than a hardcoded harness-specific form: + +- `{skill:}` renders to the harness skill sigil plus the slug -- `/` on Claude, `!` on Rovo. +- `{subagent:}` renders to the harness subagent sigil plus the slug. That sigil is empty on both current harnesses, so it renders to the bare slug, which is how a subagent is dispatched on each. + +Slugs are kebab-case and letter-led (`[a-z][a-z0-9-]*`). The sigils are a typed property of each harness in `HarnessConfig`, so a new harness must declare its own rendering or the build fails. + +A token is also a dependency edge: `sync` extracts the tokens from a skill's or subagent's include-expanded body and pulls each target into the deploy closure. An inline invocation is therefore expressed once, as the token -- it needs no duplicate `dependencies:` entry, and a token naming a non-existent artifact fails the run just as a missing `dependencies:` edge does. Because extraction runs on the include-expanded body, a token inside a shared `_partials` file becomes an edge for every skill that includes it. + +Tokens are honored only in skills and subagents -- the types whose bodies pass through the render pass. Rulebooks (embedded without that pass) and collections keep `dependencies:` / `members:`. Reserve a `dependencies:` entry for a non-inline edge; use a token for any invocation that appears in the body. _(Convention; not enforced.)_ + +## Collections + +A collection's only payload is a `members:` block -- the constituents it pulls into the deployed closure. List them per type (the same shape `dependencies:` uses), or use the computed token `'@library'` for every rulebook, skill, and subagent in the content root the collection belongs to -- the built-in library, or the owning source for a source collection: + +```yaml +members: + skills: + - capture-feedback + subagents: + - canary +``` + +`members:` is collections-only; rulebooks, skills, and subagents use `dependencies:` instead. Declaring `dependencies:` on a collection, or `members:` on any other type, is an error. The resolver follows both keys identically -- the split is semantic: a collection contains members, an artifact depends on prerequisites. + +## Frontmatter fields + +- **Rulebooks:** `slug`, `description`, `delivery` (`ambient`, `skill`, or both), optional `skill-name`, optional `version`. +- **Skills:** `name`, `description`, optional `user-invocable` (defaults to `true`), optional `harnesses` (a harness id or list restricting deployment to those harnesses; absent deploys to all). +- **Subagents:** `name`, `description`, `tools`, optional `maxTurns`, optional `skills` (skills injected into the subagent's context; `sync` pulls them into the deploy closure automatically). +- **Collections:** `name`, `description`, and a `members:` block -- the collection's only payload. + +## Naming + +A `delivery: skill` rulebook ships as `consult-`. + +Skill names are verb-led. Order list members and frontmatter lists alphabetically unless there is a reason to group otherwise. _(Conventions; not enforced.)_ + +A `codeassembly-` prefix marks an artifact governing this repository's own mechanics, as this specification does. Its absence marks content that would still apply in another project. Prefix a new artifact only when it would be meaningless outside this repository. _(Convention; not enforced.)_ + +## Skill-local reinforcement + +Behavioural rules that govern an agent's output -- the recommendation gradient, the action-items block -- are stated once in `AGENTS.md` and the shared `_data` specs, then restated at the step that produces the output: as a pointer in the skill body, or as a rendered example inlined from `_partials/`. An agent follows a rule more reliably when it sits beside the action it governs than when it was read once at session start, and it imitates a nearby concrete example more reliably still than it follows a directive. + +Treat that restatement as load-bearing redundancy, not duplication. A DRY-driven refactor that strips the skill-local pointers and leaves only the global rule removes the mechanism by which the global rule takes effect. diff --git a/packages/agents/src/__tests__/action-item-reinforcement.test.ts b/packages/agents/src/__tests__/action-item-reinforcement.test.ts index e8bb8a50..e3226158 100644 --- a/packages/agents/src/__tests__/action-item-reinforcement.test.ts +++ b/packages/agents/src/__tests__/action-item-reinforcement.test.ts @@ -4,8 +4,8 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; // The duplication this list guards is deliberate: the convention is also stated globally, and a DRY-driven refactor -// that strips the skill-local copies removes the mechanism by which the global rule takes effect. See the authoring -// rulebook, § "Skill-local reinforcement". +// that strips the skill-local copies removes the mechanism by which the global rule takes effect. See the +// `codeassembly-content-specification` rulebook, § "Skill-local reinforcement". // // Membership is skills that routinely close a turn awaiting a user response. Skills whose only ask is an exception // path (`create-pr`'s unknown-platform fallback, `capture-feedback`'s ambiguity confirm) are absent by design; a From 6b370440ced803bad8c29defe291e0f546d3ab2f Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 01:20:14 -0700 Subject: [PATCH 2/4] agents|fix: Fail sync on an unrecognized rulebook delivery mode A rulebook whose `delivery` names anything but `ambient` or `skill` now fails the sync with an error naming the file, rather than parsing cleanly and leaving the rulebook delivered nowhere. The CodeAssembly content specification sorts every rule it states into three classes: validated on parse, enforced by test, or convention. Its enforcement claims now match the toolchain, covering the two sections that carried no classification and correcting the cases that fail at build time rather than at run time. --- .../codeassembly-content-specification.md | 14 ++++++++++---- .../src/lib/__tests__/rulebook-schema.test.ts | 8 ++++++++ packages/agents/src/lib/rulebook-schema.ts | 8 +++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md b/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md index c77bb173..a154c41f 100644 --- a/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md +++ b/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md @@ -11,9 +11,13 @@ The declaration contract for CodeAssembly artifacts -- skills, subagents, rulebo ## Enforcement -Most of what follows is validated at parse time. A malformed `slug` or `skill-name`, a `delivery` value outside the permitted set, an unknown artifact-type key, a non-list value under one, a `members:` block on anything but a collection, a token naming an artifact that does not exist, or a harness that has not declared its sigil each fail the run with an error naming the source file. +Every rule below belongs to one of three classes, marked where it appears. -The remainder is convention, marked as such where it appears. Nothing checks it. +**Validated on parse.** A malformed `slug` or `skill-name`, a `delivery` value outside `ambient`/`skill`, an unknown artifact-type key, a non-list value under one, and a `members:` block on anything but a collection each fail the run with an error naming the source file. Two more fail outside the parser: a token naming an artifact that does not exist fails the run with an error naming the slug and the directories searched, and a harness that declares no sigil is a type error at its `HarnessConfig` literal, so the build fails. + +**Enforced by test.** The suites in `packages/agents/src/__tests__/` read the shipped library and assert its conventions hold. A rule one of them guards names its test. + +**Convention.** The rest is marked _(Convention; not enforced.)_ Nothing checks it. ## Declaring dependencies @@ -58,11 +62,13 @@ members: ## Frontmatter fields -- **Rulebooks:** `slug`, `description`, `delivery` (`ambient`, `skill`, or both), optional `skill-name`, optional `version`. +- **Rulebooks:** `slug`, optional `description`, optional `delivery` (`ambient`, `skill`, or both; defaults to `ambient`), optional `skill-name`, optional `version`. - **Skills:** `name`, `description`, optional `user-invocable` (defaults to `true`), optional `harnesses` (a harness id or list restricting deployment to those harnesses; absent deploys to all). - **Subagents:** `name`, `description`, `tools`, optional `maxTurns`, optional `skills` (skills injected into the subagent's context; `sync` pulls them into the deploy closure automatically). - **Collections:** `name`, `description`, and a `members:` block -- the collection's only payload. +Only the rulebook row is validated on parse; a `members:` block is validated wherever it appears. The other rows are read leniently: a field a deploy pass consumes takes effect, and an absent one falls back to a default rather than failing, so a skill with no `description` reaches Rovo's prompt index with an empty one. _(Convention; not enforced.)_ + ## Naming A `delivery: skill` rulebook ships as `consult-`. @@ -75,4 +81,4 @@ A `codeassembly-` prefix marks an artifact governing this repository's own mecha Behavioural rules that govern an agent's output -- the recommendation gradient, the action-items block -- are stated once in `AGENTS.md` and the shared `_data` specs, then restated at the step that produces the output: as a pointer in the skill body, or as a rendered example inlined from `_partials/`. An agent follows a rule more reliably when it sits beside the action it governs than when it was read once at session start, and it imitates a nearby concrete example more reliably still than it follows a directive. -Treat that restatement as load-bearing redundancy, not duplication. A DRY-driven refactor that strips the skill-local pointers and leaves only the global rule removes the mechanism by which the global rule takes effect. +Treat that restatement as load-bearing redundancy, not duplication. A DRY-driven refactor that strips the skill-local pointers and leaves only the global rule removes the mechanism by which the global rule takes effect. _(Enforced by `action-item-reinforcement.test.ts`.)_ diff --git a/packages/agents/src/lib/__tests__/rulebook-schema.test.ts b/packages/agents/src/lib/__tests__/rulebook-schema.test.ts index 98fd946c..21110cd5 100644 --- a/packages/agents/src/lib/__tests__/rulebook-schema.test.ts +++ b/packages/agents/src/lib/__tests__/rulebook-schema.test.ts @@ -38,6 +38,14 @@ describe(parseRulebookFile, () => { expect(rulebook.delivery).toEqual(['ambient']); }); + it('throws when delivery names a mode the resolver does not act on', () => { + expect(() => parseRulebookFile(rulebookFile('slug: x\ndelivery: skil'))).toThrow(/delivery/); + }); + + it('throws when a delivery list carries a mode the resolver does not act on', () => { + expect(() => parseRulebookFile(rulebookFile('slug: x\ndelivery: [ambient, skil]'))).toThrow(/delivery/); + }); + it('coerces a numeric version to a string', () => { const { rulebook } = parseRulebookFile(rulebookFile('slug: x\nversion: 3')); diff --git a/packages/agents/src/lib/rulebook-schema.ts b/packages/agents/src/lib/rulebook-schema.ts index 392fc2a3..a3ac4c08 100644 --- a/packages/agents/src/lib/rulebook-schema.ts +++ b/packages/agents/src/lib/rulebook-schema.ts @@ -6,6 +6,12 @@ import { parseFrontmatter } from './frontmatter-merger.ts'; /** Lowercase kebab-case, the shape required of any token that becomes a directory or `/` command. */ const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +/** + * The delivery modes the resolver acts on: an inlined ambient block, a `consult-` skill, or both. A value + * outside the pair would satisfy neither membership test, delivering the rulebook nowhere, so the set is closed. + */ +const DeliveryModeSchema = z.enum(['ambient', 'skill']); + /** * Frontmatter schema for a rulebook source file. The operational fields drive the resolver; unknown keys * (e.g. future classification metadata) are accepted but dropped, not preserved on the parsed object. @@ -21,7 +27,7 @@ export const RulebookFrontmatterSchema = z.object({ .regex(KEBAB_CASE, 'skill-name must be lowercase kebab-case (e.g. shell-conventions-rulebook)') .optional(), delivery: z - .union([z.string(), z.array(z.string())]) + .union([DeliveryModeSchema, z.array(DeliveryModeSchema)]) .default('ambient') .transform((value) => (typeof value === 'string' ? [value] : value)), version: z From bb7222d3ab4fca89b71501270b212e21b56ee369 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 02:05:24 -0700 Subject: [PATCH 3/4] agents|fix: Name both suites guarding skill-local reinforcement A reader checking whether the recommendation-gradient rule is machine-checked now finds the suite that guards it, not only the suite covering the action-items half. --- .../guidance/rulebooks/codeassembly-content-specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md b/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md index a154c41f..d43aaf9d 100644 --- a/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md +++ b/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md @@ -81,4 +81,4 @@ A `codeassembly-` prefix marks an artifact governing this repository's own mecha Behavioural rules that govern an agent's output -- the recommendation gradient, the action-items block -- are stated once in `AGENTS.md` and the shared `_data` specs, then restated at the step that produces the output: as a pointer in the skill body, or as a rendered example inlined from `_partials/`. An agent follows a rule more reliably when it sits beside the action it governs than when it was read once at session start, and it imitates a nearby concrete example more reliably still than it follows a directive. -Treat that restatement as load-bearing redundancy, not duplication. A DRY-driven refactor that strips the skill-local pointers and leaves only the global rule removes the mechanism by which the global rule takes effect. _(Enforced by `action-item-reinforcement.test.ts`.)_ +Treat that restatement as load-bearing redundancy, not duplication. A DRY-driven refactor that strips the skill-local pointers and leaves only the global rule removes the mechanism by which the global rule takes effect. _(Enforced by `action-item-reinforcement.test.ts` and `spec-inlining.test.ts`.)_ From 47afac89139002aa11e22519ad489eb6fff0a2e7 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Mon, 27 Jul 2026 02:05:31 -0700 Subject: [PATCH 4/4] agents|fix: Name the permitted modes when delivery is rejected A rulebook declaring an unrecognized `delivery` mode now fails with a message naming the permitted values rather than a bare `Invalid input`. --- packages/agents/src/lib/__tests__/rulebook-schema.test.ts | 7 +++++-- packages/agents/src/lib/rulebook-schema.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/agents/src/lib/__tests__/rulebook-schema.test.ts b/packages/agents/src/lib/__tests__/rulebook-schema.test.ts index 21110cd5..86018b04 100644 --- a/packages/agents/src/lib/__tests__/rulebook-schema.test.ts +++ b/packages/agents/src/lib/__tests__/rulebook-schema.test.ts @@ -2,6 +2,9 @@ import { describe, expect, it } from 'vitest'; import { parseRulebookFile } from '../rulebook-schema.ts'; +/** The rejection an unrecognized delivery mode must carry: naming the permitted set, not a bare "Invalid input". */ +const DELIVERY_MESSAGE = "delivery must be 'ambient', 'skill', or a list of the two"; + /** Wraps frontmatter and a body into a rulebook source file. */ function rulebookFile(frontmatter: string, body = '# Shell conventions\n\nUse strict mode.'): string { return `---\n${frontmatter}\n---\n\n${body}\n`; @@ -39,11 +42,11 @@ describe(parseRulebookFile, () => { }); it('throws when delivery names a mode the resolver does not act on', () => { - expect(() => parseRulebookFile(rulebookFile('slug: x\ndelivery: skil'))).toThrow(/delivery/); + expect(() => parseRulebookFile(rulebookFile('slug: x\ndelivery: skil'))).toThrow(DELIVERY_MESSAGE); }); it('throws when a delivery list carries a mode the resolver does not act on', () => { - expect(() => parseRulebookFile(rulebookFile('slug: x\ndelivery: [ambient, skil]'))).toThrow(/delivery/); + expect(() => parseRulebookFile(rulebookFile('slug: x\ndelivery: [ambient, skil]'))).toThrow(DELIVERY_MESSAGE); }); it('coerces a numeric version to a string', () => { diff --git a/packages/agents/src/lib/rulebook-schema.ts b/packages/agents/src/lib/rulebook-schema.ts index a3ac4c08..9fb92874 100644 --- a/packages/agents/src/lib/rulebook-schema.ts +++ b/packages/agents/src/lib/rulebook-schema.ts @@ -27,7 +27,11 @@ export const RulebookFrontmatterSchema = z.object({ .regex(KEBAB_CASE, 'skill-name must be lowercase kebab-case (e.g. shell-conventions-rulebook)') .optional(), delivery: z - .union([DeliveryModeSchema, z.array(DeliveryModeSchema)]) + // The message rides the union, not `DeliveryModeSchema`: a union reports its own failure and discards the + // messages of the members it tried. + .union([DeliveryModeSchema, z.array(DeliveryModeSchema)], { + error: "delivery must be 'ambient', 'skill', or a list of the two", + }) .default('ambient') .transform((value) => (typeof value === 'string' ? [value] : value)), version: z