codeassembly
v0.16.0
Published
CLI that installs reusable AI agent guidance (rulebooks, skills, and subagents) into coding-harness directories
Maintainers
Readme
codeassembly
A CLI that installs reusable AI agent guidance into coding-harness directories, and the library of rulebooks, skills, and subagents that it deploys.
Release notes — v0.16.0 (2026-09-21)
🎉 Features
Declare a report id for every prose rule, detected or not (#1687)
- Adds
williamthorsen-comment-preferencesto the rulebooks thatrevise-prosesweeps and thatprose-reviserapplies. - Directs
revise-proseandprose-reviserto take each rule's id from the<!-- rule: <id> -->marker beneath its heading, rather than from a list kept by hand, and adds that marker to each rule inwilliamthorsen-writing-preferencesandwilliamthorsen-comment-preferencesthat lacked one. - Specifies that a rule without a marker, such as one in a rulebook from outside this repository, takes the kebab-case form of its heading as its id.
- Makes
revise-prose.mjs detectaccept a--rulenaming a rule that has no mechanical detector, and list such rules underrules.undetected, which the sweep summary reports to make a misspelt marker id visible.
- Adds
Sweep prose in the content guidance, partials, and collections (#1703)
- Revises prose across guidance documents.
- Bumps the version of the 12 rulebooks whose deployed text changed and of the
plain-speechunit, which re-opens every repository's recorded sweep coverage for theplain-speechunit and the comment and writing preferences.
Forbid giving an action to a subject that does not perform it (#1707)
- Extends the
plain-speechrule to forbid making data, a file, or a part of a document the subject of an action that an agent or a command performs, and to forbid a passive that omits an actor needed by the reader. - Stops the second-person rule in
williamthorsen-writing-preferencesfrom recommending a recast that makes an artifact the subject of an action that it does not perform. - Bumps the
plain-speechunit to version 6, so that arevise-prosesweep covers again the files that a sweep covered at an earlier version.
- Extends the
Sweep content guidance and partials at current writing-rule versions (#1708)
- Adds a rule to the "Version bumps" section of
codeassembly-content-specification, now at version 21: The author of arevise-proserepair keeps the rulebook's version, because a raised version stopsrevise-prosefrom counting the coverage recorded by the same sweep.
- Adds a rule to the "Version bumps" section of
Exclude verbatim extracts from the prose sweep and fix skills m to z (#1720)
- Revises prose in a range of skill files to align with writing rules.
- Adds a
vendoredskip reason torevise-prose, which excludes from the sweep any file that has a comment line containing both "Extracted verbatim from" and "Do not edit" and reports it under that reason infilesSkipped.
Declare a sweep version on each prose rule and pin each rule's section (#1727)
- Adds a version to the rule marker, written
<!-- rule: <id> <version> -->and set to1on every rule in the library, as a first step toward keyingrevise-prosecoverage per rule rather than per rulebook. - States in
codeassembly-content-specificationwhen a rule's sweep version rises: when some text that complied with the old wording could fail the new one, or when unsure, but not for a relaxation, a clarification, or a rewording.
- Adds a version to the rule marker, written
Key prose-sweep coverage and rejections per rule (#1728)
- Makes a raised sweep version reset coverage and mark rejections stale for that rule alone in the
.agents/revise-prose.yamlsweep record, and stops a change to a rulebook'sversionfrom doing so for every rule in the rulebook. - Excludes from the record any rule that declares no sweep version, and adds a "Not recorded" line naming each such rule to
revise-prose's closing summary. - Changes the record's shape, which a
revise-proseinstalled before this change reads as covering nothing, or refuses asinvalid-recordwhen the record holds a rejection.
- Makes a raised sweep version reset coverage and mark rejections stale for that rule alone in the
Apply only the unswept rules in each prose-sweep batch (#1729)
- Limits the rules that
prose-reviserapplies and reports in eachrevise-prosebatch to the versioned rules still unswept in at least one of the batch's files, plus every rule without a sweep version, so that raising one rule's sweep version no longer re-sweeps the other versioned rules.
- Limits the rules that
Allow the where that defines a symbol in the writing preferences (#1731)
- Specifies that the
whererule inwilliamthorsen-writing-preferencespermits the word only for a place or for stating what a symbol, variable, placeholder, or value in a preceding expression stands for, and that awherethat states a condition, such as "the entry whereroleiscoder", remains a violation. - Raises the
williamthorsen-writing-preferencesversion from 8 to 9 without raising thewhererule's sweep version, so its recorded coverage and rejections stay current in arevise-proserecord keyed on each rule's sweep version and no longer count in a record still keyed on the rulebook version. - Stops
revise-prosefrom writing arootslist shared by several rules as a YAML anchor and aliases, which the nextrecordrun expanded into full lists in its diff.
- Specifies that the
Add a derivation test to the comment-discipline audit (#1740)
- Adds a fourth test to the comment-discipline audit: A doc comment that lists the items below it now fails, because that list copies each item's own description and goes out of date. The test directs the author to state the one rule that the items follow, and to leave the rest to each item's own description.
Close the plain-speech settled-term exemption to an enumerated list (#1741)
- Closes the settled-term exemption in the
plain-speechwriting rule to the six terms that its sweep calibration enumerates. - Limits the calibration's mannered-prose test to constructed figures, so plain technical vocabulary such as "inlines the partial" is not a candidate for repair.
- Raises
unit-version: plain-speechfrom 6 to 7, which re-opens theplain-speechcoverage thatrevise-proserecorded for every repository swept at 6.
- Closes the settled-term exemption in the
Sweep the sweep, change, and lede helpers against the writing rules (#1742)
- Changes the lines that
select-lede-exemplarsprints, among them the warning for a work type that neither the taxonomy nor the corpus record gives a tier.
- Changes the lines that
Replace the abbreviation ban with a test for obscure abbreviations (#1754)
- Replaces the blanket abbreviation ban in
naming-conventions.mdwith a test that permits abbreviations used in ordinary speech. - Permits
fnbecausefunctionis a reserved word. - Replaces the looser test in the file's "Unit-of-measure suffixes" section with the rule that decides an unlisted unit, namely that the suffix takes the unit's conventional written form.
- Replaces the blanket abbreviation ban in
Direct the planner to read the skills that a plan's tasks invoke (#1768)
- Directs the planner, through the plan template shared by the
plananddesign-and-planskills, to read each skill that a task invokes and to record in the task's key decisions that skill's ordering relative to other skills, its default target when the task passes no argument, and the preconditions that it states. - Requires the planner to add the missing task when an ordering constraint names a skill that no task invokes.
- Directs the planner, through the plan template shared by the
Size streamlining targets by what they deploy (#1778)
- Adds
deployedBytesbesidebytesin the output ofstreamline-guidance resolve, sizing each file by what an agent loads: a document's size once its includes are expanded, and a partial's own size times the number of documents that reach it. - Restates the
streamline-guidanceskill's Before/After/Saved summary in deployed bytes, so a run reports what its cuts removed from what agents load rather than from the source.
- Adds
Keep the statement of the change in the lede when the title names it (#1787)
- Modifies the
lede-draftersubagent so that it is no longer instructed to avoid duplicating the title's content in the lede bullets.
- Modifies the
Ban jargon in an authored title (#1788)
- Adds a "No jargon" rule to
title-voice.md, the guidance for a title authored for a ticket, commit, pull request, or squash merge.
- Adds a "No jargon" rule to
Cut ticket splits finer and raise an oversized ticket before design (#1790)
- Directs a ticket split, in the
williamthorsen-ticketing-preferencesrulebook, to err toward more and smaller tickets when the count is a judgment, and gives the test that decides a piece: whether it ships and can be verified on its own. - Extends ticket evaluation with that test, so that an agent raises the split for a ticket holding two or more independently shippable pieces before design begins rather than once its tasks are decomposed.
- Directs a ticket split, in the
Record each deployment's sizes and rank them with a sizes command (#1789)
- Adds a size-recording pass at the end of every live
sync, which measures every file thatsyncandinstalldeployed and appends a snapshot to a JSONL record under~/.codeassembly/deployed-sizes/, outside every repository. - Adds a
sizescommand, which ranks the last recorded deployment's documents by size and reports three totals beneath them: the bytes that load into every session, the bytes that load when something opens a document, and the files that load into no context. - Gates that append on two conditions, a measurement differing from the previous snapshot and a deployed source commit that is an ancestor of the default branch, so that the record tracks the default branch's sizes rather than those of each branch under development.
- Adds a size-recording pass at the end of every live
Report each live sync's size change (#1793)
- Adds a size report to each live
sync, listing every document that the deployment added, removed, or resized with its change in bytes and, when it remains, its size after the deployment, then the always-loaded, on-invocation, and asset totals and a closing line naming thesizescommand. - Adds a growth warning for each document that the deployment takes from below 5 KiB to at or above it, naming the
streamline-guidanceskill only for a document whose source sits inside the current repository and outsidenode_modules.
- Adds a size report to each live
Stop merge-pr from prompting for a lede rating after every merge (#1798)
- Stops
merge-prfrom asking the author to rate the lede, the "What"-section bullets of the merged pull request; the flow now ends by reporting whether the merge happened. - Restricts
capture-lede-decisionto the author's own invocation.
- Stops
Attribute a partial's fan-out to one report line (#1800)
- Attributes an edit to a partial, the shared content that the expander inlines into every document that includes it, to one report line naming the partial, its per-document delta, and the number of deployed documents whose growth it explains, in place of the near-identical line that each of those documents used to print.
- Reports as a residual the part of a resized document's delta that its changed partials do not explain, and orders partial and document lines together by the bytes that each accounts for.
Report each document's growth since its last streamlining review (#1807)
- Adds a "Grown since last streamlined:" block to the live sync's size report, listing each guidance document that has grown since the streamlining review that last read it, ranked by growth and stating the date of that review.
- Extends the
streamline-guidanceprocess with a marking step, which appends to the deployed-size records a review marker naming every document that the run read, whether or not the run applied a cut.
Add stand to the plain-speech watchlist (#1808)
- Adds
standto the plain-speech calibration's "Words to look for" list, which givesbuildsandcreatesas the plain replacements for the figure "stands up" and names the senses that stay: "stands in for" a substitute, and a claim or a passive that stands.
- Adds
Add a resolve-scopes subcommand that maps paths to their workspaces (#1811)
- Adds a
resolve-scopessubcommand todescribe-change.mjs, which maps each--pathgiven to the scope that owns it, the basename of the innermost workspace directory containing it orrootfor a path that no workspace contains, and reports the sorted union of those scopes alongside the per-path mapping.
- Adds a
Derive the change summary's Details and What from a drafted entry list (#1812)
- Stops
summarize-changefrom restating the diff in prose under## Details, which now renders one bullet per outcome of the change, grouped into a subsection per work type. - Makes
## Whata selection of those same bullets, so the lede and## Detailsstate each outcome in one wording rather than two. - Adds bare
#scopetags to the## Detailsbullets of a branch whose outcomes do not all name the same scopes. - Extends the breaking prefix to the merge-commit body and the changelog, where a breaking outcome previously appeared as an unmarked bullet above a migration paragraph.
- Rebuilds
merge-pr's fallback merge-commit body from the same entries, for a pull request whose own body is too thin to use.
Migration: Replace
lede-drafterwithentry-drafterin every skill, collection, tool grant, and dispatch that names it, and read the subagent's return as the fenced YAML entry list under## Entriesrather than as a bullet list. A redispatch after a rejection still returns plain text, one replacement per rejected passage, so a caller that parses YAML unconditionally fails on that path.- Stops
Record change entries in the change-record block and consolidate them (#1815)
- Adds
entriesandentries_committo thechange-recordblock: the entries drafted byentry-drafter, each with itstype,scopes,breaking, andtext, and the short SHA of the commit at which they were drafted, both reported bydescribe-change.mjs resolve-mergeundersources.block, so that a tool gets the entries as data instead of parsing the rendered## Detailslist. - Makes
summarize-changeconsolidate the change summary'sscope,type, andbreakingfrom the entries drafted byentry-drafterrather than from the commit subjects, so that the scope is resolved from the paths touched and not read from the prefix that the author typed. - Moves the rendering of the
change-recordblock fromcreate-prtosummarize-change, which ends the change summary's body with it, and makescreate-prcopy that block into the pull-request body, or report that the pull request has no change record when the summary contains no block, which is the case for a summary saved before this change. - Changes which record
describe-change.mjs resolve-mergeuses, andmerge-prtherefore proposes, when thechange-recordblock's consolidated record and the commits' disagree: the block's record if its entries were drafted at the pull request's head commit, and the commits' record otherwise, as previously in every case.
- Adds
🐛 Bug fixes
Allow a result-stating so and detect only bare or repeated uses (#1701)
- Drops the previously proposed repairs and adds a quantitative requirement: at least three sentences between one clause-joining
soand the next. - Narrows the
revise-prosesodetector.
- Drops the previously proposed repairs and adds a quantitative requirement: at least three sentences between one clause-joining
Keep live rejections and retire reviewed stale ones on a re-sweep (#1705)
- Fixes that issue that
revise-prosedeleted earlier sweeps' rejections (recorded decisions not to repair a flagged phrase) in the files that it swept again. A new sweep now preserves a rejection if its phrase still appears in its file. - Fixes the issue that
revise-prosekept rejections made under an older version of the rules after a sweep under a new version covered their files. - Stops counting stale candidates toward either the rejected count or the total of
prose-reviser's "a file that is mostly rejections" ground, which reported every remaining repair in a file as questionable when the swept rules had changed version and the subagent rejected most of those candidates again.
- Fixes that issue that
Fix writing-rule violations in skill data, partials, and skills a to l (#1711)
- Fixes writing-rule violations in shared skill data, skill partials, and a range of skills.
Describe each work type and inline the test that decides between them (#1713)
- Adds a
descriptionto every type inwork-types.json, and includes incommit-conventions,create-commit,create-ticket,merge-pr, andsummarize-changea work-type test under which guidance written for any repository takes the type of the same change to source code, guidance on working in one repository and that repository's agent configuration takeai, and a change confined to code comments takesdocs. - Changes
revise-proseto commit each batch with the type and scope thatcreate-commitderives from its files, rather than withdocsfor every batch.
- Adds a
Reduce title-voice.md to the rules of an authored title (#1714)
- Fixes violations of writing rules in
title-voice.mdand reduces its size by 60%.
- Fixes violations of writing rules in
Write and remove orchestrate's run breadcrumb on every path (#1722)
- Fixes the issue that artifacts saved during an
orchestraterun with the MCP server unavailable contained norun_idin their frontmatter, although the run had one. - Fixes the issue that artifacts saved in a worktree after an
orchestraterun whose MCP connection dropped mid-run could receive that run'srun_id, because the skill did not say whether to remove.claude/tmp/active-run-dirin that case.
- Fixes the issue that artifacts saved during an
Fix stale run_id on artifacts written after an interrupted run (#1726)
- Stops
resolve-frontmatter.shfrom adding an interrupted orchestrated run'srun_idto every artifact that a skill or subagent later wrote in the same worktree, a defect caused by the.claude/tmp/active-run-dirfile thatorchestrateremoved only when a run finished. - Makes
resolve-frontmatter.shemitrun_idonly when a caller passes--override run_id=<id>, and makesorchestrateand the subagents that it dispatches pass that override for the artifacts of a run.
- Stops
Fix writing-rule violations in the content subagents, scripts, and tests (#1733)
- Fixes writing-rule violations in the subagent bodies that
codeassemblydeploys, in the shell helpers that its skills invoke, and in the content test suites and their helpers.
- Fixes writing-rule violations in the subagent bodies that
Require a real bug before a change is typed as a fix (#1770)
- Fixes the typing error that published deliberate improvements as bug fixes, by adding to
commit-conventions,create-commit,create-ticket,merge-pr, andsummarize-changea test that admitsfixonly when a consumer meets the changed behavior and that behavior differed from what its author intended. - Corrects the tier tiebreak in
commit-conventions, which read as a direction to promote a change to the higher tier: it applies only when more than one type genuinely applies, and it governs a pull request and a merge commit as well as a single commit. - Admits a repair into
internalinwork-types.json, since the test routes a repair that no consumer meets away fromfixandinternalpreviously covered only adding or extending a capability that consumers do not use directly.
- Fixes the typing error that published deliberate improvements as bug fixes, by adding to
Say that a feedback capture records evidence rather than changing behavior (#1791)
- Fixes the
williamthorsen-workflow-preferencesrulebook's claim that capturing feedback propagates a correction to every project and machine, which led agents to report a captured correction as already in force; the rulebook now states thatcapture-feedbackrecords evidence and that behavior is unchanged until a later refinement pass writes the lesson into deployed guidance.
- Fixes the
Truncate on grapheme boundaries and share one terminal-width rule (#1795)
- Stops
feedback-memories list --verbosefrom cutting a truncated description in the middle of a character, which yielded fewer emoji than the width allowed and turned a joined emoji sequence into a replacement character. - Widens
library-list's output to 120 columns when it is piped rather than written to a terminal, matchingfeedback-memoriesand wrapping each description onto fewer lines.
- Stops
Permit correcting a saved artifact and stop agents narrating the rule (#1801)
- Fixes the issue that agents refused to correct a saved plan, review, or summary that nothing had consumed yet and proposed a duplicate artifact instead; the guidance now applies a motive test, which permits correction of a record that got its own subject wrong and forbids editing one to reflect what has happened since.
- Fixes the issue that agents announced an edit that they had declined to make; the guidance now forbids mentioning the doctrine, announcing an unmade write, and offering to reconcile a record with current state.
Installation
No install needed to try it:
npx codeassembly install
npx codeassembly syncAdd it to a project when the repo ships guidance of its own, or wants sync to run from a script:
pnpm add --save-dev codeassemblyinstall deploys the built-in library into the harness directories. sync resolves .agents/codeassembly.yaml and materializes exactly what the project declares, including guidance shipped by its dependencies (see Packages).
Supported harnesses are Claude Code and Rovo Dev; --harness narrows a run to one.
Optional: For a project whose tickets live in Jira, the deployed guidance resolves them through Atlassian's acli when it is on PATH, so acli jira auth login --web is worth running once. Without it, resolution falls back to a connected Jira read tool and then to asking for the ticket content.
Commands
Run via the codeassembly CLI: codeassembly <command> [options].
| Command | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| install | Install harness guidance, scripts, and support data into harness directories |
| init | Scaffold .agents/codeassembly.yaml for the project, or --global for ~/.agents/codeassembly.yaml |
| sync | Resolve .agents/codeassembly.yaml and materialize declared rulebooks, skills, subagents, and collections |
| uninstall | Remove installed guidance, skills, and subagents |
| sizes | Rank the last recorded deployment's documents by size, with the context aggregates beneath them |
| status | Show the current state of installed items |
| validate | Check a content root for defects that reach a consumer; writes nothing |
| library list | List available library artifacts (rulebooks, skills, subagents, collections) |
| generate <target> | Generate a configuration file (e.g., label-map) |
Global options: --harness <claude\|rovo\|all> (default all), --link, --force, --dry-run, and --help. --content <dir> applies to validate alone, and --override-writer to install and sync --global (see Designated home-domain writer). Run codeassembly --help for the authoritative list.
Session-lifecycle hooks
Skills report the work that they do, but they cannot report a session opening, exiting, or handing a turn back to the developer: At those moments no skill is running. Each harness reports them instead, through its own event hooks, and relay-hook-event.mjs turns a hook into a lifecycle event:
| Event | Claude Code | Rovo Dev |
| ----------------- | ------------------ | ------------------ |
| session.started | SessionStart | on_session_start |
| session.ended | SessionEnd | on_session_end |
| turn.started | UserPromptSubmit | on_user_prompt |
| turn.completed | Stop | on_complete |
install places the relay in each harness's scripts/ directory and then wires the entries below into the harness config (~/.claude/settings.json, ~/.rovo/config.yml) by default. The wiring is its own step, shared across the CLI:
install --skip-hooksinstalls everything else and leaves the configs untouched.codeassembly configure-hooksruns just the wiring, for re-applying it later.configure-hooks --printprints the entries without writing anything: the manual-adoption path for a config managed elsewhere. The snippets below are exactly what it emits.uninstallremoves the entries;statusreports each one as present, drifted, or absent.
Every managed command ends in --sentinel codeassembly-agents. That token is the ownership marker: The CLI creates, replaces, and removes only entries whose command contains it, so hand-written hooks and other tools' entries are never disturbed. The relay accepts the flag and ignores it.
The relay reports a boundary and nothing more. It never sends the prompt text, and it always exits 0: A relay that failed loudly would be worse than the missing event, since both harnesses read some non-zero hook exits as a signal to block the agent.
Claude Code
In ~/.claude/settings.json, under hooks. Each entry names the hook that it relays, so the relay never has to infer where it was called from:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node ~/.claude/scripts/relay-hook-event.mjs --harness claude --hook SessionStart --sentinel codeassembly-agents"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "node ~/.claude/scripts/relay-hook-event.mjs --harness claude --hook SessionEnd --sentinel codeassembly-agents"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node ~/.claude/scripts/relay-hook-event.mjs --harness claude --hook UserPromptSubmit --sentinel codeassembly-agents"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node ~/.claude/scripts/relay-hook-event.mjs --harness claude --hook Stop --sentinel codeassembly-agents"
}
]
}
]
}
}Omit matcher on all four. SessionStart and SessionEnd accept one to select a start source or an end reason, and leaving it out relays every one of them; UserPromptSubmit and Stop ignore it.
Keep the whole invocation in command rather than splitting the flags into an args array: ~ expands only in the single-string form.
Rovo Dev
In ~/.rovo/config.yml, under eventHooks:
eventHooks:
events:
- name: on_session_start
commands:
- command: node /Users/you/.rovo/scripts/relay-hook-event.mjs --harness rovo --hook on_session_start --sentinel codeassembly-agents
- name: on_session_end
commands:
- command: node /Users/you/.rovo/scripts/relay-hook-event.mjs --harness rovo --hook on_session_end --sentinel codeassembly-agents
- name: on_user_prompt
commands:
- command: node /Users/you/.rovo/scripts/relay-hook-event.mjs --harness rovo --hook on_user_prompt --sentinel codeassembly-agents
- name: on_complete
commands:
- command: node /Users/you/.rovo/scripts/relay-hook-event.mjs --harness rovo --hook on_complete --sentinel codeassembly-agentsWrite the home directory out in full where the snippet shows /Users/you: configure-hooks writes the machine's absolute path here, matching the entries that Rovo's own tooling generates.
Two things to know about Rovo:
- Restart to pick up the change. Rovo reads its config at startup, so a running session ignores hooks added under it.
on_completefires when a run completes successfully. A turn that errors or is aborted may not report its end, leaving that session reading as still working until its next event.
Project declaration
A project opts into shared artifacts through .agents/codeassembly.yaml. Run codeassembly init to scaffold one, declare the artifacts that the project needs, then run codeassembly 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 init --global scaffolds ~/.agents/codeassembly.yaml, seeded with the recommended and triage collections. See Scopes.
Authoring conventions for the declared artifacts (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
The declaration is grouped by artifact type. Each type's block takes a use list (the slugs to adopt) and an optional drop list (slugs to remove from what broader scopes contributed):
rulebooks:
use:
- shell-conventions
skills:
use:
- people-report
subagents:
use:
- canaryA declared rulebook is delivered by its delivery mode: An ambient rulebook is injected into the ambient region of each targeted harness's guidance file, and a skill rulebook is delivered as a consult-<slug> skill in each targeted harness. A third mode, hook, produces no delivery of its own: It records that the rulebook is reached by a guidance-hook binding, which a codeassembly.yaml writes rather than the rulebook (see Guidance hooks). A rulebook may declare any combination of the three, and a list naming none of them is rejected.
A declared skill is deployed into each targeted harness's project-local skills directory (.claude/skills/<slug>/) with the harness transform applied (include expansion, {tool:…} rewrite, link rewriting), carrying a <!-- codeassembly-skill:<slug> --> ownership marker so that sync can retract it once it is no longer declared. Bare sync deploys into the project's harness directories; sync --global resolves the user-global tier and deploys the same way into the home harness directories instead (see Scopes).
A skill may restrict itself to specific harnesses with a supported-harnesses: frontmatter field (a single harness id or a list, e.g. supported-harnesses: [rovo]); sync then deploys it only into those harnesses, and library list shows the restriction. A skill with no supported-harnesses: field deploys to every harness. This is how a skill that one harness provides natively, but the library supplies for the others, is targeted at just the harnesses that need it, without duplicating it per harness.
A declared subagent is deployed into each targeted harness's project-local subagents directory (.claude/agents/<slug>.md), with the harness transform applied (frontmatter _defaults merge, {tool:…} rewrite, {harness_home_dir} rewrite) and a <!-- codeassembly-subagent:<slug> --> ownership marker so that sync can retract it once it is no longer declared. A declared subagent deploys into the repo under sync and into the home harness directories under sync --global.
rulebooks, skills, subagents, and collections are all deployed.
Two further top-level keys name where artifacts come from rather than which to adopt: sources (see Sources) and packages (see Packages). packages takes the same use/drop shape as a type block, so the semantics above carry over to it unchanged.
A third, harnesses, names where they go: see Harness targeting. A fourth, guidance-hooks, configures the artifacts the rest adopt rather than naming any: see Guidance hooks.
Harness targeting
harnesses declares which harnesses a run deploys into, in the same use/drop shape as a type block, with harness ids for entries:
harnesses:
use:
- claude
drop:
- rovoA run resolves its targets in this order, stopping at the first that answers:
- The
--harness <id>flag. (--harness allis the not-specified default and falls through.) - The
harnessesdeclaration, if any file in the chain declares one. A declaration that resolves to an empty set is honored: The run targets nothing and says so. - The harnesses installed for this user, detected by the presence of their home directories (
~/.claude,~/.rovo). A harness home is created by that harness's own installer, so its presence is evidence the harness is installed; a repository's own.claude/directory is not, which is why the repository is never probed.
harnesses resolves on a chain of its own. Which harnesses a developer runs is a fact about the developer. The key resolves across the user-global and project tiers together, the one key that crosses the domains defined under Scopes. Artifact keys deliberately do not: A user-global collections: use: [all] would otherwise deploy the whole catalog into every repository's harness directories.
root: true clears only its own domain's contributions. For every artifact key this is indistinguishable from clearing the whole chain, since their chain lies within one domain. It matters for harnesses alone, because it keeps a committed project file from discarding what the developer declared in the user-global tier. A drop still crosses the boundary, from either project-tier file: the committed .agents/codeassembly.yaml withdraws a harness for everyone working on the project, and the gitignored .agents/codeassembly.local.yaml withdraws one for a single checkout.
The three tiers therefore state three different things: the user-global tier states which harnesses are installed, the project tier states which the project requires, and codeassembly.local.yaml overrides either for one developer.
Targeting selects the harness set; artifact narrowing filters within it. A run targeting [claude, rovo] with a skill declaring supported-harnesses: [rovo] deploys that skill to Rovo alone. The two keys are distinct: harnesses lives in codeassembly.yaml and governs a whole run, while supported-harnesses lives in an artifact's frontmatter and governs that artifact.
sync, sync --global, and install all honor the declaration; install resolves it against the home tier alone, since it deploys into the harness homes. A declaration naming a harness whose home does not yet exist provisions that home, which detection could never reach. uninstall, status, and configure-hooks read --harness and the installed set, never the declaration: They must reach what is installed rather than what is declared.
Dropping a harness from the declaration retracts it. The next install removes that harness's tracked files, unwires its session-lifecycle hook entries, and drops it from the manifest; an empty declared set retracts every harness. A user-modified file is kept without --force and keeps its harness tracked for that file alone, and --dry-run previews the removals. The next sync clears what it deployed there in turn: skills across both namespaces, subagents, the per-source support root, the ambient region, and the prompts.yml region. Every removal there is gated on a sync provenance marker or a well-formed sync-owned region, so a hand-authored file survives. A damaged region is reported and left standing, since repairing the markers is the developer's call. The harness-home guidance file keeps its ambient markers, whose placement is install's; the project-local host loses the region outright and is deleted once nothing else remains in it. Retraction follows the declaration alone in both commands: --harness claude names a run's target rather than declaring the other harnesses unwanted, and a harness that detection misses has no home directory holding stale files.
Every run names what it targeted and what decided it:
Targeting claude, rovo (detected in ~).Guidance hooks
A guidance hook is a named slot that a skill or subagent declares in its body with <!-- guidance-hook: <name> -->, filled at sync time with the bodies of the rulebooks bound to it by a declaration. It is the third route that guidance takes into an agent's context, beside delivery: ambient, which charges every session, and delivery: skill, which depends on the agent choosing to consult it. A hook is scoped to the act: The guidance is present when the skill runs, and nowhere else.
A rulebook records the route with delivery: hook, alone or alongside the other two. That mode instructs nothing, unlike its siblings: The binding lives in a codeassembly.yaml, so a rulebook cannot splice itself into a host body and a hook fills from the deploy closure whether or not the rulebook names the route. Declaring it enables the three checks below.
guidance-hooks is the one map-valued key. Each hook name owns a use/drop block of its own, resolved on the scope chain exactly as an artifact type is. A tier binds to one hook without disturbing another:
guidance-hooks:
implementation-preferences:
use:
- williamthorsen-code-layout-preferences
- williamthorsen-typescript-preferencesA binding is also a dependency edge: A bound rulebook joins the deploy closure and still deploys by its own delivery:, so binding it and declaring it are one act. Bound bodies fill in declaration order, with their headings demoted one level so that a rulebook's title nests under the host's structure, and the result is wrapped in <!-- codeassembly-guidance-hook:<name>:start --> / :end markers enclosing one <!-- rulebook:<slug> --> block per rulebook, each naming the rulebook's version on a <!-- rulebook-version: <version> --> line when it declares one. A deployed file therefore says what filled it, and at which version, without being re-rendered.
A hook that nothing binds contributes nothing to deployed output, marker included. install reads no guidance-hooks: block, so every hook that it meets is unbound; so is every hook in a rulebook body, a skills/_data/ support entry, or a harness guidance file, none of which a binding can reach. Filling is for declared skills and subagents alone.
Name a hook for the concern rather than the consumer (implementation-preferences, not implement-plan-preferences), since concern-scoping lets one binding fill every consumer, and give it no user or org prefix, since the slot is generic and only the binding is personal. Names are lowercase kebab-case and letter-led, the same grammar enforced by the directive. Concern-scoping and the no-prefix rule are conventions; nothing checks them.
The library declares four hook names:
| Hook | Concern | Declared by |
| ---------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------- |
| comment-preferences | the register of comments written into source | the coder subagent, the five reviewer subagents, prose-reviser, and revise-prose |
| implementation-preferences | how code is written and judged | the implementing and reviewing skills, the coder, and the five plan-shaping subagents |
| ticketing-preferences | how work is split across tickets | the ticket-composing skills and planner |
| writing-preferences | how agent-authored prose reads | every subagent but the deployment canary, and revise-prose |
A binding fills a hook with the whole of the bound rulebook's body, and there is no way to bind part of one. A rulebook bound to a hook that only subagents declare is spliced entire into every declaring subagent, and none of the reports below can see that it contains guidance that those subagents have no use for. Keep such a rulebook coherent for its narrowest consumer: Once it mixes session-only guidance with the subagent-relevant kind, split it rather than binding the whole.
Two failures are worth naming. A binding to a rulebook that does not exist fails the run, naming the rulebook and the hook that bound it. A binding to a rulebook whose own body declares a hook fails too: Bound guidance is spliced as rendered, so nothing downstream could fill a hook inside it.
Three further mismatches are reported without failing the run, on a live sync and a dry run alike. A rulebook's delivery is written by its author and a binding by whoever adopts it. A disagreement between the two is not always the adopter's to resolve:
| Reported | Condition | Level |
| ----------------- | ------------------------------------------------------------------------------------------ | ------- |
| Bound, undeclared | a binding names a rulebook whose delivery omits hook | warning |
| Bound, unreached | a binding names a hook that no deployed skill or subagent declares, so it delivers nothing | advice |
| Declared, unbound | a rulebook names hook and no binding uses it | advice |
The last two are not defects. A collection can carry a hook-declaring rulebook into a project that never binds it, and a home-tier binding applies to every project, including those that deploy nothing declaring the hook. Each line names an affordance going unused rather than something broken. A binding that reaches nothing is also how a mistyped hook name surfaces, since nothing else would say so.
A rulebook whose delivery names ambient alongside hook is reported by none of them, and two things make the pairing legitimate. A hook that only subagents declare duplicates nothing: A subagent's context never contains the ambient region. The two routes are how one rulebook reaches a session and a subagent both. When a skill declares the hook, ambient delivery places the rulebook in the guidance file loaded by that session, so the fill hands it a second copy; the author who wrote both routes into delivery has weighed that, and the skill may be parsing what the fill delivers rather than only containing it, as revise-prose does. content/__tests__/guidance-hook-reach.unit.test.ts holds the library's record of which skills may.
A guidance hook is not a partial. A partial resolves by path, fixed at authoring time; a guidance hook resolves by binding, chosen per project or per machine. Guidance that every consumer of the library should get is a partial; guidance that one user or one project wants is a hook. See content/_partials/README.md.
Collections
A collection is a traversal-only aggregate: It deploys no file of its own, but declaring it pulls in its members' transitive closure, which sync then deploys. Declare one like any other type:
collections:
use:
- recommendedA collection lists its constituents under a members: key, either an explicit per-type block (the same shape that dependencies: uses) or the computed token '@library':
members:
skills:
- capture-feedback
subagents:
- canarymembers: is collections-only; rulebooks, skills, and subagents declare prerequisite edges under dependencies: instead. Declaring dependencies: on a collection, or members: on any other type, is an error that names the offending artifact.
Dropping or omitting a collection, or setting root: true, excludes its entire closure; dropping a single member that a collection contributed is not supported, so opt out of the whole collection or declare members à la carte instead.
Five collections ship, each making a claim that a reader can act on:
| Collection | Claim |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
| atlassian | Examined and found fitted to Bitbucket and Jira. Nothing outside it reaches its members, so only declaring it deploys them. |
| recommended | Examined and found generally applicable: no personal doctrine, no coupling to one author's environment. |
| williamthorsen | Examined and found deliberately personal: one author's preferences, environment, and domain. |
| triage | Not yet examined, and where new content starts. It shrinks by promotion. |
| all | The whole catalog, computed. It makes no claim about its members, and is the escape hatch rather than the expected choice. |
An artifact in none of them is standalone: deliberate, declared directly where wanted, and either too rarely invoked to justify a standing line in the skill index or wanted only in specific projects. The criteria deciding which disposition an artifact takes are recorded in the codeassembly-content-specification rulebook, under ## Collections.
codeassembly init --global seeds the user-global declaration (~/.agents/codeassembly.yaml) with recommended and triage; add any other collection to that file by hand. A project adds a collection for repo deployment by declaring it explicitly.
The @library token
A collection whose members: is the string '@library' resolves to every deployable artifact (all rulebooks, skills, and subagents) in the content root from which the collection resolves: the built-in library for a library collection, or the owning source for a collection declared in a source. It is computed at resolution time so that a newly added artifact joins automatically with no edit. The @ sigil marks a computed directive rather than a literal slug, so the value must be YAML-quoted ('@library'). Collections are excluded from the result: The resolver never emits them, and "every collection" would be self-referential.
The shipped all collection declares '@library'; declaring collections: use: [all] deploys the whole catalog.
Dependencies
A rulebook, skill, or subagent may declare dependencies on other artifacts in its frontmatter, grouped by artifact type. Resolution follows these edges transitively (deduped, with cycle detection), so declaring one artifact pulls in its whole closure:
dependencies:
rulebooks:
- shell-conventions
skills:
- people-report
subagents:
- canaryThe resolver follows members: and dependencies: identically; the split is semantic: A collection contains members, while an artifact depends on prerequisites.
Sources
By default, a declared artifact resolves from CodeAssembly's built-in content library. A top-level sources: list adds further content directories (machine-local, project-local, or a third-party guidance repo), each structured like the library's content/ (guidance/rulebooks/, guidance/_harnesses/, guidance/shared/, skills/, subagents/, collections/, scripts/). Resolution searches the declared sources first, then the library:
sources:
- name: org-guidance
path: ../shared-guidance
- name: personal
path: ~/guidance
rulebooks:
use:
- team-standardsEach source is a { name, path } pair (both required). A relative path resolves against the declaring file's .agents/ directory; ~ expands to the home directory, and absolute paths are used as-is. A source may declare the content format against which it was authored; see Content-format version. Declaration entries stay bare slugs: Resolution is transparent, so team-standards resolves from whichever source (or the library) provides it, with no per-entry from: syntax.
Precedence. A later-declared source shadows an earlier one, and any source shadows the library, which lets a source override a same-slug library artifact. A package adopted via packages is a source too, ranked below every hand-declared one. Repeating a source name remaps its path and moves it ahead of the sources declared before it. Because paths are .agents/-relative, commit only repo-relative source paths in codeassembly.yaml; confine machine-specific and absolute paths to codeassembly.local.yaml. A higher-precedence tier's root: true discards previously-declared sources exactly as it discards rulebooks, skills, subagents, and collections.
Undeclared content. scripts/ and the harness guidance templates under guidance/_harnesses/ are named by no declaration entry, so they resolve by directory rather than by slug and install deploys them. Scripts merge by file name across every root: A source shipping one script leaves the library's others in place. A harness's template directory is owned whole by the highest-precedence root shipping it, which keeps the guidance/shared/AGENTS.md that a template inlines resolving inside one root; a template file omitted by the owning source is retracted from the harness home. A file name or template directory shipped by more than one root installs from the highest-precedence one and warns, whether the loser is another source or the library. A source-owned deployed file's provenance marker names its path within the source, the source's name, and the source directory, in place of the codeassembly URL that a library file names.
Every artifact type resolves through sources: An artifact's body and its closure edges (dependencies:, or members: for a collection) resolve from the source that owns it, with ownership and retraction semantics identical to a library artifact's. A source-resolved skill or subagent expands its <!-- include: … --> directives against its own source root: It can reuse partials within its own source tree, but a target that resolves outside that root fails. A source-resolved collection expands its members through the resolver like any other type, and its '@library' token is source-scoped: It enumerates that source's own catalog rather than the built-in library. A declared source whose path is not a directory, or is unreadable, fails the run (dry-run included) before any file is written; one whose directory does not exist yet is reported as a warning and contributes nothing, so a source can be declared before it is populated. A slug found in no source or the library fails with an error naming every location searched.
Packages
A dependency can ship the guidance for using it, and a project adopts it by naming the package (no filesystem path, no generated file to keep in sync):
packages:
use:
- '@williamthorsen/nmr'That one line does two things: the package's content directory joins the source search order, and every rulebook, skill, and subagent shipped by the package is deployed. Nothing else is needed, because a package's whole catalog is its declaration, which is also why granularity is all-or-nothing. Adopting a package takes every artifact in its catalog; an individual one cannot be dropped, matching the existing limitation on collection members.
packages: is an ordinary declaration block, so use, drop, and root: true behave exactly as they do for an artifact type. A project-local tier can therefore decline a package adopted by the committed tier:
# .agents/codeassembly.local.yaml
packages:
drop:
- '@williamthorsen/nmr'Precedence. Every sources entry, from any tier, outranks every package, and every package outranks the built-in library: A directory named by hand should win over a dependency's. Among packages the ordinary rule applies: the highest tier wins, and within a tier the last declared wins. A package that masks a library slug is reported by the same shadow warning that a declared source triggers; two packages that ship the same slug resolve by precedence with no warning, and sync --dry-run names the source from which each artifact resolved.
Resolution. A declared package resolves through the module resolver, walking the node_modules chain searched by Node itself, so it holds under pnpm's hoisting and symlinked layouts. It also holds under a workspace:* link, which means a repo that produces a guidance-shipping package consumes its own guidance through the same declaration that a third party writes, resolved against the live source tree rather than a packed copy. A declared package that is not installed, or declares no content directory, fails the run (dry-run included) before any file is written, naming what was searched. One that declares a content directory that it does not ship warns rather than failing, like any other missing source. The consumer's declaration holds no path to correct, so the remedy is to create the directory in a package that the consumer maintains, or report the omission upstream in one that they do not.
Discovery. sync reports any direct dependency that ships content that the project has not declared, printing the packages: block that would adopt it. That is advice, not action: An undeclared dependency contributes nothing. Installing one changes nothing about what an agent reads, and drop silences the advice for a package that the project has turned down.
Upgrading an already-declared package is the other case. Its catalog is read from the filesystem, so a version that adds an artifact deploys it with no declaration change, the freshness property that makes the rendered guidance a function of what is installed. sync --dry-run prints the resolution report naming every artifact and the source from which it came, which is where that change is visible.
Shipping guidance from a package
A package declares where its content lives with a codeassembly key in its package.json, pointing at a directory structured like the library's content/:
{
"name": "@williamthorsen/nmr",
"codeassembly": { "content": "content/agents" },
"files": ["bin", "content", "dist"]
}content/agents/
collections/
guidance/rulebooks/
skills/
subagents/The key is required and has no default location. That is deliberate: A default would claim a directory name in every producer's package root, so instead a producer says where its content lives and can nest it under a directory that it already owns, including build output, if a build step puts it there.
A package's catalog is its rulebooks, skills, and subagents; a collections/ entry is resolvable but not adopted on its own. A collection reaches a consumer only when that consumer declares it by name. Its members are already in the catalog anyway. The way to pull in an artifact from outside the package (a library rulebook, say) is a dependencies: edge on an artifact that the catalog does contain.
Shipping support files. Anything under skills/ that contains no SKILL.md is a support entry: shared reference content that a skill or rulebook reads at runtime by path, skills/_data/ being the usual case. A package ships them by placing them where the library does, and they deploy alongside the skills whenever the package is adopted: no declaration of their own, since nothing names them but the links that reach them.
content/agents/
skills/
_data/
house-style.md
org-review/
SKILL.md # links to ../_data/house-style.mdEach source's support entries deploy into a namespace of their own, under skills/_sources/<source-name>/, so the built-in library and any number of packages can each ship a _data/house-style.md without one masking another. A scoped package name nests as its own segments (_sources/@williamthorsen/nmr/). Author links exactly as the library does, relative to the file's own place in the content tree, and delivery rewrites them to wherever they are deployed; a source name that could not name a directory fails the run rather than being silently reshaped.
_partials/ is the exception, being an include target inlined into the files that include it rather than a file that deploys.
Include the content directory in files. This is the one thing most likely to go wrong, because a workspace:* self-link resolves the live source tree and so never exercises packing. A producer that omits the entry sees its own guidance work perfectly and every consumer's install fail. pnpm pack and inspecting the tarball is the check that catches it.
Authoring the artifacts themselves is no different from authoring library content; see the content specification for frontmatter fields, dependencies:, members:, and invocation tokens. A package's content directory is a content root, so it contains a codeassembly-content.yaml like any other; see Content-format version.
Gate the content in the producer's own build. codeassembly validate runs the checks that a consumer's sync runs before writing (dependency closure, artifact resolution, delivery collisions, and a per-harness render) over the whole content root, writing nothing:
codeassembly validateBecause it reads no codeassembly.yaml, a package that produces guidance without consuming any still has a gate: Wire it into the repo's check and a defect fails the producer's build instead of the next consumer's install. The root comes from --content <dir>, or from the codeassembly.content key above when the flag is absent; neither yielding one is an error naming both routes. --harness narrows the run, and the default checks every harness to which the root could deploy, since a defect can reach only one. A clean root exits 0; any defect exits 1 after a report grouped by file. One check has no sync counterpart, and catches what nothing else would: a skill declaring the retired harnesses: key, which narrows nothing and survives into the deployed file rather than failing anywhere.
Coverage is what the root ships that reaches a consumer: rulebooks, skills, subagents, collections, and the support entries under skills/ that contain no SKILL.md. Link-target existence and cross-file anchors are not checked: A target resolves against the deployed tree, which unions this content with the library's and with every other declared source's.
One shape cannot consume its own guidance: A single-package repo whose package is the repo root has no workspace:* self-link to resolve through. Such a repo declares a sources: entry pointing at the directory instead.
Content-format version
A content root and the tool that deploys it are released separately, so a checkout can be newer than the codeassembly reading it. In a codeassembly-content.yaml at its top level, a root states the format contract against which it was authored, a sources: path and a packages: content directory alike:
# content/codeassembly-content.yaml
format: 1The tool holds the set of formats that it supports and refuses a root declaring any other, before any file is written and --dry-run included, naming the root, the format that it declares, and the formats supported. sync, sync --global, and install fail; validate reports it as a defect and exits 1. The remedy is to upgrade codeassembly to a version that supports the declared format.
A root with no manifest is format 1, which keeps a producer that predates the manifest working unchanged. A manifest that exists states its format: An absent or malformed format fails rather than passing as format 1, so every manifest that exists is self-describing.
Unknown keys pass through. A later tool can read a key that an older one ignores without the older one rejecting a root that it would otherwise honor. helpers: is reserved for the helper-bundling command and is unread at format 1.
What a bump obliges. The format version names the contract that the tool implements (frontmatter keys, invocation tokens, directives, and content-root layout), so it rises when content authored against the new contract would deploy wrongly under the old one rather than failing outright. Adding a key nothing older depends on does not need one; changing what an existing key means does.
| Format | Contract |
| ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| 1 | The contract documented here. |
| 2 | Adds the optional invocation-token form, {skill?:<slug>} and {subagent?:<slug>}, which names a target without deploying it. |
Scopes
The declaration resolves in two independent domains, each with its own base and local tiers and its own deployment target. The tiers within a domain run lowest to highest precedence.
Repo domain: codeassembly sync, deploying into the repo:
- Project:
.agents/codeassembly.yaml, committed and shared with the team. - Project-local:
.agents/codeassembly.local.yaml, gitignored, for personal overrides.
Home domain: codeassembly sync --global, deploying into the home harness directories (~/.claude, ~/.rovo) and ~/.agents/:
- User-global:
~/.agents/codeassembly.yaml, created byinit --global(declaresallby default). - User-global-local:
~/.agents/codeassembly.local.yaml, for personal overrides that survive reinstalls.
A higher tier adds to and overrides the tiers below it within the same domain: use adds an entry, drop removes one contributed by a broader tier in that domain, and root: true discards everything from broader tiers in that domain. Artifact keys never cross the domains: A project tier cannot drop a user-global rulebook, skill, subagent, or collection, and bare sync never writes the home directories (it refuses to run when invoked from the home directory, directing the caller to sync --global). harnesses is the one deliberate exception: Which harnesses a developer runs is a fact about the developer rather than about either domain's catalog, so it resolves across both tiers (see Harness targeting). Guidance-hook bindings do not cross either: sync resolves the project chain and sync --global the home chain, and neither sees the other. A project that deploys a hook-bearing skill therefore shadows the user's bound home copy with one bound only by the project's own chain. Guidance bound globally by the developer goes missing in that repository until the project binds it too. In both domains, ambient rulebooks are injected into the ambient region of a per-harness guidance file loaded by the harness at launch. In the repo domain the host is each targeted harness's machine-local project guidance file at the project root (CLAUDE.local.md, AGENTS.local.md), which sync creates when the project declares an ambient rulebook and appends its region to when the file already exists; because that host is gitignored, a multi-worktree checkout needs a sync per worktree (see Keeping deployed guidance current). In the home domain the host is each targeted harness's guidance file (~/.claude/CLAUDE.md, ~/.rovo/AGENTS.md), whose region's location comes from install's rendered template while its content belongs to sync --global: install preserves the region across re-renders and ignores it for drift detection, while hand edits elsewhere in those files still count as drift. Run install once before the first sync --global so that the region exists to fill; a guidance file without the region is skipped with a warning. sync --global also retires a legacy ~/.agents/GLOBAL.md, removing its sync-owned blocks and deleting the file unless it holds hand-written content; install and uninstall retire a legacy ~/.agents/AGENTS.md the same way, removing a copy deployed by the CLI and keeping one that holds hand-written content. For per-machine ambient guidance that should stay out of source control, declare a machine-local source (see Sources) holding a personal rulebook with delivery: ambient. In both domains, the deployed Rovo Dev skills are indexed into .rovo/prompts.yml so that they surface in Rovo Dev's available-skills list; sync owns a single sentinel-delimited region in that file and leaves any hand-authored entries outside it untouched, in the home file as well as the project file.
When upgrading from a build in which install deployed the catalog, run install once
