# Distribution Specification
<!--TOC-->
- [Purpose](#purpose)
- [Requirements](#requirements)
- [`distribution:manifest-identifies-every-owned-file` — The manifest identifies every owned file](#distributionmanifest-identifies-every-owned-file--the-manifest-identifies-every-owned-file)
- [`distribution:initialization-preserves-project-content` — Initialization preserves project content](#distributioninitialization-preserves-project-content--initialization-preserves-project-content)
- [`distribution:instances-operate-offline` — Instances operate offline](#distributioninstances-operate-offline--instances-operate-offline)
- [`distribution:upgrade-conflicts-are-atomic` — Upgrade conflicts are atomic](#distributionupgrade-conflicts-are-atomic--upgrade-conflicts-are-atomic)
- [`distribution:skills-are-part-of-the-payload` — Skills are part of the payload](#distributionskills-are-part-of-the-payload--skills-are-part-of-the-payload)
- [`distribution:a-skill-has-one-owner` — A skill has one owner](#distributiona-skill-has-one-owner--a-skill-has-one-owner)
- [`distribution:a-skill-obeys-the-portable-format` — A skill obeys the portable format](#distributiona-skill-obeys-the-portable-format--a-skill-obeys-the-portable-format)
- [`distribution:a-landing-classifies-its-target-first` — A landing classifies its target first](#distributiona-landing-classifies-its-target-first--a-landing-classifies-its-target-first)
- [`distribution:a-skill-checks-its-host-before-it-plans` — A skill checks its host before it plans](#distributiona-skill-checks-its-host-before-it-plans--a-skill-checks-its-host-before-it-plans)
- [`distribution:the-doctor-answers-for-the-installed-skills` — The doctor answers for the installed skills](#distributionthe-doctor-answers-for-the-installed-skills--the-doctor-answers-for-the-installed-skills)
- [`distribution:a-skill-plans-before-it-acts` — A skill plans before it acts](#distributiona-skill-plans-before-it-acts--a-skill-plans-before-it-acts)
- [`distribution:skill-install-previews-before-writing` — Skill install previews before writing](#distributionskill-install-previews-before-writing--skill-install-previews-before-writing)
- [`distribution:a-stale-skill-is-not-a-conflict` — A stale skill is not a conflict](#distributiona-stale-skill-is-not-a-conflict--a-stale-skill-is-not-a-conflict)
- [`distribution:a-skill-install-restores-on-failure` — A skill install restores on failure](#distributiona-skill-install-restores-on-failure--a-skill-install-restores-on-failure)
- [`distribution:skill-uninstall-removes-only-what-it-wrote` — Skill uninstall removes only what it wrote](#distributionskill-uninstall-removes-only-what-it-wrote--skill-uninstall-removes-only-what-it-wrote)
- [`distribution:an-install-sweeps-what-the-payload-dropped` — An install sweeps what the payload dropped](#distributionan-install-sweeps-what-the-payload-dropped--an-install-sweeps-what-the-payload-dropped)
- [`distribution:user-scope-files-stay-unrecorded` — User-scope files stay unrecorded](#distributionuser-scope-files-stay-unrecorded--user-scope-files-stay-unrecorded)
- [`distribution:shared-skill-artifacts-have-one-home` — Shared skill artifacts have one home](#distributionshared-skill-artifacts-have-one-home--shared-skill-artifacts-have-one-home)
- [`distribution:the-payload-names-no-planning-tool` — The payload names no planning tool](#distributionthe-payload-names-no-planning-tool--the-payload-names-no-planning-tool)
- [`distribution:the-payload-names-no-other-project` — The payload names no other project](#distributionthe-payload-names-no-other-project--the-payload-names-no-other-project)
- [`distribution:the-payload-roots-are-declared-once` — The payload roots are declared once](#distributionthe-payload-roots-are-declared-once--the-payload-roots-are-declared-once)
- [`distribution:a-seeded-rule-runs-no-canon-command` — A seeded rule runs no canon command](#distributiona-seeded-rule-runs-no-canon-command--a-seeded-rule-runs-no-canon-command)
<!--TOC-->
## Purpose
Rules governing installation, ownership classes, offline verification, and upgrades. The distribution is one installed binary, `sdd`, that carries the payload, and every rule here binds whoever authors that binary. No instance adopts this spec: its subject is the installer, so an instance holding these rules would hold obligations it cannot violate and verifications it cannot run. What a project owes its own installation is stated in `SPEC-instance.md`; the rules the canon alone runs at release time are stated in `SPEC-release.md`.
## Requirements
### `distribution:manifest-identifies-every-owned-file` — The manifest identifies every owned file
The installer MUST record each installed file with its ownership class, destination, and SHA-256, and the record MUST state which canon version produced it.
#### Scenario: An agent encounters a local edit
- GIVEN an installed file differs from its baseline
- WHEN the verifier reads the manifest
- THEN it distinguishes managed drift from adopted reconciliation
Verify: `cargo nextest run -E 'binary(cmd_verify) + binary(cmd_status)'`
### `distribution:initialization-preserves-project-content` — Initialization preserves project content
When a target is non-empty, the installer MUST preview its changes before writing any file.
#### Scenario: A repository has a hand-commented hook configuration
- GIVEN comments outside the managed markers
- WHEN initialization inserts its block
- THEN every outside comment remains byte-identical
Verify: `cargo nextest run -E 'binary(cmd_init)'`
### `distribution:instances-operate-offline` — Instances operate offline
The installed binary MUST verify and upgrade an instance without a network or canon checkout.
#### Scenario: The canon repository is unreachable
- GIVEN a fully installed target
- WHEN `sdd verify` and `sdd upgrade` run with no network
- THEN they check hashes, the managed block, rule IDs, and the binary's own version against the manifest, from the payload the binary carries
Verify: `cargo nextest run -E 'binary(cmd_verify) + binary(cmd_upgrade)'`
### `distribution:upgrade-conflicts-are-atomic` — Upgrade conflicts are atomic
If a managed file differs from its installed hash, then the upgrader MUST abort without changing the target.
#### Scenario: One managed configuration is edited locally
- GIVEN a valid installed instance with one managed edit
- WHEN an upgrade is requested
- THEN it lists every conflict in one run and changes no target byte
Verify: `cargo nextest run -E 'binary(cmd_upgrade)'`
### `distribution:skills-are-part-of-the-payload` — Skills are part of the payload
The distribution MUST embed every skill authored under `skills/`, so a binary carries the skills of its own version and no instance fetches them.
#### Scenario: A skill is authored under the canon's skills directory
- GIVEN a skill directory added under `skills/`
- WHEN the binary is built
- THEN `sdd skill list` names it and `sdd skill show` prints it byte-identical to the authored file
Verify: `cargo nextest run -E 'binary(cmd_skill)'`
### `distribution:a-skill-has-one-owner` — A skill has one owner
The distribution MUST install every skill at user scope alone; no profile may project a skill into an instance, because an agent resolves a skill by name and a second copy under one name is a second entry offering the same skill.
#### Scenario: A project is initialized inside a home that already carries the skills
- GIVEN a home directory holding the skills and a target repository with no instance
- WHEN `sdd init --apply` runs with either profile
- THEN the target carries no `.claude/skills/` or `.agents/skills/` file and the manifest records none, so each skill resolves to exactly one file
Verify: `cargo nextest run -E 'binary(cmd_init) + binary(cmd_skill)'`
### `distribution:a-skill-obeys-the-portable-format` — A skill obeys the portable format
Every skill MUST carry only the portable Agent Skills frontmatter fields, a `name` equal to its directory name, and a body at or below 150 lines.
#### Scenario: A skill gains an agent-specific field
- GIVEN a skill edited to add a vendor-only frontmatter key
- WHEN the canon test suite runs
- THEN the conformance test fails and names the offending field
Verify: `cargo nextest run -E 'binary(canon)'`
### `distribution:a-landing-classifies-its-target-first` — A landing classifies its target first
Where a setup or migration task finds no instance at a target, the skills and the shared pre-flight gate MUST route by `sdd assess`, which MUST report its evidence and exactly one verdict — `brownfield` where a documentation root is populated or a methodology marker exists, `greenfield` where no document beyond root metadata exists, and `needs-decision` otherwise — writing nothing, with every produced classification exiting 0.
#### Scenario: A documented target carries no instance
- GIVEN a repository holding a populated documentation root and no instance manifest
- WHEN `sdd assess --target . --json` runs
- THEN the report classifies `brownfield` and exits 0, so the routing skill loads the migration path instead of landing seeds beside the corpus
Verify: `cargo nextest run -E 'binary(cmd_assess) + binary(canon)'`
### `distribution:a-skill-checks-its-host-before-it-plans` — A skill checks its host before it plans
Every skill MUST direct the agent to run the shared pre-flight gate — which observes the host with `sdd doctor` — before planning, whatever flags the request carries; the `--no-plan` flag changes only when the plan gate asks for approval.
#### Scenario: A request says to skip the checks
- GIVEN a request carrying `--no-plan` and an instruction to act immediately
- WHEN the agent follows the skill's opening section
- THEN the pre-flight still runs, because the task's steps have the same dependencies whatever the request says, and only the plan gate's approval turn is skipped
Verify: `cargo nextest run -E 'binary(canon)'`
### `distribution:the-doctor-answers-for-the-installed-skills` — The doctor answers for the installed skills
`sdd doctor` MUST run every probe in the catalog and exit 0 whatever they find, and its skill probes MUST pick the remediation by the user-scope record: drift the record vouches for is a stale install corrected by a plain apply, and drift it cannot account for is the user's own, corrected only with `--force`.
#### Scenario: A home holds a skill an older release installed
- GIVEN an agent root holding a `SKILL.md` whose digest the user-scope record vouches for
- WHEN `sdd doctor --json` runs
- THEN the `skill-payload` probe fails naming `sdd skill install --apply` without `--force`, and the exit code is 0
Verify: `cargo nextest run -E 'binary(cmd_doctor)'`
### `distribution:a-skill-plans-before-it-acts` — A skill plans before it acts
Every skill MUST open its body with one section that directs the agent to read the two shared gates in order — the pre-flight gate first, then the plan gate — before the first action of a task and to hold the plan gate's three phases — plan, validate, execute — for the whole task, ahead of every other section.
#### Scenario: A skill gains a section above the gate
- GIVEN a skill edited so another section precedes the gate section
- WHEN the canon test suite runs
- THEN the conformance test fails naming the skill, because an agent acts on the first instruction it reads
Verify: `cargo nextest run -E 'binary(canon)'`
### `distribution:skill-install-previews-before-writing` — Skill install previews before writing
When run without `--apply`, `sdd skill install` MUST list every destination and write nothing, and when a destination holds bytes neither the payload nor the user-scope record accounts for, an apply MUST refuse atomically, listing every conflict.
#### Scenario: A home directory already carries an edited skill
- GIVEN `~/.claude/skills/sdd-setup/SKILL.md` with bytes the user wrote
- WHEN `sdd skill install --apply` runs
- THEN it exits 73 listing every conflicting destination, writes no file, and states `--force` as the override
Verify: `cargo nextest run -E 'binary(cmd_skill)'`
### `distribution:a-stale-skill-is-not-a-conflict` — A stale skill is not a conflict
Where a user-scope destination holds the bytes a previous apply recorded writing there, `sdd skill install --apply` MUST replace it without `--force`.
#### Scenario: A release edits a skill the user never touched
- GIVEN a home directory whose installed skills came from an older release
- WHEN a newer `sdd skill install --apply` runs
- THEN every destination is rewritten and none is reported as a conflict, because bytes this tool wrote are not the user's work
Verify: `cargo nextest run -E 'binary(cmd_skill)'`
### `distribution:a-skill-install-restores-on-failure` — A skill install restores on failure
Where an apply fails partway, `sdd skill install` MUST restore every destination it backed up and name the path that failed.
#### Scenario: The second skill root cannot be written
- GIVEN two skill roots, the second holding a destination the process cannot write
- WHEN `sdd skill install --apply` has already rewritten the first root
- THEN it exits 73 naming the unwritable path and leaves both roots as found, because one agent reading a newer skill than another is worse than neither being upgraded
Verify: `cargo nextest run -E 'binary(cmd_skill)'`
### `distribution:skill-uninstall-removes-only-what-it-wrote` — Skill uninstall removes only what it wrote
When run without `--apply`, `sdd skill uninstall` MUST list every removal and delete nothing, and when applied it MUST remove each embedded skill's `SKILL.md`, every other destination the record vouches for, and a directory left holding nothing else, while leaving every file it did not write.
#### Scenario: A skill directory carries a user's own note
- GIVEN an installed `~/.claude/skills/sdd-setup/` holding `SKILL.md` and a hand-written `notes.md`
- WHEN `sdd skill uninstall --apply` runs
- THEN `SKILL.md` is removed, `notes.md` and its directory remain, and the kept directory is named in the output
Verify: `cargo nextest run -E 'binary(cmd_skill)'`
### `distribution:an-install-sweeps-what-the-payload-dropped` — An install sweeps what the payload dropped
Where the user-scope record vouches for a destination the current payload no longer carries, `sdd skill install --apply` and `sdd skill uninstall --apply` MUST remove it and the directory it leaves empty, and MUST leave a destination the record cannot vouch for alone.
#### Scenario: A release renames a skill
- GIVEN a home directory holding a skill under its old name, recorded by the apply that wrote it
- WHEN a newer `sdd skill install --apply` runs
- THEN the old name's `SKILL.md` and its directory are gone, the new name is installed, and only one entry is offered under either name
Verify: `cargo nextest run -E 'binary(cmd_skill)'`
### `distribution:user-scope-files-stay-unrecorded` — User-scope files stay unrecorded
Files `sdd skill install` writes outside an instance MUST NOT appear in any instance manifest; the payload and the user-scope record are the references the installer compares them against, and no verification reads either.
#### Scenario: An instance is verified after a user-scope install
- GIVEN an installed instance and a completed `sdd skill install --apply`
- WHEN `sdd verify` runs against the instance
- THEN the report is unchanged by anything under the home directory
Verify: `cargo nextest run -E 'binary(cmd_skill)'`
### `distribution:shared-skill-artifacts-have-one-home` — Shared skill artifacts have one home
The distribution MUST install every artifact the skills share exactly once, at the state root the user-scope record lives in, whichever agent an install selects, and an uninstall MUST retain those artifacts while any agent root still holds an installed skill.
#### Scenario: One agent family's skills are uninstalled
- GIVEN both agent roots holding installed skills and the shared artifacts landed
- WHEN `sdd skill uninstall --agent codex --apply` runs
- THEN the shared artifacts remain, because the other root's skills still name them, and only the uninstall that takes the last skills takes the artifacts along
Verify: `cargo nextest run -E 'binary(cmd_skill)'`
### `distribution:the-payload-names-no-planning-tool` — The payload names no planning tool
The author MUST keep every embedded payload root free of a planning tool's name, so an instance may pair this framework with any work-record convention or none.
#### Scenario: A method chapter names the tool it was tested against
- GIVEN a chapter edited to illustrate the seam with one planning tool by name
- WHEN the canon test suite runs
- THEN the check fails naming the file and the term, because a framework that names one tool stops being pairable with another
Verify: `cargo nextest run -E 'binary(canon)'`
### `distribution:the-payload-names-no-other-project` — The payload names no other project
The author MUST keep every embedded payload root free of the name of a project, repository, or organization outside this one, apart from the forges, agents, and reference works the method documents as integrations.
#### Scenario: A chapter carries an example from the repository it was drafted in
- GIVEN a chapter or skill that names a sibling project while illustrating a rule
- WHEN the canon test suite runs
- THEN the check fails naming the file and the term, because a reader who lacks that project meets a reference they cannot follow
Verify: `cargo nextest run -E 'binary(canon)'`
### `distribution:the-payload-roots-are-declared-once` — The payload roots are declared once
The author MUST declare the embedded payload roots in one place that the binary, the build script, and the canon suite all read.
#### Scenario: An eighth root is embedded
- GIVEN a new root added to the embedding module alone
- WHEN the canon suite scans the payload for what it may not carry
- THEN the scan walks a list that no longer describes the payload, so the root ships unscanned unless one declaration feeds all three
Verify: `cargo nextest run -E 'kind(lib)'`
### `distribution:a-seeded-rule-runs-no-canon-command` — A seeded rule runs no canon command
Where a spec is seeded into an instance, the author MUST keep the words `cargo` and `just` out of every shell command its verification lines carry.
#### Scenario: A canon-only rule is left in a seeded spec
- GIVEN a seeded spec carrying a rule verified by a cargo test
- WHEN the canon test suite runs
- THEN the check fails naming the spec and the command, because the adopter would read an unrunnable verification as work it owes
Verify: `cargo nextest run -E 'binary(canon)'`