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..d43aaf9d --- /dev/null +++ b/packages/agents/content/guidance/rulebooks/codeassembly-content-specification.md @@ -0,0 +1,84 @@ +--- +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 + +Every rule below belongs to one of three classes, marked where it appears. + +**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 + +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`, 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-`. + +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. _(Enforced by `action-item-reinforcement.test.ts` and `spec-inlining.test.ts`.)_ 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 diff --git a/packages/agents/src/lib/__tests__/rulebook-schema.test.ts b/packages/agents/src/lib/__tests__/rulebook-schema.test.ts index 98fd946c..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`; @@ -38,6 +41,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_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_MESSAGE); + }); + 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..9fb92874 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,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([z.string(), z.array(z.string())]) + // 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