.facet` — the single distributable artifact, with the build manifest embedded inside it (see [Archive format](/specification/archive)). `dist/` is purged first, so it contains exactly one `.facet` after a successful build. For a scoped identity the name's `/` renders as a nested path: `@acme/cowsay` at `1.0.0` lands at `dist/@acme/cowsay-1.0.0.facet`.
diff --git a/docs/cli/authoring/create.mdx b/docs/cli/authoring/create.mdx
index 83f1991e..8200a2a4 100644
--- a/docs/cli/authoring/create.mdx
+++ b/docs/cli/authoring/create.mdx
@@ -23,6 +23,7 @@ facet create ./my-facet \
--version 0.1.0 \
--skill greet --agent helper --command run \
--json # headless scaffold, machine-readable result
+facet create ./bare --name bare --skill greet --no-readme # skip the default README
```
## Flags
@@ -44,7 +45,11 @@ facet create ./my-facet \
- An asset to scaffold. Repeat per asset; at least one is required in headless mode. Asset names must be kebab-case.
+ An asset to scaffold. Repeat per asset; at least one is required in headless mode. Asset names are single-segment [Agent Skills names](/specification/manifest#asset-names); a skill and command can't share a name.
+
+
+
+ Skip the default `README.md`. `facet create` writes and declares an editable `README.md` (seeded from the name and description) by default; pass `--no-readme` to scaffold without it.
@@ -79,17 +84,19 @@ After scaffolding, validate with [`facet build --verify`](/cli/authoring/build),
3. **Version** -- defaults to `0.0.0`.
4. **Privacy** -- choose Public (the default) or Private.
5. **Assets** -- add skills, agents, and commands by name.
-6. **Confirmation** -- review the summary -- and confirm.
+6. **README** -- enabled by default. The wizard seeds `README.md` from the name and description; open the editor to customize it, or toggle it off. Edited content is preserved even if you change the name or description later.
+7. **Confirmation** -- review the summary — which lists `README.md` when enabled — and confirm.
## Generated files
On confirmation, the wizard writes:
-- `facet.json` -- the manifest with named asset descriptors
+- `facet.json` -- the manifest with named asset descriptors (and `README.md` in top-level `files` when README is enabled)
+- `README.md` -- editable README, written and declared by default (skip with `--no-readme`)
- `skills//SKILL.md` -- starter skill template (Agent Skills directory convention)
- `agents/.md` -- starter agent template
- `commands/.md` -- starter command template
-Content files are markdown. Optional YAML front matter [survives the build untouched](/specification/build#steps); at install time the manifest's `name`, `description`, and any per-adapter extras are merged on top of whatever the author wrote.
+Content files are markdown. A primary asset file (skill, agent, command) carries **no** YAML front matter — asset metadata lives in the manifest. `README.md` and other [supplementary files](/specification/manifest#supplementary-files) are written verbatim.
After creating the project, use `facet edit` to iterate on your facet, or `facet build` to validate and package it.
diff --git a/docs/cli/authoring/edit.mdx b/docs/cli/authoring/edit.mdx
index 21516659..5b3bb3dd 100644
--- a/docs/cli/authoring/edit.mdx
+++ b/docs/cli/authoring/edit.mdx
@@ -30,10 +30,12 @@ The edit command has two phases:
If the edit command detects drift between the manifest and the files on disk, it enters a reconciliation phase first. Drift includes:
-- **New files on disk** not tracked in the manifest -- choose "Add to manifest" or "Ignore for now"
-- **Missing files** declared in the manifest but absent from disk -- choose "Scaffold template" or "Remove from manifest"
+- **New assets on disk** not tracked in the manifest -- choose "Add to manifest" or "Ignore for now"
+- **Undeclared files inside a declared skill directory** -- adopt them into that skill's [companion `files`](/specification/manifest#supplementary-files), or ignore
+- **Common root files** like `LICENSE` -- adopt them into the top-level `files`, or ignore
+- **Missing files** declared in the manifest but absent from disk -- choose "Scaffold template" or "Remove from manifest" (at the exact declared path, for both assets and supplementary files)
-YAML front matter in content files is not flagged here -- it is permitted and [preserved verbatim through the build](/specification/build#steps), then merged with the manifest's `name`, `description`, and any per-adapter extras at install time.
+`README.md` and the extensionless `README` are handled by their own [README panel](#readme-panel), not generic reconciliation, so they never appear twice. Ignored files stay on disk and undeclared.
All reconciliation items must be resolved before proceeding to editing.
@@ -43,19 +45,36 @@ After reconciliation (or immediately if no drift), the edit phase allows:
- **Identity editing** -- modify the facet name, description, and version
- **Privacy editing** -- inspect the facet's current visibility intent without opening `facet.json`, and switch between Public and Private.
-- **Asset management** -- add, remove, or rename skills, agents, and commands
+- **Asset management** -- add, remove, or rename skills, agents, and commands. Deleting a skill removes its declared companion files too, while any files you added to the skill directory yourself are preserved.
- **Description editing** -- press Enter on an asset to edit its name, or press ↓ during name editing to open the description in your terminal editor (`$VISUAL` / `$EDITOR` / `vi`)
+A primary asset file carries no YAML front matter — asset metadata lives in the manifest, and edit writes confirmed content without front matter.
+
All changes are transactional -- nothing is written to disk until you review and confirm on the confirmation page. Exit at any point with Esc Esc to discard all changes.
+### README panel
+
+Both conventional README paths — `README.md` and the extensionless `README` — get a dedicated panel, managed independently. The actions offered depend on each path's current state:
+
+| State | Actions |
+| --- | --- |
+| Present and declared | **Edit** the content, or **Remove** (deletes the file and its declaration) |
+| Present but undeclared | **Adopt** (declare it, bytes untouched), or **Edit and adopt** |
+| Declared but missing | **Scaffold** at that path, or **Remove declaration** |
+| Absent and undeclared | **Create** (defaults to `README.md`) |
+
+Adopt preserves the existing bytes unless you explicitly edit them. Every README operation is queued until Apply, like all other edits.
+
## Confirmation
Before applying, a summary shows the final state of your facet -- identity fields, the privacy intent, and all assets with their descriptions -- along with a reminder that a privacy change is embedded at build time (rebuild to apply it, and bump the version if it was already published). Choose "Apply" to write changes or "Go back" to continue editing.
-On apply, the edit command:
+The confirmation lists every queued file operation with its exact path. On apply, the edit command transactionally:
- Writes the updated `facet.json`
- Scaffolds template files for new assets
-- Deletes files for removed assets
+- Deletes files for removed assets (and a removed skill's declared companions)
+- Writes or deletes README files per the [README panel](#readme-panel) choices
+- Scaffolds or removes reconciled supplementary files
## See also
diff --git a/docs/cli/authoring/modify.mdx b/docs/cli/authoring/modify.mdx
index dd937a55..bd8d6392 100644
--- a/docs/cli/authoring/modify.mdx
+++ b/docs/cli/authoring/modify.mdx
@@ -78,12 +78,16 @@ facet modify agent helper --description "Reviews code"
### Rename an asset
-Renames the manifest key and moves the asset's file (removing the now-empty skill directory, for skills).
+Renames the manifest key and moves the asset's primary file (removing the now-empty skill directory, for skills).
```sh
facet modify command run --rename start
```
+
+ `facet modify` operates on asset primaries. To adopt, move, or reconcile a skill's [companion files](/specification/manifest#supplementary-files) or top-level supplementary files, use [`facet edit`](/cli/authoring/edit).
+
+
### Remove an asset
Removes the manifest descriptor and deletes the asset's file.
diff --git a/docs/docs/learn/skills.mdx b/docs/docs/learn/skills.mdx
index 22801a59..a9a52b3a 100644
--- a/docs/docs/learn/skills.mdx
+++ b/docs/docs/learn/skills.mdx
@@ -3,19 +3,15 @@ title: Skills
description: Reusable knowledge and guidelines
---
-A skill is a markdown file containing domain-specific instructions, guidelines, or best practices. Skills follow the [Agent Skills](https://agentskills.io/specification) specification. They are the passive knowledge layer of a facet -- text that shapes how an AI assistant approaches a domain without defining a persona or a user-invokable action.
+A skill is a directory bundle — a `SKILL.md` primary plus any declared companion files — containing domain-specific instructions, guidelines, or best practices. Skills follow the [Agent Skills](https://agentskills.io/specification) specification. They are the passive knowledge layer of a facet -- text that shapes how an AI assistant approaches a domain without defining a persona or a user-invokable action.
-Each skill lives at `skills//SKILL.md` and is declared in `facet.json` under the `skills` map. The normative rules — placement, descriptor shape, and the YAML front-matter contract — live at [Text assets](/specification/manifest#text-assets) in the manifest specification.
+Each skill lives in its own directory at `skills//SKILL.md` and is declared in `facet.json` under the `skills` map. The normative rules — placement, descriptor shape, and the no-front-matter contract — live at [Text assets](/specification/manifest#text-assets) in the manifest specification.
## Example
A "code-review" skill (`skills/code-review/SKILL.md`):
```markdown expandable skills/code-review/SKILL.md
----
-description: Code review guidelines for TypeScript projects
----
-
# Code Review
## Structure
@@ -40,6 +36,23 @@ description: Code review guidelines for TypeScript projects
- Do not comment obvious code.
```
+## Companion files
+
+A skill can ship more than its `SKILL.md`. Declare **companion files** — references, scripts, templates — in the skill descriptor's `files` array, as exact paths relative to the skill directory:
+
+```json
+{
+ "skills": {
+ "code-review": {
+ "description": "Code review guidelines",
+ "files": ["references/style-guide.md", "scripts/lint.ts"]
+ }
+ }
+}
+```
+
+Companions ship inside the archive and install and remove **atomically** with their skill; files you add to a skill directory yourself are left untouched on removal. They ship as opaque bytes (binary and empty files are fine), the skill name is still a single segment (the `/` in a companion path is directory depth, not part of the name), and a companion can't be `SKILL.md` itself. See [Supplementary files](/specification/manifest#supplementary-files) for the full rules.
+
## When to use skills
Skills are **passive knowledge** -- they inform how the assistant thinks and works but do not define who it is or what the user can invoke. Use skills for:
diff --git a/docs/guides/create-your-first-facet.mdx b/docs/guides/create-your-first-facet.mdx
index a9095dfd..5c4d111b 100644
--- a/docs/guides/create-your-first-facet.mdx
+++ b/docs/guides/create-your-first-facet.mdx
@@ -18,7 +18,8 @@ By the end of this guide you will have a working `.facet` file ready to publish
> The short version:
>
> ```sh
-> # 1. Scaffold headlessly (no wizard):
+> # 1. Scaffold headlessly (no wizard). A default README.md is written and
+> # declared unless you pass --no-readme:
> facet create my-facet \
> --name my-facet --description "My first facet" \
> --skill code-review --agent reviewer --command review-pr
@@ -59,12 +60,13 @@ That gives you:
```
my-facet/
├── facet.json
+├── README.md
├── skills/code-review/SKILL.md
├── agents/reviewer.md
└── commands/review-pr.md
```
-Each asset type has a conventional path: skills at `skills//SKILL.md`, agents at `agents/.md`, commands at `commands/.md`. Asset names are always plain kebab-case, even when the facet identity is scoped (`@scope/name`).
+Each asset type has a conventional path: skills at `skills//SKILL.md`, agents at `agents/.md`, commands at `commands/.md`. Asset names are single-segment [Agent Skills names](/specification/manifest#asset-names) (lowercase ASCII, digits, and hyphens; no slashes), even when the facet identity is scoped (`@scope/name`); skills and commands share one namespace and can't reuse a name. `facet create` also writes an editable `README.md` by default and declares it in the manifest's top-level `files` — pass `--no-readme` to skip it.
## Understand the manifest
@@ -77,12 +79,31 @@ Each asset type has a conventional path: skills at `skills//SKILL.md`, age
"description": "My first facet",
"skills": { "code-review": { "description": "A Code Review skill" } },
"agents": { "reviewer": { "description": "A Reviewer agent" } },
- "commands": { "review-pr": { "description": "A Review Pr command" } }
+ "commands": { "review-pr": { "description": "A Review Pr command" } },
+ "files": ["README.md"]
}
```
Each asset type is a map of name to descriptor. A descriptor needs a `description` and may carry optional `adapters` metadata. The name maps to the file at its conventional path. At least one asset is required. The full schema and name grammar live in the [manifest specification](/specification/manifest).
+### Ship supporting files
+
+The top-level `files` array declares [supplementary files](/specification/manifest#supplementary-files) — non-asset files like `README.md`, `LICENSE`, or design notes. They ship inside the archive and are integrity-protected, but never materialize on disk at install. A skill can also declare `files` of its own — **companion files** (references, scripts, templates) that install and remove atomically with the skill:
+
+```json
+{
+ "skills": {
+ "code-review": {
+ "description": "A Code Review skill",
+ "files": ["references/style-guide.md", "scripts/lint.ts"]
+ }
+ },
+ "files": ["README.md", "LICENSE"]
+}
+```
+
+Declare exact paths only (no globs). Top-level paths can't point under `skills/`, and a skill's `files` are relative to its own directory and can't list `SKILL.md`. Supplementary bytes ship verbatim — binary and empty files are fine, and front matter is never parsed.
+
You rarely hand-edit `facet.json` for routine changes -- [`facet modify`](/cli/authoring/modify) does it for you:
```sh
@@ -92,15 +113,11 @@ facet modify facet --version 0.1.0
## Write your content
-Open each starter file and replace the template with your content. The files are plain markdown; optional YAML front matter is preserved through the build and merged with the manifest's metadata at install time.
+Open each starter file and replace the template with your content. The files are plain markdown. Asset metadata lives in the manifest, not the file, so a primary asset file MUST NOT carry YAML front matter — set `description` and adapter config with `facet modify` instead.
**`skills/code-review/SKILL.md`** -- a [skill](/docs/learn/skills) provides reusable guidelines, following the [Agent Skills specification](https://agentskills.io/specification):
```markdown
----
-description: Guidelines for reviewing code changes
----
-
# Code Review
- Check new files are in the right directory and imports are tidy.
@@ -162,7 +179,7 @@ A scoped identity renders as a nested path: `@acme/my-facet` at `0.0.0` writes t
Two ways to change a facet after scaffolding:
- **[`facet modify`](/cli/authoring/modify)** -- scriptable, one change per command. Add, remove, rename, or re-describe assets, set adapter config, or update facet metadata. This is the fastest path (and what agents use).
-- **[`facet edit`](/cli/authoring/edit)** -- an interactive workbench that also reconciles drift between `facet.json` and files you added, renamed, or removed on disk.
+- **[`facet edit`](/cli/authoring/edit)** -- an interactive workbench that also reconciles drift between `facet.json` and files you added, renamed, or removed on disk, adopts skill companions and root files like `LICENSE`, and has a dedicated panel for authoring `README.md`.
Whichever you use, finish with `facet build --verify` to confirm the facet still builds.
diff --git a/docs/guides/install-facets.mdx b/docs/guides/install-facets.mdx
index c3fbc79e..3736d868 100644
--- a/docs/guides/install-facets.mdx
+++ b/docs/guides/install-facets.mdx
@@ -94,7 +94,7 @@ Your project now has two files -- commit both to version control so teammates a
**`facets.json`** -- the project manifest: which facets belong to the project, and at what version. It is the source of truth for your dependencies.
-**`facets.lock`** -- the lockfile: exact resolved versions, integrity hashes, and asset lists for reproducible installs. It is managed by the CLI; you never hand-edit it.
+**`facets.lock`** -- the lockfile: exact resolved versions, integrity hashes, and per-file asset records for reproducible installs. It is managed by the CLI; you never hand-edit it.
@@ -109,17 +109,26 @@ Your project now has two files -- commit both to version control so teammates a
```json expandable facets.lock
{
- "lockfileVersion": 1,
+ "lockfileVersion": 0.2,
"facets": {
"cowsay": {
"source": { "kind": "registry", "registry": "https://api.agentfacets.io" },
"version": "1.0.0",
"integrity": "sha256:70fe8f9f0ba1…",
"assets": [
- { "scope": "project", "type": "skill", "name": "cowsay-rules" },
- { "scope": "project", "type": "agent", "name": "cowsay" },
- { "scope": "project", "type": "command", "name": "cowchat" },
- { "scope": "project", "type": "command", "name": "cowsay" }
+ {
+ "scope": "project", "type": "skill", "name": "cowsay-rules",
+ "files": [
+ { "path": "skills/cowsay-rules/SKILL.md", "integrity": "sha256:11aa22…" },
+ { "path": "skills/cowsay-rules/references/moods.md", "integrity": "sha256:22bb33…" }
+ ]
+ },
+ { "scope": "project", "type": "agent", "name": "cowsay",
+ "files": [{ "path": "agents/cowsay.md", "integrity": "sha256:33cc44…" }] },
+ { "scope": "project", "type": "command", "name": "cowchat",
+ "files": [{ "path": "commands/cowchat.md", "integrity": "sha256:44dd55…" }] },
+ { "scope": "project", "type": "command", "name": "cowsay",
+ "files": [{ "path": "commands/cowsay.md", "integrity": "sha256:55ee66…" }] }
]
}
}
@@ -141,12 +150,13 @@ The full schema — every field, the tagged source kinds, and the exact-version
```
.opencode/
├── skills/cowsay-rules/SKILL.md
+├── skills/cowsay-rules/references/moods.md
├── agents/cowsay.md
├── commands/cowsay.md
└── commands/cowchat.md
```
-Other adapters use their own conventional location (Claude Code under `.claude/`, and so on). These files are `project`-scoped -- the same `scope` you see in the lockfile -- so they belong to the checkout, not your machine. `facet remove` deletes them again, and `facet install` recreates them from the lockfile after a clone.
+A skill's [companion files](/specification/manifest#supplementary-files) land beside its `SKILL.md`, and a skill installs and removes as one atomic bundle. [Archive-only files](/specification/commit#materialize) — a facet's top-level `README.md` or `LICENSE` — ship in the archive but are **never** written into your adapters. Other adapters use their own conventional location (Claude Code under `.claude/`, and so on). These files are `project`-scoped -- the same `scope` you see in the lockfile -- so they belong to the checkout, not your machine. `facet remove` deletes them again — including a skill's owned companions, while preserving any files you added into a skill directory yourself — and `facet install` recreates them from the lockfile after a clone.
## Reinstall after a git clone
diff --git a/docs/guides/troubleshooting.mdx b/docs/guides/troubleshooting.mdx
index d5a2bdc3..bee93b32 100644
--- a/docs/guides/troubleshooting.mdx
+++ b/docs/guides/troubleshooting.mdx
@@ -48,6 +48,18 @@ Common errors, what causes them, and how to fix them. Each entry mirrors the CLI
Reinstalling selects the newest release compatible with this CLI. See [compatible resolution](/cli/adapters/install#compatible-resolution) for how selection works, and note that [`facet adapter remove`](/cli/adapters/remove) always works regardless of compatibility.
+
+ **Cause:** the facet's archive uses a [`facetVersion`](/specification/archive) your CLI doesn't support. A newer facet may use a format published after your CLI; a corrupt or unknown archive fails the same way. The adapter API, archive `facetVersion`, and lockfile version are [independent axes](/specification/install) — this is the archive axis.
+
+ **Fix:** update the CLI with [`facet self-update`](/cli/self-update). The error names the minimum supporting release when the format is known; when it isn't, update to the latest.
+
+
+
+ **Cause:** a materialized file no longer matches the [per-file integrity](/specification/lockfile) recorded in `facets.lock` — a file was edited or corrupted on disk, or the cache was tampered with. Install reconciles every file (including each skill companion) before writing and reports the exact drifting path.
+
+ **Fix:** re-run [`facet install`](/cli/install) — it repairs a drifted file in place (reported as *repaired*) without touching files you added yourself. If a registry facet fails to reconcile against its lockfile integrity, the download is rejected rather than silently replaced; confirm the lockfile matches what you expect.
+
+
**Cause:** `facet publish` (and `facet whoami`) need a registry credential and none was found.
diff --git a/docs/snippets/asset-descriptor.mdx b/docs/snippets/asset-descriptor.mdx
index ec071601..84bc2bb1 100644
--- a/docs/snippets/asset-descriptor.mdx
+++ b/docs/snippets/asset-descriptor.mdx
@@ -9,6 +9,15 @@ export const AssetDescriptor = ({ assetType, promptPath }) => (
produce a warning; invalid metadata for an installed adapter is a build
error.
+ {assetType === "skill" && (
+
+ Companion files shipped inside the skill directory, as exact paths
+ relative to skills/<name>/ (e.g.{" "}
+ references/api.md). They install and remove atomically with
+ the skill and cannot list SKILL.md. See{" "}
+ Supplementary files.
+
+ )}
The {assetType}'s prompt is the content of {promptPath},
resolved by convention at build time — it is not a manifest field.
diff --git a/docs/specification/archive.mdx b/docs/specification/archive.mdx
index c802f402..3c8218c9 100644
--- a/docs/specification/archive.mdx
+++ b/docs/specification/archive.mdx
@@ -5,7 +5,7 @@ tag: .facet
description: The two-layer .facet archive format
---
-The facet archive (`.facet`) is the self-contained artifact a facet travels in: built by [`facet build`](/specification/build) (the sole producer), verified and uploaded by [`facet publish`](/specification/publish), stored by the registry, and downloaded and verified at [install](/specification/commit#verify). It contains the embedded [facet manifest](/specification/manifest) and every declared asset — nothing is fetched at install time.
+The facet archive (`.facet`) is the self-contained artifact a facet travels in: built by [`facet build`](/specification/build) (the sole producer), verified and uploaded by [`facet publish`](/specification/publish), stored by the registry, and downloaded and verified at [install](/specification/commit#verify). It contains the embedded [facet manifest](/specification/manifest), every declared asset, and every declared [supplementary file](/specification/manifest#supplementary-files) — skill companions (files beside a `SKILL.md`) and top-level archive-only files like `README.md` or `LICENSE`. Nothing is fetched at install time.
The `.facet` file is a two-layer container. Both layers are tar archives; only the inner layer is compressed. Its content-hash contract is defined in the [Integrity Model](/specification/integrity).
@@ -14,7 +14,9 @@ The `.facet` file is a two-layer container. Both layers are tar archives; only t
An **uncompressed tar** with exactly two entries:
- The build manifest: `facetVersion` (the archive-format revision this artifact conforms to — currently `0.1`; breaking changes to the archive layout MUST bump it, and consumers key compatibility off this field), `archive` (the inner archive's entry name), `integrity` (the canonical fingerprint, `sha256:` followed by 64 hex characters), and `assets` (a map of inner-archive entry path to `sha256:` per-asset hash).
+ The build manifest: `facetVersion` (the archive-format revision this artifact conforms to; breaking changes to the archive layout bump it, and consumers key compatibility off this field), `archive` (the inner archive's entry name, which MUST be the exact literal `archive.tar.gz`), `integrity` (the canonical fingerprint, `sha256:` followed by 64 hex characters), and `files` (a map of **every** inner-archive entry path — `facet.json`, each primary asset, and each supplementary file — to its `sha256:` per-entry hash).
+
+ The current archive format is `0.2`; consumers also accept the legacy `0.1` format during the compatibility window. The two are dispatched by exact match on `facetVersion`, never by numeric ordering: a `0.2` manifest carries a `files` map, a `0.1` manifest carries the older `assets` map, and neither shape is reinterpreted as the other. The authoritative versions are the `FACET_ARCHIVE_VERSION`, `LEGACY_FACET_ARCHIVE_VERSION`, and `SUPPORTED_FACET_VERSIONS` constants in `@agent-facets/protocol`.
@@ -25,7 +27,7 @@ The outer tar MUST be uncompressed so the build manifest can be read without dec
## Inner archive
-The inner archive decompresses to a **deterministic uncompressed tar** containing the embedded `facet.json` plus every declared asset at its conventional path (`skills//SKILL.md`, `agents/.md`, `commands/.md`). Entries MUST be sorted lexicographically by path.
+The inner archive decompresses to a **deterministic uncompressed tar** containing the embedded `facet.json`, every declared asset at its conventional path (`skills//SKILL.md`, `agents/.md`, `commands/.md`), and every declared supplementary file at its declared path (skill companions under `skills//`, top-level files like `README.md` at the tree root). Entries MUST be sorted lexicographically by path. The complete entry set is derived from the embedded manifest by the shared archive-plan operation, so archive membership stays explicit and reviewable — there is no recursive auto-discovery.
## Determinism
@@ -39,10 +41,10 @@ Verifiers MUST recompute the fingerprint by decompressing the inner archive and
## Content rules
-Beyond hash verification, a verifier MUST enforce three structural rules on the archive's contents. An archive violating any of them MUST be rejected.
+Beyond hash verification, a verifier MUST enforce three structural rules on the archive's contents. An archive violating any of them MUST be rejected. Both tar layers are validated at the raw-header level — before any path-keyed map is built — so duplicate paths, non-regular entries (symlinks, hard links, directories, devices), and unsafe or non-portable names are rejected before they can alias a legitimate entry. Duplicate JSON object members in `facet.json` and `build-manifest.json` are likewise rejected before schema validation.
-1. **Path safety.** Every asset key in the build manifest's `assets` map and every entry name in the inner tar MUST be a safe relative path: no traversal segments (`..`), no absolute paths, no backslashes. A malicious archive that smuggles an unsafe path through either channel is stopped before any content is trusted.
+1. **Path safety.** Every key in the build manifest's `files` map and every entry name in the inner tar MUST be a safe, portable relative path. The grammar forbids traversal (`..`), empty and `.` segments, absolute paths, drive and URL prefixes, backslashes, NUL and control bytes, the portable-reserved characters `< > : " | ? *`, Windows reserved device names (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`, `LPT1`–`LPT9`), and trailing dot or space on any segment. Paths that collide after Unicode normalization or case folding, and any file/directory prefix conflict, are also rejected. A malicious archive that smuggles an unsafe path through either channel is stopped before any content is trusted.
-2. **Declared-vs-present reconciliation, in both directions.** Every inner-tar entry MUST appear in the build manifest's `assets` map, and every `assets` key MUST have a matching inner-tar entry — an undeclared extra entry and a declared-but-missing asset are both rejections. Each entry's recomputed content hash MUST equal the hash the `assets` map records for it.
+2. **Declared-vs-present reconciliation, in both directions.** Every inner-tar entry MUST appear in the build manifest's `files` map, and every `files` key MUST have a matching inner-tar entry — an undeclared extra entry and a declared-but-missing file are both rejections. Each entry's recomputed content hash MUST equal the hash the `files` map records for it. This applies to every entry, asset or not.
-3. **Outer exclusivity.** Every inner-tar entry MUST be derivable from the embedded `facet.json`: the entry set is limited to `facet.json` itself plus the conventional path of each declared asset (`skills//SKILL.md`, `agents/.md`, `commands/.md`). The build manifest is attacker-controlled, so rule 2 alone is insufficient — the embedded facet manifest is the trust root. Without this rule, an archive could carry undeclared files that land on disk at install time.
+3. **Outer exclusivity.** Every inner-tar entry MUST be derivable from the embedded `facet.json`: the entry set is exactly `facet.json` plus the conventional path of each declared asset (`skills//SKILL.md`, `agents/.md`, `commands/.md`) and the declared path of each supplementary file (skill companions in a skill's `files`, top-level entries in the manifest's `files`). The expected set is derived from the embedded manifest, never from the attacker-controlled build manifest, so rule 2 alone is insufficient — the embedded facet manifest is the trust root. Relaxing this rule to admit declared supplementary files preserves the supply-chain guarantee: an archive still cannot carry an undeclared file that lands on disk at install time.
diff --git a/docs/specification/build.mdx b/docs/specification/build.mdx
index 3d84421c..d8efcd56 100644
--- a/docs/specification/build.mdx
+++ b/docs/specification/build.mdx
@@ -11,15 +11,17 @@ description: Produce the canonical .facet archive
1. **Parse and validate the manifest.** Read the facet manifest (`facet.json`) and validate it against the [manifest schema](/specification/manifest). Invalid manifests MUST be rejected with a descriptive error. The manifest's `name` MUST be a valid facet identity; the `version` MUST be a semver string; at least one text asset MUST be declared.
-2. **Resolve prompts.** Read each declared asset file from its conventional path: `skills//SKILL.md` for skills, `agents/.md` for agents, `commands/.md` for commands. Missing files are build errors. YAML front matter in content files is permitted and MUST be preserved verbatim in the archive -- the manifest's `name`, `description`, and any per-adapter extras are merged on top of the author's front matter at install time.
+2. **Resolve prompts.** Read each declared asset file from its conventional path: `skills//SKILL.md` for skills, `agents/.md` for agents, `commands/.md` for commands. Missing files are build errors. Primary asset files MUST NOT contain YAML front matter — the manifest is the single source of asset metadata, and a primary file that carries front matter is a build error. (Authoring tools strip front matter before writing.)
-3. **Validate content.** A declared asset MUST NOT be empty (zero bytes or whitespace only). Two assets within the same type MUST NOT share a name. Compact `facets[]` entries MUST match the `name@version` shape.
+3. **Resolve supplementary files.** Read each declared [supplementary file](/specification/manifest#supplementary-files) as opaque bytes: top-level entries from the manifest's `files` array, and each skill's companions from its `files` array (relative to the skill directory). Supplementary bytes are shipped verbatim — binary and empty files are valid, and front matter is never parsed or stripped. A declared-but-missing file, a path that resolves to a symlink or non-regular file, an [unsafe or non-portable path](/specification/archive#content-rules), or a resolved-path collision is a build error.
-4. **Validate adapter metadata.** For each asset with an `adapters` block, validate the adapter-specific metadata against each installed adapter's schema. Unknown adapters SHOULD produce a warning. Invalid metadata for an installed adapter MUST be a build error.
+4. **Validate content.** A declared primary asset MUST NOT be empty (zero bytes or whitespace only). Asset names MUST be single-segment [Agent Skills names](/specification/manifest#asset-names); skills and commands share one namespace and MUST NOT collide, while agents are separate. Compact `facets[]` entries MUST match the `name@version` shape.
-5. **Assemble the archive.** Compute per-asset SHA-256 hashes. Assemble the deterministic inner tar. Compute the integrity hash over the uncompressed inner tar. Gzip the inner tar (`archive.tar.gz`). Assemble the outer tar containing `build-manifest.json` (recording the integrity hash and the per-asset hash map) plus the gzipped inner archive. Write the result to `dist/-.facet`. The layout, determinism rules, and hash domains are specified in [Facet Archive](/specification/archive).
+5. **Validate adapter metadata.** For each asset with an `adapters` block, validate the adapter-specific metadata against each installed adapter's schema. Unknown adapters SHOULD produce a warning. Invalid metadata for an installed adapter MUST be a build error.
-`facet build` purges `dist/` before writing, so the directory contains exactly one `.facet` after a successful build: the archive for the current source's name and version. For a scoped facet identity, the name's `/` renders as a nested path under `dist/` — `@acme/cowsay` at `1.0.0` is written to `dist/@acme/cowsay-1.0.0.facet`, with parent directories created as required.
+6. **Assemble the archive.** Drive collection and hashing from the shared archive plan derived from the manifest. Compute a per-entry SHA-256 hash for every entry — `facet.json`, primaries, and supplementary files alike. Assemble the deterministic inner tar. Compute the integrity hash over the uncompressed inner tar. Gzip the inner tar (`archive.tar.gz`). Assemble the outer tar containing `build-manifest.json` (recording the integrity hash and the complete `files` hash map) plus the gzipped inner archive. Write the result to `dist/-.facet`. The layout, determinism rules, and hash domains are specified in [Facet Archive](/specification/archive).
+
+Every build emits the current `facetVersion: 0.2` format — including asset-only facets, which carry no supplementary files. **Validation happens before cleanup:** all source inputs (steps 1–5) are validated before `dist/` is touched, so a build that fails validation leaves the previous `dist/` output intact. Only after validation succeeds does `facet build` purge `dist/` and write, so the directory contains exactly one `.facet` after a successful build: the archive for the current source's name and version. For a scoped facet identity, the name's `/` renders as a nested path under `dist/` — `@acme/cowsay` at `1.0.0` is written to `dist/@acme/cowsay-1.0.0.facet`, with parent directories created as required. Build output displays the emitted format version, the complete entry listing, and the integrity hash.
## Boundaries
diff --git a/docs/specification/commit.mdx b/docs/specification/commit.mdx
index a6b286d7..e4df79f2 100644
--- a/docs/specification/commit.mdx
+++ b/docs/specification/commit.mdx
@@ -97,7 +97,7 @@ A cache hit is **never taken at face value**. The self-audit always runs on the
- Recompute the cached content's hashes (per-asset and canonical archive) against the integrity sidecar (`cache-integrity.json`) written at populate time. A failure MUST **evict the slot**; the chain is retried exactly once as a download. Tampered content MUST NOT be materialized.
+ Recompute the cached content's hashes (per-entry and canonical archive) against the integrity sidecar (`cache-integrity.json`) written at populate time. A failure MUST **evict the slot**; the chain is retried exactly once as a download. Tampered content MUST NOT be materialized.
When the lockfile pins this version, the audited integrity MUST equal the locked integrity (string comparison). A mismatch MUST fail the commit — the highest-priority check, never a silent re-download.
@@ -126,7 +126,9 @@ flowchart TD
REG --> PUT["Cache + materialize"]
```
-On a miss, the downloaded content's canonical fingerprint MUST be **genuinely recomputed** from the extracted bytes (per-asset hashes plus the canonical-archive hash) — never taken from the build manifest's self-declared claim. The recomputed value MUST be verified against the registry's published `content_integrity` and, when the lockfile pins this version, against the locked integrity, before the verified content populates the cache.
+On a miss, the downloaded content's canonical fingerprint MUST be **genuinely recomputed** from the extracted bytes (per-entry hashes plus the canonical-archive hash) — never taken from the build manifest's self-declared claim. The recomputed value MUST be verified against the registry's published `content_integrity` and, when the lockfile pins this version, against the locked integrity, before the verified content populates the cache.
+
+Before any file is written, install also proves a four-way agreement per materialized file: the recomputed archive-entry hash, the [lockfile `0.2`](/specification/lockfile) per-file integrity, the verified build-manifest hash, and the complete owned-file set MUST all agree. A mismatch fails the commit with the exact drifting path. This gate — and, in frozen mode, the bidirectional-consistency gate — completes before any receipt-driven cleanup begins. Archive-only supplementary files (like `README.md`) are verified as archive entries but are **never written to disk**.
#### Git and local sources
@@ -138,15 +140,15 @@ Git and local facets are **built from source** during commit rather than downloa
### Materialize
-Materialization writes assets from verified content into each selected adapter's storage. Writes are skip-if-identical — a facet whose lockfile entry is unchanged but whose on-disk assets needed rewriting is reported as *repaired* (the full outcome taxonomy is documented at [`facet install`](/cli/install#outcomes)). Every write MUST journal its inverse operation for rollback.
+Materialization writes assets from verified content into each selected adapter's storage. A skill is written as an **atomic bundle**: its `SKILL.md` and every owned companion file are staged, committed, and rolled back as one all-or-nothing adapter operation carrying the validated owned-file set — the adapter never infers ownership from disk. Writes are skip-if-identical at per-file granularity — a facet whose lockfile entry is unchanged but whose on-disk files needed rewriting is reported as *repaired*, and a single drifted companion is repaired without touching the rest (the full outcome taxonomy is documented at [`facet install`](/cli/install#outcomes)). An interrupted install converges on re-run through the same per-file reconciliation without deleting unowned files. Every write MUST journal its inverse operation for rollback.
## Lockfile shape
-Commit writes `facets.lock` as part of the tri-write: per facet, the tagged source provenance, the exact resolved version, the verified integrity, and the contributed asset tuples. The full schema is specified in [Lockfile](/specification/lockfile).
+Commit writes `facets.lock` as part of the tri-write: per facet, the tagged source provenance, the exact resolved version, the verified integrity, and the contributed assets — each asset carrying a per-file `{ path, integrity }` ownership record. The full schema is specified in [Lockfile](/specification/lockfile).
## Machine-local install receipt
-Receipts are per-project, machine-local records stored outside the project's version-controlled tree ([`$FACET_DIR`](/cli/env)`/receipts/`). They track what has been materialized so drift removal can clean up correctly — even when the lockfile no longer mentions the facet.
+Receipts are per-project, machine-local records (schema `0.2`) stored outside the project's version-controlled tree ([`$FACET_DIR`](/cli/env)`/receipts/`). They mirror the committed lockfile's asset and per-file ownership so drift removal can clean up correctly — including a skill's owned companions — even when the lockfile no longer mentions the facet, and entirely offline. A legacy primary-only receipt is safely refined rather than rejected. The receipt records ownership, not hashes.
@@ -156,16 +158,16 @@ Receipts are per-project, machine-local records stored outside the project's ver
The receipt embeds the canonical path. A mismatch on load MUST fail closed — treated as absent and re-bootstrapped from the lockfile.
- Asset entries with crafted names (path traversal, backslashes) are **reported and skipped individually** — the rest of the receipt still loads and is processed.
+ Asset entries and per-file ownership paths with crafted names (path traversal, backslashes) are **reported and skipped individually** — each owned path is containment-validated before use, and the rest of the receipt still loads and is processed.
- Deletion goes through adapters using validated **asset tuples** — `(scope, type, name)` triples, the adapter-agnostic identity of a materialized asset — never raw filesystem paths taken from the receipt.
+ Deletion goes through adapters using validated **asset tuples** — `(scope, type, name)` triples, the adapter-agnostic identity of a materialized asset — never raw filesystem paths taken from the receipt. For a skill, the delete request also carries the validated owned-companion path set, so only owned files are removed, unowned files survive, and emptied directories are pruned.
## Drift removal
-After the loop, the receipt is compared against the desired set. Any facet the receipt shows as installed — but that is no longer wanted — has its assets deleted. The comparison runs against the **receipt**, not the on-disk lockfile; this is what makes **orphan-on-pull** recoverable (a facet whose lockfile entry vanished because a teammate removed it and you pulled — the receipt still remembers the local materialization). Lockfile entries missing from a freshly-bootstrapped receipt are caught too. And because the asset tuples to delete come from the receipt itself, removal needs no cache and no network.
+After the loop, the receipt is compared against the desired set. Any facet the receipt shows as installed — but that is no longer wanted — has its assets deleted, including each skill's owned companion files. The comparison runs against the **receipt**, not the on-disk lockfile; this is what makes **orphan-on-pull** recoverable (a facet whose lockfile entry vanished because a teammate removed it and you pulled — the receipt still remembers the local materialization). Lockfile entries missing from a freshly-bootstrapped receipt are caught too. And because the asset tuples and owned-file sets to delete come from the receipt itself, removal needs no cache and no network.
Drift removal runs **before** the tri-write, but its deletions MUST be journaled like every other write — if the commit fails afterward, rollback restores them.
diff --git a/docs/specification/install.mdx b/docs/specification/install.mdx
index 4ec35068..fa6c1138 100644
--- a/docs/specification/install.mdx
+++ b/docs/specification/install.mdx
@@ -12,8 +12,16 @@ Installation is a single pipeline with two phases. **[Planning](/specification/p
Before the pipeline runs, **every installed adapter's** declared adapter API MUST be inspected against the CLI's supported set. An incompatible or broken installed adapter MUST fail the operation before planning begins — before any adapter method is invoked and before any project write — with per-adapter diagnostics and the best available reinstall command for each entry. The commit phase then re-checks the adapters selected for the operation as defense-in-depth: the same incompatibility MUST fail at [Load state and gate](/specification/commit), before the per-facet loop and therefore before any Git or local facet build can invoke adapter metadata methods.
+The [adapter API](/guides/custom-adapters), the [archive `facetVersion`](/specification/archive), and the [lockfile version](/specification/lockfile) are three independent version axes, each classified separately. The adapter-compatibility preflight runs **before** archive-version dispatch, so a positional `0.0` adapter fails on the adapter axis — with reinstall guidance — before a facet's `facetVersion` is even examined.
+
Project state MUST NOT be written unless every check passes: a failure anywhere in commit MUST roll back all materialization via the journal and leave the project exactly as it was.
+What install materializes — and what it does not — is a first-class part of the contract:
+
+- **Materialization boundary.** Skill companion files materialize atomically with their skill; every other supplementary file (top-level `README.md`, `LICENSE`, extras beside agents or commands) ships in the archive but is **never written to disk**. Detail in [Commit — Materialize](/specification/commit#materialize).
+- **Per-file integrity and drift.** Install reconciles each materialized file against its [lockfile `0.2`](/specification/lockfile) integrity before writing and reports the exact drifting path; a single drifted companion is repaired without touching the rest.
+- **Unsupported archive versions.** An archive whose `facetVersion` the CLI does not support fails with a structured error the CLI renders as upgrade guidance — a known newer format names the minimum supporting release, an unknown format says update to the latest.
+
Phase 1 — build the delta. No version resolution, no lockfile reads, no project mutation.
diff --git a/docs/specification/integrity.mdx b/docs/specification/integrity.mdx
index cfd5a26b..f135cf4f 100644
--- a/docs/specification/integrity.mdx
+++ b/docs/specification/integrity.mdx
@@ -20,7 +20,7 @@ Facet archives -- anything published to the facets registry -- participate in th
The registry publishes **two hashes** for each version, serving different purposes:
-- **`content_integrity`** (canonical fingerprint) -- SHA-256 of the canonical uncompressed inner tar archive. This is the domain the lockfile, cache sidecar, and build manifest all record. It is the trust anchor for integrity confirmation and lockfile comparison.
+- **`content_integrity`** (canonical fingerprint) -- SHA-256 of the canonical uncompressed inner tar archive. This is the domain the lockfile, cache sidecar, and build manifest all record. It is the trust anchor for integrity confirmation and lockfile comparison. Alongside it, the build manifest records a **per-entry hash for every inner-tar entry** — `facet.json`, each primary asset, and each [supplementary file](/specification/manifest#supplementary-files) — and verification recomputes every one. Integrity therefore covers every file the archive ships, asset or not, including archive-only files like `README.md`.
- **`content_hash`** (transport hash) -- SHA-256 of the uploaded `.facet` tarball (the gzipped delivery bytes). Used only at download time for a raw-bytes transport check. Never persisted to the lockfile.
These are not interchangeable: gzip is a delivery concern outside the hash contract. The canonical fingerprint cannot be recomputed from the transport bytes without decompressing first.
@@ -39,16 +39,16 @@ These are not interchangeable: gzip is a delivery concern outside the hash contr
The transport hash (`content_hash`) MUST be verified against the raw downloaded bytes.
- The canonical fingerprint MUST be genuinely recomputed from the extracted content (per-asset hashes + canonical-archive hash) — never taken from the build manifest's self-declared claim — and MUST match both the manifest's claim and the registry's published `content_integrity`.
+ The canonical fingerprint MUST be genuinely recomputed from the extracted content (per-entry hashes + canonical-archive hash) — never taken from the build manifest's self-declared claim — and MUST match both the manifest's claim and the registry's published `content_integrity`. An unsupported `facetVersion` produces a structured failure carrying the observed and supported versions, which the CLI renders as upgrade guidance.
- The canonical fingerprint (`content_integrity`) and per-asset hashes are written to a sidecar alongside the cached content.
+ The canonical fingerprint (`content_integrity`) and per-entry hashes are written to a sidecar alongside the cached content.
The cached content MUST be re-hashed against its sidecar (self-audit). Tampered content MUST be evicted and re-fetched.
- When the lockfile pins a version, the audited integrity MUST equal the locked integrity.
+ When the lockfile pins a version, the audited integrity MUST equal the locked integrity. Before any file is written, install also reconciles the recomputed archive-entry hash against each [lockfile `0.2` per-file record](/specification/lockfile) and the verified build-manifest hash; a mismatch fails with the exact drifting path.
When a lockfile entry is being created or replaced, the audited integrity MUST match the registry's published `content_integrity`. An unreachable registry MUST fail the operation.
@@ -71,15 +71,15 @@ Two variants sit outside this lifecycle: **git installs** MUST verify built cont
- Canonical fingerprint (`content_integrity`) per facet.
+ Canonical fingerprint (`content_integrity`) per facet, plus a per-materialized-file `{ path, integrity }` record inside each asset entry.
- Canonical fingerprint + per-asset hash map (`cache-integrity.json`) per cache slot. Written at cache-populate time; re-verified on every cache hit.
+ Canonical fingerprint + per-entry hash map (`cache-integrity.json`) per cache slot. Written at cache-populate time; re-verified on every cache hit.
- Canonical fingerprint + per-asset hash map. Embedded in the `.facet` archive.
+ Canonical fingerprint + complete `files` hash map (one entry per inner-tar file). Embedded in the `.facet` archive.
- Asset tuples per materialized facet (not hashes — used for drift removal).
+ Per-facet asset and owned-file ownership records (not hashes — used for offline drift removal).
diff --git a/docs/specification/lockfile.mdx b/docs/specification/lockfile.mdx
index 25815a12..62fffac0 100644
--- a/docs/specification/lockfile.mdx
+++ b/docs/specification/lockfile.mdx
@@ -5,7 +5,7 @@ tag: facets.lock
description: The facets.lock schema and entry semantics
---
-The lockfile (`facets.lock`) records resolved installation state: for every declared facet, the exact version, the verified integrity, the provenance it was resolved from, and the assets it contributed. It is the **resolution** counterpart to the [project manifest](/specification/project-manifest)'s **intent** — the manifest records what the user asked for, the lockfile records what that resolved to.
+The lockfile (`facets.lock`) records resolved installation state: for every declared facet, the exact version, the verified integrity, the provenance it was resolved from, and the assets it contributed — each with a per-file integrity record for every materialized file. It is the **resolution** counterpart to the [project manifest](/specification/project-manifest)'s **intent** — the manifest records what the user asked for, the lockfile records what that resolved to.
Both files are version-controlled. The lockfile is written **only** by the install pipeline, as part of the [tri-write](/specification/commit#transactional-tri-write) — never ahead of success, and never by hand. A hand-edited or merge-conflicted lockfile fails validation at load rather than surfacing as a failure deep inside an install.
@@ -13,14 +13,21 @@ Both files are version-controlled. The lockfile is written **only** by the insta
```json expandable facets.lock
{
- "lockfileVersion": 1,
+ "lockfileVersion": 0.2,
"facets": {
"cowsay": {
"source": { "kind": "registry", "registry": "https://api.agentfacets.io" },
"version": "1.2.0",
"integrity": "sha256:a1b2c3…",
"assets": [
- { "scope": "project", "type": "command", "name": "cowsay" }
+ {
+ "scope": "project",
+ "type": "command",
+ "name": "cowsay",
+ "files": [
+ { "path": "commands/cowsay.md", "integrity": "sha256:11aa22…" }
+ ]
+ }
]
},
"viper-plans": {
@@ -28,7 +35,15 @@ Both files are version-controlled. The lockfile is written **only** by the insta
"version": "2.0.0",
"integrity": "sha256:d4e5f6…",
"assets": [
- { "scope": "project", "type": "skill", "name": "viper-planning" }
+ {
+ "scope": "project",
+ "type": "skill",
+ "name": "viper-planning",
+ "files": [
+ { "path": "skills/viper-planning/SKILL.md", "integrity": "sha256:33cc44…" },
+ { "path": "skills/viper-planning/references/api.md", "integrity": "sha256:55ee66…" }
+ ]
+ }
]
}
}
@@ -38,7 +53,11 @@ Both files are version-controlled. The lockfile is written **only** by the insta
## Fields
- The lockfile schema version, currently `1`. Breaking shape changes MUST bump it; migrations key off this field. An implementation encountering a `lockfileVersion` greater than it supports MUST fail the load rather than guess.
+ The lockfile schema version. The current schema is `0.2`; the numeric `1` names the earlier legacy-alpha schema. Versions are dispatched by **exact match**, never by numeric ordering (`0.2` is numerically less than `1`, so ordering would be wrong): a loader recognizes exactly the versions it supports and fails on any other rather than guessing. A normal install MAY migrate a verified legacy `1` lockfile to `0.2`; a [frozen install](/specification/commit#frozen-lockfile) never rewrites it, and a `0.2` archive requires a `0.2` lockfile in frozen mode. The authoritative version is the `CURRENT_LOCKFILE_VERSION` constant in `@agent-facets/protocol`.
+
+
+ Before the future stable lockfile v1 (which reuses the numeric `1`), legacy-alpha-`1` parsing is removed and replaced with delete-and-regenerate guidance for old-shape files.
+
@@ -64,12 +83,14 @@ Both files are version-controlled. The lockfile is written **only** by the insta
- The assets this facet contributed, as `{ scope, type, name }` tuples (`scope`: `system` | `user` | `project`; `type`: `skill` | `agent` | `command`). Names MUST pass asset-name validation — a crafted lockfile cannot smuggle path traversal into adapter I/O. Adapter-agnostic by design: the same asset set is applied to every selected adapter, and no per-adapter fields exist here.
+ The assets this facet contributed. Each entry is `{ scope, type, name, files }` (`scope`: `system` | `user` | `project`; `type`: `skill` | `agent` | `command`). Names MUST pass asset-name validation — a crafted lockfile cannot smuggle path traversal into adapter I/O. Adapter-agnostic by design: the same asset set is applied to every selected adapter, and no per-adapter fields exist here.
+
+ The `files` array records every **materialized** file inside the asset as a deterministically-sorted `{ path, integrity }` record: a skill lists its `SKILL.md` plus each companion; an agent or command lists its single primary file. The integrity is the recomputed [canonical per-entry hash](/specification/integrity#two-hashes-two-domains) of the verified file — never copied from a self-declared value. Archive-only supplementary files (like `README.md`) are **not** materialized and therefore never appear in any asset's `files`; they stay protected by facet-level integrity. Install reconciles these per-file records against recomputed archive hashes before writing and fails with the exact mismatching path.
## Role in the pipeline
- **Trust anchor.** When the lockfile pins a version, the audited content MUST hash to the locked integrity — a mismatch is a hard failure, never a silent re-download. → [Commit — Verify](/specification/commit#verify)
- **Lockfile trust for resolution.** Whether a satisfying entry short-circuits version resolution depends on how the facet was requested (addition vs reproduction). → [Commit — Resolve](/specification/commit#resolve)
-- **Drift-proof deletion.** The entry's `assets` array is the OLD asset set; the freshly extracted build manifest is the NEW set; the difference is deleted with no separate bookkeeping. → [Drift removal](/specification/commit#drift-removal)
+- **Drift-proof deletion.** The entry's `assets` array — with its per-file `files` records — is the OLD ownership set; the freshly extracted build manifest is the NEW set; the difference is deleted with no separate bookkeeping. Deletion supplies the validated owned-file set to the adapter, so a skill's companions are removed with it while unowned files are preserved. → [Drift removal](/specification/commit#drift-removal)
- **Frozen mode.** With `--frozen-lockfile`, the lockfile is the complete, authoritative source of truth and is never written. → [Frozen lockfile](/specification/commit#frozen-lockfile)
diff --git a/docs/specification/manifest.mdx b/docs/specification/manifest.mdx
index 85d37aec..65f59be9 100644
--- a/docs/specification/manifest.mdx
+++ b/docs/specification/manifest.mdx
@@ -7,7 +7,7 @@ description: The facet.json schema and name grammar
import { AssetDescriptor } from "/snippets/asset-descriptor.mdx";
-The facet manifest (`facet.json`) is the source of truth for a facet's identity and the text assets it contains. This page defines every field in the manifest schema and is the canonical reference for the facet name grammar.
+The facet manifest (`facet.json`) is the source of truth for a facet's identity, the text assets it contains, and the [supplementary files](#supplementary-files) it ships. This page defines every field in the manifest schema and is the canonical reference for both the facet name grammar and the asset name grammar.
The facet manifest is distinct from the [project manifest](/specification/project-manifest) (`facets.json`) — the file in a consuming project that declares which facets are installed. Throughout this page, "the manifest" means the facet manifest.
@@ -52,7 +52,7 @@ Every top-level field of `facet.json`. The `name` and `version` fields MUST be p
### Fields
- Facet identity: an unscoped name (`cowsay`) or a scoped name (`@scope/name`). Validated, never normalized — see [Facet name grammar](#facet-name-grammar). Asset names — skill, command, and agent names alike — are validated independently against the [Agent Skills name grammar](https://agentskills.io/specification#name-field): each `/`-separated segment is 1–64 characters of lowercase ASCII letters, digits, and hyphens, must not start or end with a hyphen, and must not contain consecutive hyphens. Slashes separate namespace segments (`viper-plans/planning`); asset names are never scoped with `@`.
+ Facet identity: an unscoped name (`cowsay`) or a scoped name (`@scope/name`). Validated, never normalized — see [Facet name grammar](#facet-name-grammar). This is the facet's own name; the names of the assets it contains follow the separate [asset name grammar](#asset-names).
@@ -72,7 +72,7 @@ Every top-level field of `facet.json`. The `name` and `version` fields MUST be p
- Skill name → skill descriptor.
+ Skill name → skill descriptor. A skill descriptor MAY declare a `files` array of [companion files](#supplementary-files) shipped inside the skill directory.
@@ -92,6 +92,10 @@ Every top-level field of `facet.json`. The `name` and `version` fields MUST be p
+
+ Top-level [supplementary files](#supplementary-files): exact repo-relative paths to non-asset files that ship in the archive but never materialize (e.g. `README.md`, `LICENSE`, `docs/design.md`). Exact paths only — no globs or patterns. Every path MUST satisfy the [portable path grammar](/specification/archive#content-rules) and MUST NOT resolve under `skills/` (skill companions are declared in their skill's own `files`).
+
+
References to other facets (`name@version` strings or selective entries). Schema-validated only: build does not resolve the references, and [`facet add`](/cli/add) rejects sources that declare a non-empty `facets` array.
@@ -143,6 +147,18 @@ Names are validated, never normalized. Uppercase letters, non-ASCII characters,
The following scoped shapes are rejected: a bare scope (`@scope`), a missing scope (`@/name`), a missing name (`@scope/`), extra path depth (`@scope/name/extra`), and the legacy un-prefixed form (`scope/name`).
+### Asset names
+
+Asset names — skill, command, and agent names — follow the [Agent Skills name grammar](https://agentskills.io/specification#name-field), interpreted by Facets as a **single ASCII segment**: 1–64 characters of lowercase ASCII letters, digits, and hyphens, not starting or ending with a hyphen and with no consecutive hyphens. Unlike the earlier format, a current-format asset name is a single segment: it MUST NOT contain `/`, so slash-namespaced names like `viper-plans/planning` are no longer valid (use `viper-planning`).
+
+Skills and commands share **one namespace**: within a facet, a skill and a command MUST NOT use the same name. Agents are a separate namespace and may reuse a skill or command name.
+
+A companion file's path may contain `/` for directory depth (`references/api.md`), but those separators are part of the file path, not the skill name — the skill name itself is still a single segment.
+
+
+Legacy `0.1` archives retain their earlier multi-segment naming during the compatibility window. New manifests use the single-segment grammar above.
+
+
### Constraints
1. The `name` MUST be a valid facet identity (see [Facet name grammar](#facet-name-grammar)), and the `version` MUST be a semver string.
@@ -156,10 +172,27 @@ The following scoped shapes are rejected: a bare scope (`@scope`), a missing sco
A facet's content is carried by three text asset types: skills, agents, and commands. Each asset is declared in the manifest under its type's map, and its name maps to a file at a conventional path in the source tree. The file's content is the asset's prompt — it is resolved by convention at [build time](/specification/build#steps), never referenced by a manifest field.
-- **Skill** — `skills//SKILL.md`. Each skill lives in its own directory, per the [Agent Skills](https://agentskills.io/specification) convention; the main file MUST be named `SKILL.md`.
+- **Skill** — `skills//SKILL.md`. Each skill lives in its own directory, per the [Agent Skills](https://agentskills.io/specification) convention; the main file MUST be named `SKILL.md`. A skill directory MAY also contain declared [companion files](#supplementary-files) at any safe depth (`references/api.md`, `scripts/run.ts`).
- **Agent** — `agents/.md`. A single markdown file whose content is the agent's system prompt.
- **Command** — `commands/.md`. A single markdown file whose content is the command's prompt.
-YAML front matter in an asset file is optional. When present it MUST be preserved verbatim through the [build](/specification/build#steps); the manifest's `name`, `description`, and any per-adapter extras are merged on top of the author's front matter at install time.
+A primary asset file (a `SKILL.md`, agent, or command file) MUST NOT contain YAML front matter — the manifest is the single source of asset metadata. Authoring tools strip front matter before writing, and [`facet build`](/specification/build#steps) rejects a primary file that still carries it.
An asset descriptor's `adapters` block is validated at [build time](/specification/build#steps) against each installed adapter's schema: unknown adapters produce a warning, and invalid metadata for an installed adapter is a build error.
+
+## Supplementary files
+
+Beyond its assets, a facet can declare **supplementary files** — non-asset files that ship inside the `.facet` archive and are integrity-protected like everything else, but are not independently installable assets. They have no asset type, no adapter metadata, and no lockfile asset tuple.
+
+There are two declaration sites, and every supplementary archive entry MUST be derivable from one of them (there is no auto-discovery):
+
+- **Top-level `files`** — repo-relative paths at the tree root, declared in the manifest's [`files`](#fields) array. Used for `README.md`, `LICENSE`, development notes, and other project files. These ship but **never materialize**: they are not written to disk at install time.
+- **Per-skill `files`** — paths relative to a skill's directory, declared in that [skill descriptor's](#fields) `files` array. These are the skill's **companion files** (references, scripts, templates). A companion materializes atomically with its owning skill and cannot list `SKILL.md` itself.
+
+Supplementary files are declared as **exact paths only** — no globs or patterns — and each path MUST satisfy the [portable path grammar](/specification/archive#content-rules). A top-level path MUST NOT resolve under `skills/`, so a skill's files have exactly one declaration site. Supplementary bytes are shipped verbatim: binary and empty files are valid, and front matter is never parsed or stripped.
+
+`README.md` and the extensionless `README` are ordinary top-level `files` declarations — there is no README-specific manifest field. Authoring support for them is described in [Create Your First Facet](/guides/create-your-first-facet) and [`facet edit`](/cli/authoring/edit).
+
+
+A CLI older than the first `0.2`-producing release tolerates the `files` fields but omits their bytes when it builds, producing a declared-but-empty facet. Build with a current release so declared files actually ship.
+
diff --git a/docs/specification/publish.mdx b/docs/specification/publish.mdx
index 193b420d..bd3a8229 100644
--- a/docs/specification/publish.mdx
+++ b/docs/specification/publish.mdx
@@ -15,7 +15,7 @@ Errors returned by the registry MUST be rendered verbatim -- implementations sh
## What the author uploads
-The CLI uploads the complete built `.facet` archive -- the same self-contained two-layer artifact `facet build` produced. The outer tar contains the `build-manifest.json` (recording the integrity hash and per-asset hashes) and the gzipped inner archive (carrying the embedded `facet.json` and every declared asset). No part of the archive is re-derived at upload time: the bytes on disk are the bytes on the wire.
+The CLI uploads the complete built `.facet` archive -- the same self-contained two-layer artifact `facet build` produced. The outer tar contains the `build-manifest.json` (recording the integrity hash and the complete per-entry [`files`](/specification/archive) hash map) and the gzipped inner archive (carrying the embedded `facet.json`, every declared asset, and every declared supplementary file). No part of the archive is re-derived at upload time: the bytes on disk are the bytes on the wire.
The `name` and `version` used to address the upload come from the verified artifact's embedded manifest, not from a separate parse of the source-tree `facet.json`. This matters when the user explicitly chooses to ship a drifted artifact under its own embedded identity (see [identity drift](#when-the-built-artifact-has-drifted-from-source)).
@@ -23,7 +23,7 @@ The manifest's [`private`](/specification/manifest#privacy) declaration is part
## What the registry does
-1. **Verify the upload.** The registry runs the same archive-verification operation `facet publish` ran locally: parse the outer container, decompress the inner archive (within the registry's size policy), verify the integrity hash, verify each per-asset hash, validate the embedded manifest, and apply the [artifact content rules](/specification/archive#content-rules). A verification failure rejects the publish.
+1. **Verify the upload.** The registry runs the same archive-verification operation `facet publish` ran locally: dispatch on the archive `facetVersion` (accepting both the current `0.2` and legacy `0.1` during the compatibility window), parse the outer container, decompress the inner archive (within the registry's size policy), verify the integrity hash, verify each per-entry hash, validate the embedded manifest, and apply the [artifact content rules](/specification/archive#content-rules) — including the expanded supplementary-file membership. A verification failure rejects the publish. The registry is a separate implementation of this specification and adopts the same relaxed rules.
2. **Store the artifact.** The verified bytes are stored under `(name, version)`. The registry records both the canonical fingerprint (`content_integrity`) and the transport hash (`content_hash`). Consumers verify both at different stages (see [Integrity Model](/specification/integrity)).
@@ -68,7 +68,7 @@ The prompt choreography for both drift classes is documented at [`facet publish`
| Reads | Source tree (`facet.json` + asset files) | Built archive in `dist/` |
| Produces | `dist/-.facet` | No on-disk output |
| Network | None | One POST to the registry |
-| Verification | Build-time validators (schema, content rules, collisions) | Full archive verification (integrity hash, per-asset hashes, embedded manifest, content rules) |
+| Verification | Build-time validators (schema, content rules, collisions) | Full archive verification (integrity hash, per-entry hashes, embedded manifest, content rules) |
Neither step mutates the manifest. An invalid input fails the respective step — publish fails with a verification error and never contacts the registry.
diff --git a/docs/specification/terminology.mdx b/docs/specification/terminology.mdx
index 8a3e4d08..203c1bb9 100644
--- a/docs/specification/terminology.mdx
+++ b/docs/specification/terminology.mdx
@@ -32,7 +32,7 @@ Canonical terms used throughout the specification. Implementations SHOULD use th
- A directory asset following the Agent Skills format.
`skills//SKILL.md`
+ A directory asset following the Agent Skills format; a `SKILL.md` plus optional companion files.
`skills//SKILL.md`
A single-file agent definition.
`agents/.md`
@@ -44,9 +44,13 @@ Canonical terms used throughout the specification. Implementations SHOULD use th
| Term | Definition | Reference |
| --- | --- | --- |
-| **Asset tuple** | A `(scope, type, name)` triple — the adapter-agnostic identity of a materialized asset. | [Install receipt](/specification/commit#machine-local-install-receipt) |
+| **Asset tuple** | A `(scope, type, name)` triple — the adapter-agnostic identity of a materialized asset. Skill ownership also carries a validated owned-companion path set. | [Install receipt](/specification/commit#machine-local-install-receipt) |
| **Managed asset** | Installed by a facet; tracked in the lockfile and install receipt. | [`facet edit`](/cli/authoring/edit) |
| **Unmanaged asset** | In an adapter directory but not connected to any facet: user-created, or kept from an uninstalled facet. | [`facet edit`](/cli/authoring/edit) |
+| **Supplementary file** | A manifest-declared non-asset file shipped in the archive and integrity-protected, but never an independently installable asset. | [Supplementary files](/specification/manifest#supplementary-files) |
+| **Companion file** | A per-skill supplementary file that materializes atomically with its owning skill. | [Supplementary files](/specification/manifest#supplementary-files) |
+| **Archive-only file** | A top-level supplementary file (e.g. `README.md`) that ships in the archive but is never written to disk at install. | [Materialize](/specification/commit#materialize) |
+| **Owned companion path set** | The engine-supplied, containment-validated set of a skill's companion paths handed to the adapter for install, read, and delete. | [Materialize](/specification/commit#materialize) |
## Identity & versioning
@@ -63,8 +67,9 @@ Canonical terms used throughout the specification. Implementations SHOULD use th
| --- | --- | --- |
| **Canonical fingerprint** | `content_integrity` — SHA-256 of the uncompressed inner tar; the trust anchor recorded in the lockfile, cache sidecar, and build manifest. | [Two hashes, two domains](/specification/integrity#two-hashes-two-domains) |
| **Transport hash** | `content_hash` — SHA-256 of the uploaded `.facet` tarball; a download-time transit check only, never persisted. | [Two hashes, two domains](/specification/integrity#two-hashes-two-domains) |
-| **Build manifest** | `build-manifest.json`, embedded in the archive's outer layer; the integrity claim and per-asset hash map. | [Archive format](/specification/archive) |
-| **Cache sidecar** | `cache-integrity.json`, stored alongside cached content; the canonical fingerprint plus per-asset hashes. | [Where hashes live](/specification/integrity#where-hashes-live) |
+| **Build manifest** | `build-manifest.json`, embedded in the archive's outer layer; the integrity claim and the complete per-entry `files` hash map. | [Archive format](/specification/archive) |
+| **Cache sidecar** | `cache-integrity.json`, stored alongside cached content; the canonical fingerprint plus per-entry hashes. | [Where hashes live](/specification/integrity#where-hashes-live) |
+| **Adapter API version** | The identifier for the adapter contract shape (currently `0.1`); classified by exact match, independent of the archive and lockfile versions. | [Custom adapters](/guides/custom-adapters) |
| **Cache self-audit** | Re-verification of cached content against its sidecar on every materialization; a mismatch evicts the slot. | [Commit — Verify](/specification/commit#verify) |
| **Integrity confirmation** | The registry metadata check required whenever a lockfile entry is created or replaced; fails offline. | [Registry interactions](/specification/commit#registry-interactions) |
| **One-check reproduction guard** | The git-source defense: built content must hash to the locked integrity (the tag-move defense). | [Git and local sources](/specification/commit#git-and-local-sources) |
@@ -83,11 +88,11 @@ Canonical terms used throughout the specification. Implementations SHOULD use th
| **Install lock** | The per-project advisory lock ensuring one install at a time. | [Sequence](/specification/commit#sequence) |
| **Journal** | The log in which every materialization write records its inverse operation, replayed in reverse on failure. | [Commit](/specification/commit) |
| **Materialization** | Writing assets from verified content into each selected adapter's storage. | [Materialize](/specification/commit#materialize) |
-| **Install receipt** | The machine-local, per-project record under `$FACET_DIR/receipts/` tracking what this machine has materialized. | [Install receipt](/specification/commit#machine-local-install-receipt) |
+| **Install receipt** | The machine-local, per-project record (schema `0.2`) under `$FACET_DIR/receipts/` mirroring committed asset and per-file ownership for offline removal. | [Install receipt](/specification/commit#machine-local-install-receipt) |
| **Drift removal** | Deleting assets of facets no longer wanted, computed from the receipt, entirely offline. | [Drift removal](/specification/commit#drift-removal) |
| **Orphan-on-pull** | The recoverable state where a teammate's removal reaches you via `git pull`; the receipt still remembers. | [Drift removal](/specification/commit#drift-removal) |
| **Tri-write** | The atomic commit of `facets.json`, `facets.lock`, and the receipt together; failure leaves all three unchanged. | [Transactional tri-write](/specification/commit#transactional-tri-write) |
-| **Lockfile** | `facets.lock` — resolved installation state: tagged source, exact version, integrity, and asset tuples per facet. | [Lockfile](/specification/lockfile) |
+| **Lockfile** | `facets.lock` — resolved installation state: tagged source, exact version, integrity, and per-asset entries with per-file `{ path, integrity }` records. | [Lockfile](/specification/lockfile) |
| **Cache** | The machine-local content-addressed store for fetched facet payloads under `$FACET_DIR/cache/`. | [`facet install`](/cli/install#cache) |
## Authoring & publishing
diff --git a/openspec/changes/support-non-asset-files/design.md b/openspec/changes/support-non-asset-files/design.md
index b36128e0..ce64bc0a 100644
--- a/openspec/changes/support-non-asset-files/design.md
+++ b/openspec/changes/support-non-asset-files/design.md
@@ -167,7 +167,7 @@ Before any materialization, install SHALL require exact agreement among:
Any disagreement SHALL return structured failure data containing the facet, asset, canonical path, expected integrity, and actual integrity when available. Frozen mode SHALL fail without rewriting. Normal resolution MAY write a new lock entry only after all checks against the newly resolved artifact succeed.
-Drift checking SHALL operate per locked file. Verbatim companion files are hashed directly from disk. For primary files whose adapter representation differs from archive bytes, the adapter `readAsset` contract SHALL return canonical logical content so the engine can compare the corresponding locked canonical integrity without encoding adapter-specific bytes in `facets.lock`. Because archived primary files contain no YAML front matter (the manifest is the metadata source of truth), the canonical logical content of an undrifted primary equals its archive bytes — adapter-added storage encoding is stripped by `readAsset`, so the locked hash is reproducible offline. Reports SHALL identify the exact locked path that drifted.
+Drift checking SHALL operate per locked file. Verbatim companion files are hashed directly from disk. For primary files whose adapter representation differs from archive bytes, the adapter `readAsset` contract SHALL return canonical logical content so the engine can compare the corresponding locked canonical integrity without encoding adapter-specific bytes in `facets.lock`. Author-supplied front matter in a primary asset is archived verbatim, but the manifest remains the metadata source of truth: materialization merges manifest-owned metadata over any author front matter (manifest wins), and `readAsset` projects the installed primary back to that canonical logical form by stripping adapter-added storage encoding, so the locked hash is reproducible offline. Reports SHALL identify the exact locked path that drifted.
The machine-local receipt SHALL mirror the successfully committed lockfile asset/file ownership set so offline removal and rollback remain exact even after a pulled lockfile drops an entry. Receipt-driven removal supplies that validated ownership set to the adapter delete request (D8), so offline cleanup after a pulled lockfile drops an entry deletes exactly the recorded owned files. The receipt remains adapter-agnostic and stores no adapter-encoded hashes. Receipt and lockfile changes SHALL commit in the same install transaction as materialization; rollback restores all three. Receipts remain untrusted input: identity, path containment, and file-integrity record validation MUST precede deletion, and unowned paths MUST never be deleted. The receipt schema version SHALL become `0.2`; legacy receipt version `1` MAY be refined to primary-only file sets because the legacy system could not install companions.
diff --git a/openspec/changes/support-non-asset-files/specs/authoring__facets/spec.md b/openspec/changes/support-non-asset-files/specs/authoring__facets/spec.md
index 4baff809..834243cb 100644
--- a/openspec/changes/support-non-asset-files/specs/authoring__facets/spec.md
+++ b/openspec/changes/support-non-asset-files/specs/authoring__facets/spec.md
@@ -338,7 +338,7 @@ All fields SHALL remain editable. Exit confirmation SHALL prevent accidental los
### Requirement: Authors can build a facet locally for validation and inspection
-The system SHALL compile a facet project into a deterministic `.facet` archive after validating the manifest and every source input. It SHALL verify that primary asset files exist, are non-empty, contain no YAML front matter, and resolve from their conventional paths. It SHALL verify that every declared supplementary file exists as a regular file at a safe, collision-free path. It SHALL archive the embedded manifest, every primary asset, and every declared supplementary file, and SHALL record a content hash for every entry. Validation SHALL finish before previous `dist/` output is removed. The build SHALL NOT modify source files and SHALL behave identically in interactive and non-interactive environments.
+The system SHALL compile a facet project into a deterministic `.facet` archive after validating the manifest and every source input. It SHALL verify that primary asset files exist, are non-empty, and resolve from their conventional paths. Author-supplied YAML front matter in a primary asset SHALL be permitted and preserved verbatim in the archive; the manifest remains the source of truth for asset metadata, and front matter is reconciled with the manifest at install time rather than rejected at build. It SHALL verify that every declared supplementary file exists as a regular file at a safe, collision-free path. It SHALL archive the embedded manifest, every primary asset, and every declared supplementary file, and SHALL record a content hash for every entry. Validation SHALL finish before previous `dist/` output is removed. The build SHALL NOT modify source files and SHALL behave identically in interactive and non-interactive environments.
For a scoped facet identity or other slash-containing output name, the system SHALL create required parent directories below `dist/`. On success, the system SHALL display pipeline progress, the emitted archive-format version, complete entry listing, and integrity hash, followed by a persistent summary. On failure, it SHALL identify the failed stage and structured field or path errors and SHALL suggest the editing command when appropriate.
@@ -376,10 +376,11 @@ For a scoped facet identity or other slash-containing output name, the system SH
- **WHEN** a declared asset's conventional primary file is missing
- **THEN** the system SHALL identify the asset and expected path and write no new output
-#### Scenario: Build fails on primary asset front matter
+#### Scenario: Primary asset front matter is preserved in the archive
-- **WHEN** a primary asset file contains YAML front matter
-- **THEN** the system SHALL identify that file and require front-matter removal
+- **WHEN** a primary asset file contains author-supplied YAML front matter
+- **THEN** the build SHALL succeed and archive the primary bytes verbatim
+- **AND** the manifest SHALL remain the source of truth for that asset's metadata
#### Scenario: Build fails on empty primary asset
@@ -597,64 +598,25 @@ The system SHALL detect missing conventional asset files and missing declared su
- **WHEN** top-level `files` declares missing `LICENSE`
- **THEN** edit SHALL offer Scaffold at `LICENSE` or Remove Declaration
-### Requirement: Edit parses front matter for defaults and strips it
+### Requirement: Edit confirms asset identity and preserves primary content
-The system SHALL parse YAML front matter only from primary asset files encountered during edit. A parsed `name` or `description` SHALL pre-fill the corresponding asset field; otherwise the conventional filename or skill-directory name SHALL provide the default. The author SHALL confirm every asset's name and description. The final author-confirmed asset name SHALL determine that asset's conventional path on disk. Extra fields SHALL be shown and may be converted to platform configuration or dropped. Converted fields SHALL be placed under a selected known platform or a valid custom kebab-case platform name. Confirmed primary content SHALL be written without front matter. Supplementary files, including README, SHALL NOT be parsed or stripped and SHALL retain exact bytes unless explicitly edited.
+The system SHALL require the author to confirm every asset's name and description during edit, defaulting the name to the conventional filename or skill-directory name. The final author-confirmed asset name SHALL determine that asset's conventional path on disk, and confirmed metadata SHALL be written to the manifest, which remains the source of truth. Edit SHALL NOT strip author-supplied front matter from a primary asset file; primary content SHALL be preserved verbatim unless the author explicitly edits it, and manifest metadata is reconciled with any front matter at install time. Supplementary files, including README, SHALL NOT be parsed and SHALL retain exact bytes unless explicitly edited.
-#### Scenario: Front matter name pre-fills asset name
+#### Scenario: Missing metadata uses conventional name
-- **WHEN** `skills/skill/SKILL.md` contains `name: typescript-best-practices`
-- **THEN** edit SHALL pre-fill that name and require confirmation
+- **WHEN** `skills/code-review/SKILL.md` is reconciled during edit
+- **THEN** edit SHALL default the name to `code-review` and require confirmation
-#### Scenario: Missing front matter name uses conventional name
+#### Scenario: Confirmed metadata is written to the manifest
-- **WHEN** `skills/code-review/SKILL.md` has no front-matter name
-- **THEN** edit SHALL pre-fill `code-review`
+- **WHEN** the author confirms an asset's name and description
+- **THEN** Apply SHALL persist that metadata in the manifest
+- **AND** the confirmed name SHALL determine the conventional file path
-#### Scenario: Front matter description pre-fills description
+#### Scenario: Primary content is preserved verbatim
-- **WHEN** a primary file contains a front-matter description
-- **THEN** edit SHALL pre-fill that value and require confirmation
-
-#### Scenario: Extra front matter fields are surfaced
-
-- **WHEN** a primary file contains fields beyond name and description
-- **THEN** edit SHALL offer conversion to platform configuration or removal
-
-#### Scenario: Extra fields convert to known platform configuration
-
-- **WHEN** the author selects a known platform for extra fields
-- **THEN** Apply SHALL place those fields under that platform key
-
-#### Scenario: Extra fields convert to custom platform configuration
-
-- **WHEN** the author selects valid custom platform `cursor`
-- **THEN** Apply SHALL place those fields under `cursor`
-
-#### Scenario: Invalid custom platform is rejected
-
-- **WHEN** the author enters `My Platform` or `CURSOR`
-- **THEN** edit SHALL reject the custom platform name
-
-#### Scenario: Extra fields may be dropped
-
-- **WHEN** the author chooses Drop
-- **THEN** Apply SHALL discard the extra fields
-
-#### Scenario: Primary front matter is stripped
-
-- **WHEN** the author confirms a primary asset that contained front matter
-- **THEN** the persisted primary file SHALL contain only its markdown body
-
-#### Scenario: Malformed front matter is treated as absent
-
-- **WHEN** primary content resembles front matter but cannot be parsed
-- **THEN** edit SHALL process it as content without parsed defaults
-
-#### Scenario: Existing declared primary with front matter is reconciled
-
-- **WHEN** a declared primary asset contains front matter
-- **THEN** edit SHALL show its values and strip them on confirmation
+- **WHEN** an existing primary asset file contains author-supplied front matter and the author does not edit its body
+- **THEN** Apply SHALL preserve the primary file bytes unchanged
#### Scenario: Supplementary front matter-like bytes are preserved
@@ -683,7 +645,7 @@ All identity, privacy, asset, README, supplementary-file, and manifest changes S
#### Scenario: Confirmation shows all deltas
-- **WHEN** a session includes identity, privacy, asset, README, companion, and front-matter changes
+- **WHEN** a session includes identity, privacy, asset, README, and companion changes
- **THEN** confirmation SHALL list each change and exact affected path
#### Scenario: README and companion changes wait for Apply
@@ -692,24 +654,25 @@ All identity, privacy, asset, README, supplementary-file, and manifest changes S
- **THEN** confirmation SHALL list both exact paths
- **AND** neither disk nor manifest SHALL change before Apply
-### Requirement: Content files contain no front matter
+### Requirement: The manifest is the source of truth for primary asset metadata
-The manifest SHALL remain the single source of truth for primary asset metadata. Primary skill, agent, and command files on disk and in archives SHALL contain pure markdown without YAML front matter. Create, edit, and build SHALL enforce this rule for primary asset files. Supplementary files SHALL be exempt because they are opaque bytes and may contain any content, including front-matter-like text.
+The manifest SHALL remain the single source of truth for primary skill, agent, and command metadata. Author-supplied YAML front matter in a primary asset file SHALL be permitted: build SHALL preserve it verbatim in the archive, and materialization SHALL reconcile it with the manifest by merging manifest-owned metadata on top of any author front matter (the manifest wins on conflicting keys) before writing the asset to a selected adapter. Scaffolded starter files SHALL contain pure markdown with no front matter. Supplementary files SHALL be opaque bytes and may contain any content, including front-matter-like text, without reconciliation.
#### Scenario: Scaffolded primary files have no front matter
- **WHEN** create or edit scaffolds a primary asset file
- **THEN** the file SHALL contain markdown without YAML front matter
-#### Scenario: Build rejects primary front matter
+#### Scenario: Primary front matter is preserved and reconciled at install
-- **WHEN** a primary asset file contains YAML front matter
-- **THEN** the build SHALL fail and identify the file
+- **WHEN** a primary asset file contains author-supplied YAML front matter
+- **THEN** the build SHALL archive those bytes verbatim
+- **AND** materialization SHALL merge manifest-owned metadata on top of the author front matter, with the manifest winning on conflicting keys
-#### Scenario: Archive contains clean primary files
+#### Scenario: Archive preserves primary bytes
- **WHEN** a facet is built
-- **THEN** every primary asset entry SHALL contain markdown without YAML front matter
+- **THEN** every primary asset entry SHALL contain the author's exact source bytes
#### Scenario: Supplementary front matter is allowed
diff --git a/openspec/changes/support-non-asset-files/tasks.md b/openspec/changes/support-non-asset-files/tasks.md
index d4f9e133..c93301ef 100644
--- a/openspec/changes/support-non-asset-files/tasks.md
+++ b/openspec/changes/support-non-asset-files/tasks.md
@@ -134,27 +134,27 @@
## 14. Documentation — Research
-- [ ] 14.1 Explore: Audit `docs/` and root `README.md` for archive versions, hash-map shape, manifest naming/declarations, lockfile/receipt semantics, install behavior, adapter contracts, and asset-only wording
-- [ ] 14.2 Explore: Inspect documentation generation and shared snippets so field descriptions and compatibility values are referenced from authoritative schemas or constants rather than duplicated
-- [ ] 14.3 Explore: Inspect the protocol-only, adapter-only, and held CLI release notes and package metadata so documentation describes the actual staged rollout without making package versions a second source of truth
-- [ ] 14.4 Propose: Define the documentation and generated-reference update set, including compatibility warnings, the custom-adapter contract, and the consumer-first protocol → registry → adapter → CLI release sequence
+- [x] 14.1 Explore: Audit `docs/` and root `README.md` for archive versions, hash-map shape, manifest naming/declarations, lockfile/receipt semantics, install behavior, adapter contracts, and asset-only wording
+- [x] 14.2 Explore: Inspect documentation generation and shared snippets so field descriptions and compatibility values are referenced from authoritative schemas or constants rather than duplicated
+- [x] 14.3 Explore: Inspect the protocol-only, adapter-only, and held CLI release notes and package metadata so documentation describes the actual staged rollout without making package versions a second source of truth
+- [x] 14.4 Propose: Define the documentation and generated-reference update set, including compatibility warnings, the custom-adapter contract, and the consumer-first protocol → registry → adapter → CLI release sequence
## 15. Documentation — Implementation
-- [ ] 15.1 Implement: Update archive, build, manifest, integrity, lockfile, commit, install, publish, and terminology documentation for supplementary membership, strict versions, path safety, per-file hashes, atomic skill bundles, and the protocol-first release boundary
-- [ ] 15.2 Implement: Update create, edit, install, troubleshooting, first-facet, install-facets, skills, and custom-adapter guides plus root `README.md` for the README workflow, materialization boundary, upgrade guidance, non-asset files, and adapter API `0.0`→`0.1` migration
-- [ ] 15.3 Implement: Generate or share schema-derived field references where practical, keep one authoritative minimum-version mapping, and link other documentation to it instead of copying values
-- [ ] 15.4 Implement: Add durable release notes identifying the previously accepted archive, lockfile, naming, and adapter behaviors that become incompatible, and retain the approved protocol delta as the authoritative permanent pre-1.0 breaking-minor/post-1.0 breaking-major policy update to be synced during change finalization
-- [ ] 15.5 Verify: Run documentation checks and verify every compatibility and release-order claim against authoritative schemas, constants, package metadata, and the staged Changesets
+- [x] 15.1 Implement: Update archive, build, manifest, integrity, lockfile, commit, install, publish, and terminology documentation for supplementary membership, strict versions, path safety, per-file hashes, atomic skill bundles, and the protocol-first release boundary
+- [x] 15.2 Implement: Update create, edit, install, troubleshooting, first-facet, install-facets, skills, and custom-adapter guides plus root `README.md` for the README workflow, materialization boundary, upgrade guidance, non-asset files, and adapter API `0.0`→`0.1` migration
+- [x] 15.3 Implement: Generate or share schema-derived field references where practical, keep one authoritative minimum-version mapping, and link other documentation to it instead of copying values
+- [x] 15.4 Implement: Add durable release notes identifying the previously accepted archive, lockfile, naming, and adapter behaviors that become incompatible, and retain the approved protocol delta as the authoritative permanent pre-1.0 breaking-minor/post-1.0 breaking-major policy update to be synced during change finalization
+- [x] 15.5 Verify: Run documentation checks and verify every compatibility and release-order claim against authoritative schemas, constants, package metadata, and the staged Changesets
## 16. Held CLI Release Gate and Final Readiness
-- [ ] 16.1 Explore: Audit the completed implementation, package versions, pending Changesets, generated release notes, and release automation to define a minimal held `agent-facets` activation PR with no unintended protocol or adapter publication
-- [ ] 16.2 Propose: Present the exact CLI-only pre-1.0 minor changeset, activation evidence, PR base/stack placement, and merge conditions; the user retains sole authority to merge the held release gate
-- [ ] 16.3 Implement: Create and submit the tiny held CLI release-gate PR containing the `agent-facets` changeset and final release notes, without merging, publishing, or deploying it
-- [ ] 16.4 Verify: Run strict OpenSpec validation, package API/build checks, and the full `bun check` suite, fixing formatter-only findings with `bun format`, then verify implementation coverage scenario-by-scenario across all seven delta specs
-- [ ] 16.5 Verify: Confirm the protocol-only release from Section 5 is published and exposes strict `0.1`/`0.2` verification, tagged results, structured failures, and cross-version helpers from a clean consumer install
-- [ ] 16.6 Verify: Confirm the adapter SDK and all three first-party adapters are published with `facetAdapterApiVersion: 0.1`, while existing `0.0` CLIs retain compatible `0.0` adapter resolution
-- [ ] 16.7 Verify: Confirm the deployed registry pins the released protocol, accepts valid `0.1` and `0.2`, rejects malformed/unsupported archives before persistence, preserves supplementary hashes, and reads only intended primary resources
-- [ ] 16.8 Verify: Build the unreleased candidate CLI, publish a representative `0.2` archive to the stage registry, and verify metadata, archive download, stored-content behavior, and legacy `0.1` retention end to end
+- [x] 16.1 Explore: Audit the completed implementation, package versions, pending Changesets, generated release notes, and release automation to define a minimal held `agent-facets` activation PR with no unintended protocol or adapter publication
+- [x] 16.2 Propose: Present the exact CLI-only pre-1.0 minor changeset, activation evidence, PR base/stack placement, and merge conditions; the user retains sole authority to merge the held release gate
+- [x] 16.3 Implement: Create and submit the tiny held CLI release-gate PR containing the `agent-facets` changeset and final release notes, without merging, publishing, or deploying it
+- [x] 16.4 Verify: Run strict OpenSpec validation, package API/build checks, and the full `bun check` suite, fixing formatter-only findings with `bun format`, then verify implementation coverage scenario-by-scenario across all seven delta specs
+- [x] 16.5 Verify: Confirm the protocol-only release from Section 5 is published and exposes strict `0.1`/`0.2` verification, tagged results, structured failures, and cross-version helpers from a clean consumer install
+- [ ] 16.6 Verify: Confirm the adapter SDK and all three first-party adapters are published with `facetAdapterApiVersion: 0.1`, while existing `0.0` CLIs retain compatible `0.0` adapter resolution — BLOCKED (pending adapter release cycle): adapter changeset still pending, `0.1` not yet published; source proven release-ready (`ADAPTER_API_VERSION = '0.1'` + prepack injection); all three first-party adapters currently publish `0.0` and current `0.0` CLIs resolve them
+- [ ] 16.7 Verify: Confirm the deployed registry pins the released protocol, accepts valid `0.1` and `0.2`, rejects malformed/unsupported archives before persistence, preserves supplementary hashes, and reads only intended primary resources — BLOCKED (pending registry deployment)
+- [ ] 16.8 Verify: Build the unreleased candidate CLI, publish a representative `0.2` archive to the stage registry, and verify metadata, archive download, stored-content behavior, and legacy `0.1` retention end to end — BLOCKED (pending stage registry access)
- [ ] 16.9 Review: Present the final activation packet and evidence to the user; the held CLI release-gate PR remains unmerged until the user explicitly authorizes the Changesets version-and-publish sequence
diff --git a/packages/cli/src/tui/views/__tests__/validate-asset-name.test.ts b/packages/cli/src/tui/views/__tests__/validate-asset-name.test.ts
new file mode 100644
index 00000000..05db5eb3
--- /dev/null
+++ b/packages/cli/src/tui/views/__tests__/validate-asset-name.test.ts
@@ -0,0 +1,58 @@
+import { describe, expect, test } from 'bun:test'
+import type { FormState } from '../../context/form-state-context.ts'
+import { validateAssetNameInWizard } from '../validate-asset-name.ts'
+
+function assets(overrides: Partial = {}): FormState['assets'] {
+ const empty = { items: [] as string[], descriptions: {}, adding: false }
+ return {
+ skill: { ...empty },
+ command: { ...empty },
+ agent: { ...empty },
+ ...overrides,
+ }
+}
+
+describe('validateAssetNameInWizard', () => {
+ test('accepts a valid, unused single-segment name', () => {
+ expect(validateAssetNameInWizard('skill', 'code-review', assets())).toBeUndefined()
+ })
+
+ test('rejects an invalid single-segment name', () => {
+ const err = validateAssetNameInWizard('skill', 'Code_Review', assets())
+ expect(err).toBeString()
+ expect(err).toContain('Name')
+ })
+
+ test('rejects a duplicate within the same type', () => {
+ const state = assets({ skill: { items: ['review'], descriptions: {}, adding: false } })
+ expect(validateAssetNameInWizard('skill', 'review', state)).toContain('already exists')
+ })
+
+ test('allows editing an item to its own name', () => {
+ const state = assets({ skill: { items: ['review'], descriptions: {}, adding: false, editing: 'review' } })
+ expect(validateAssetNameInWizard('skill', 'review', state)).toBeUndefined()
+ })
+
+ test('rejects a command that collides with an existing skill (shared namespace)', () => {
+ const state = assets({ skill: { items: ['review'], descriptions: {}, adding: false } })
+ const err = validateAssetNameInWizard('command', 'review', state)
+ expect(err).toContain('already used by a skill')
+ expect(err).toContain('share one namespace')
+ })
+
+ test('rejects a skill that collides with an existing command (shared namespace)', () => {
+ const state = assets({ command: { items: ['review'], descriptions: {}, adding: false } })
+ const err = validateAssetNameInWizard('skill', 'review', state)
+ expect(err).toContain('already used by a command')
+ })
+
+ test('allows an agent to share a name with a skill (separate namespace)', () => {
+ const state = assets({ skill: { items: ['review'], descriptions: {}, adding: false } })
+ expect(validateAssetNameInWizard('agent', 'review', state)).toBeUndefined()
+ })
+
+ test('allows an agent to share a name with a command (separate namespace)', () => {
+ const state = assets({ command: { items: ['review'], descriptions: {}, adding: false } })
+ expect(validateAssetNameInWizard('agent', 'review', state)).toBeUndefined()
+ })
+})
diff --git a/packages/cli/src/tui/views/create/create-view.tsx b/packages/cli/src/tui/views/create/create-view.tsx
index ca705235..bee71706 100644
--- a/packages/cli/src/tui/views/create/create-view.tsx
+++ b/packages/cli/src/tui/views/create/create-view.tsx
@@ -1,5 +1,5 @@
import { DEFAULT_VERSION } from '@agent-facets/engine'
-import { parseFacetName, validateAssetNameSegment, validateFacetName } from '@agent-facets/protocol'
+import { parseFacetName, validateFacetName } from '@agent-facets/protocol'
import { Box, Text } from 'ink'
import { useCallback, useEffect } from 'react'
import type { AssetType } from '../../../commands/create/types'
@@ -10,6 +10,7 @@ import { EditableField } from '../../components/editable-field.tsx'
import { useFocusOrder } from '../../context/focus-order-context.ts'
import { useFormState } from '../../context/form-state-context.ts'
import { WizardLayout } from '../../layouts/wizard-layout.tsx'
+import { validateAssetNameInWizard } from '../validate-asset-name.ts'
const ASSET_TYPES: AssetType[] = ['skill', 'command', 'agent']
@@ -183,13 +184,7 @@ export function CreateView({
defaultName={form.assets[type].items.length === 0 ? defaultAssetName : undefined}
dimmed={!assetsReady}
onEditDescription={onEditDescription}
- validate={(v) => {
- const check = validateAssetNameSegment(v)
- if (!check.ok) return `Name ${check.reason}`
- const editing = form.assets[type].editing
- if (form.assets[type].items.some((item) => item === v && item !== editing)) return `"${v}" already exists`
- return undefined
- }}
+ validate={(v) => validateAssetNameInWizard(type, v, form.assets)}
/>
))}
diff --git a/packages/cli/src/tui/views/edit/edit-view.tsx b/packages/cli/src/tui/views/edit/edit-view.tsx
index 1b71e763..7000cc28 100644
--- a/packages/cli/src/tui/views/edit/edit-view.tsx
+++ b/packages/cli/src/tui/views/edit/edit-view.tsx
@@ -1,5 +1,5 @@
import { DEFAULT_VERSION } from '@agent-facets/engine'
-import { validateAssetNameSegment, validateFacetName } from '@agent-facets/protocol'
+import { validateFacetName } from '@agent-facets/protocol'
import { Box, Text } from 'ink'
import { useCallback, useEffect } from 'react'
import { AssetSection } from '../../components/asset-section.tsx'
@@ -10,6 +10,7 @@ import { useFocusOrder } from '../../context/focus-order-context.ts'
import type { AssetSectionKey } from '../../context/form-state-context.ts'
import { useFormState } from '../../context/form-state-context.ts'
import { THEME } from '../../theme.ts'
+import { validateAssetNameInWizard } from '../validate-asset-name.ts'
const ASSET_TYPES: AssetSectionKey[] = ['skill', 'command', 'agent']
const ASSET_LABELS: Record = {
@@ -125,13 +126,7 @@ export function EditView({
section={type}
label={ASSET_LABELS[type]}
onEditDescription={onEditDescription}
- validate={(v) => {
- const check = validateAssetNameSegment(v)
- if (!check.ok) return `Name ${check.reason}`
- const editing = form.assets[type].editing
- if (form.assets[type].items.some((item) => item === v && item !== editing)) return `"${v}" already exists`
- return undefined
- }}
+ validate={(v) => validateAssetNameInWizard(type, v, form.assets)}
/>
))}
diff --git a/packages/cli/src/tui/views/validate-asset-name.ts b/packages/cli/src/tui/views/validate-asset-name.ts
new file mode 100644
index 00000000..9a7fb68e
--- /dev/null
+++ b/packages/cli/src/tui/views/validate-asset-name.ts
@@ -0,0 +1,40 @@
+import { validateAssetNameSegment } from '@agent-facets/protocol'
+import type { AssetSectionKey, FormState } from '../context/form-state-context.ts'
+
+/**
+ * Shared wizard-level asset-name validation for both the create and edit
+ * views. Returns an error string to display, or `undefined` when the name is
+ * acceptable at input time.
+ *
+ * Enforces three rules, in order:
+ * 1. the current single-segment asset-name grammar;
+ * 2. uniqueness within the asset's own type (excluding the item being
+ * edited);
+ * 3. the shared skill/command namespace — skills and commands MUST be
+ * disjoint, so a name already used by the sibling type is rejected.
+ * Agents occupy a separate namespace and are exempt.
+ *
+ * Rule 3 mirrors the build/schema collision check (`facet-manifest.ts`), so an
+ * author is told about a shared-namespace collision at the wizard rather than
+ * only at build time.
+ */
+export function validateAssetNameInWizard(
+ type: AssetSectionKey,
+ value: string,
+ assets: FormState['assets'],
+): string | undefined {
+ const check = validateAssetNameSegment(value)
+ if (!check.ok) return `Name ${check.reason}`
+
+ const editing = assets[type].editing
+ if (assets[type].items.some((item) => item === value && item !== editing)) {
+ return `"${value}" already exists`
+ }
+
+ const sibling = type === 'skill' ? 'command' : type === 'command' ? 'skill' : undefined
+ if (sibling && assets[sibling].items.some((item) => item === value)) {
+ return `"${value}" is already used by a ${sibling} (skills and commands share one namespace)`
+ }
+
+ return undefined
+}
diff --git a/packages/cli/src/util/__tests__/archive-compatibility.test.ts b/packages/cli/src/util/__tests__/archive-compatibility.test.ts
index b6e6be6b..efe8f9ba 100644
--- a/packages/cli/src/util/__tests__/archive-compatibility.test.ts
+++ b/packages/cli/src/util/__tests__/archive-compatibility.test.ts
@@ -7,7 +7,7 @@ describe('archiveCompatibilityGuidance', () => {
expect(g.what).toContain('archive format 0.2')
expect(g.detail).toContain('supported archive formats: 0.1')
// Known format → concrete minimum release, not a bare "update to latest".
- expect(g.fix).toContain('0.2.0 or later')
+ expect(g.fix).toContain('0.31.0 or later')
})
test('advises updating to latest for an unknown future format without inventing a minimum', () => {
diff --git a/packages/cli/src/util/archive-compatibility.ts b/packages/cli/src/util/archive-compatibility.ts
index 60439f45..0a78b4f2 100644
--- a/packages/cli/src/util/archive-compatibility.ts
+++ b/packages/cli/src/util/archive-compatibility.ts
@@ -28,10 +28,10 @@
* formats precisely as they are added.
*/
const MINIMUM_RELEASE_FOR_FORMAT: Readonly> = {
- // The first release that emits/consumes the `0.2` archive format. Present so
- // the mapping mechanism is exercised and documented; a CLI that supports
- // `0.2` will not itself render `0.2` as unsupported.
- '0.2': '0.2.0',
+ // The first `agent-facets` release that emits/consumes the `0.2` archive
+ // format. A CLI that supports `0.2` never renders `0.2` as unsupported; this
+ // entry is what an *older* pre-`0.2` CLI is told to update to.
+ '0.2': '0.31.0',
}
export interface ArchiveCompatibilityGuidance {
diff --git a/packages/engine/src/__tests__/run-install.test.ts b/packages/engine/src/__tests__/run-install.test.ts
index d02c156d..8e997f9f 100644
--- a/packages/engine/src/__tests__/run-install.test.ts
+++ b/packages/engine/src/__tests__/run-install.test.ts
@@ -373,6 +373,38 @@ describe('runInstall — local source success path', () => {
const after = JSON.parse(readFileSync(join(projectRoot, 'facets.lock'), 'utf8'))
expect(after.facets['viper-plans'].assets[0].files[0].integrity).toBe(wrong)
})
+
+ test('a locked owned-path set that differs from the plan aborts with RECONCILE_OWNED_PATH_SET', async () => {
+ const local = buildLocalFixture('viper-plans')
+ const relPath = `./${local.split('/').pop()}`
+ writeFileSync(join(projectRoot, 'facets.json'), JSON.stringify({ facets: { 'viper-plans': relPath } }))
+
+ // First install writes a valid 0.2 lockfile.
+ const first = await runInstall({ projectRoot, adapters: [buildFakeAdapter('test')] })
+ if (!first.ok) expect.unreachable()
+
+ // Inject an extra owned-file record into the locked skill entry so the
+ // locked path set has a path the freshly-derived plan does not own.
+ const lockPath = join(projectRoot, 'facets.lock')
+ const lock = JSON.parse(readFileSync(lockPath, 'utf8'))
+ lock.facets['viper-plans'].assets[0].files.push({
+ path: 'skills/planning/references/ghost.md',
+ integrity: `sha256:${'2'.repeat(64)}`,
+ })
+ writeFileSync(lockPath, JSON.stringify(lock))
+
+ const second = await runInstall({ projectRoot, adapters: [buildFakeAdapter('test')] })
+ if (second.ok) expect.unreachable()
+ if (second.failure.code !== 'RECONCILE_OWNED_PATH_SET') expect.unreachable()
+ expect(second.failure.facet).toBe('viper-plans')
+ expect(second.failure.asset).toBe('skill:planning')
+ // The extra locked path is reported as missing from the plan.
+ expect(second.failure.missing).toContain('skills/planning/references/ghost.md')
+ expect(second.failure.unexpected).toEqual([])
+ // Reconciliation runs before materialize, so nothing was written.
+ if (second.rollback.kind === 'not-needed') expect.unreachable()
+ expect(second.rollback.entriesUndone).toBe(0)
+ })
})
describe('runInstall — registry source surfaces REGISTRY_ERROR on resolution failure', () => {
diff --git a/packages/engine/src/registry/generated/registry-api.ts b/packages/engine/src/registry/generated/registry-api.ts
index 987ec900..6d52a5c7 100644
--- a/packages/engine/src/registry/generated/registry-api.ts
+++ b/packages/engine/src/registry/generated/registry-api.ts
@@ -149,8 +149,8 @@ export interface paths {
cookie?: never;
};
/**
- * Get the verified bodies of a scoped facet version's resources
- * @description Returns the verified body of each skill, agent, and command in the scoped facet version.
+ * Get the verified files of a scoped facet version
+ * @description Returns every verified file in the scoped facet version except the manifest, as a path-sorted list of text content and binary metadata.
*/
get: operations["getV0FacetsByScopeByNameByVersionContents"];
put?: never;
@@ -269,8 +269,8 @@ export interface paths {
cookie?: never;
};
/**
- * Get the verified bodies of a version's resources
- * @description Returns the verified body of each skill, agent, and command in the version. A concrete public version is immutable and identical for every caller, so it is long-cacheable; a private version is `no-store` because each read is authorized at request time, as is a `latest` resolution.
+ * Get the verified files of a version
+ * @description Returns every verified file in the version except the manifest, as a path-sorted list. A text file carries its decoded UTF-8 content; a binary file carries only its fingerprint and byte size. A concrete public version is immutable and identical for every caller, so it is long-cacheable; a private version is `no-store` because each read is authorized at request time, as is a `latest` resolution.
*/
get: operations["getV0FacetsByNameByVersionContents"];
put?: never;
@@ -1398,10 +1398,33 @@ export interface components {
};
ApiErrorBody: {
/** @enum {unknown} */
- code: "E_ACCOUNT_SUSPENDED" | "E_ADMIN_REQUIRED" | "E_ALREADY_MEMBER" | "E_ALREADY_ONBOARDED" | "E_API_KEY_MISSING" | "E_ARCHIVE_DECOMPRESSED_TOO_LARGE" | "E_ARCHIVE_MALFORMED" | "E_CLAIM_ALREADY_PENDING" | "E_CLAIM_PENDING_ELSEWHERE" | "E_CONTENT_INTEGRITY_MISMATCH" | "E_CONTROL_CONCURRENT_MODIFICATION" | "E_CONTROL_INVALID_TARGET" | "E_CONTROL_INVALID_VALUE" | "E_CONTROL_MANAGEMENT_IMMUTABLE" | "E_CONTROL_NOT_FOUND" | "E_CONTROL_RULE_NOT_FOUND" | "E_DRY_RUN_REQUIRED" | "E_FACET_NOT_FOUND" | "E_FACET_NOT_OWNED" | "E_FORBIDDEN" | "E_GLOBAL_FACET_MUST_BE_PUBLIC" | "E_IMPERSONATION_FORBIDDEN" | "E_INTERACTIVE_SESSION_REQUIRED" | "E_INTERNAL" | "E_INVALID_CURSOR" | "E_INVALID_NAME" | "E_INVALID_VERSION" | "E_INVITATION_NOT_FOUND" | "E_LOGOUT_REQUIRES_JWT" | "E_MANIFEST_CONTENT_MISMATCH" | "E_MEMBER_NOT_FOUND" | "E_MIGRATION_ALREADY_COMPLETED" | "E_MIGRATION_BATCH_NOT_FOUND" | "E_MIGRATION_BATCH_RUNNING" | "E_MIGRATION_DEPENDENCY_UNMET" | "E_MIGRATION_NOT_FOUND" | "E_MIGRATION_RUNNING" | "E_NAME_BLOCKED" | "E_ONBOARDING_REQUIRED" | "E_ORG_FORBIDDEN" | "E_ORG_LAST_ADMIN" | "E_ORG_NAME_RESERVED" | "E_ORG_NAME_TAKEN" | "E_ORG_NOT_FOUND" | "E_PREFIX_COLLISION_RETRY_EXHAUSTED" | "E_PRIVATE_FACET_ENTITLEMENT_REQUIRED" | "E_PROFILE_CORRUPT" | "E_QUEUE_FULL" | "E_QUEUE_ITEM_NOT_FOUND" | "E_QUEUE_ITEM_NOT_PENDING" | "E_READ_ONLY" | "E_REGISTRY_UNAVAILABLE" | "E_RESERVATION_EXISTS" | "E_RESERVATION_NOT_FOUND" | "E_REVIEW_ARTIFACT_MISSING" | "E_RUN_NOT_FOUND" | "E_SCOPE_NOT_FOUND" | "E_SCOPE_NOT_OWNED" | "E_TARBALL_CORRUPTED" | "E_TARBALL_TOO_LARGE" | "E_TOKEN_EXPIRED" | "E_TOKEN_NOT_FOUND" | "E_TOKEN_REVOKED" | "E_UNAUTHENTICATED" | "E_UNDECLARED_CONTENT" | "E_USERNAME_TAKEN" | "E_USER_NOT_FOUND" | "E_VERSION_EXISTS" | "E_WRITE_BANNED";
+ code: "E_ACCOUNT_SUSPENDED" | "E_ADMIN_REQUIRED" | "E_ALREADY_MEMBER" | "E_ALREADY_ONBOARDED" | "E_API_KEY_MISSING" | "E_ARCHIVE_DECOMPRESSED_TOO_LARGE" | "E_ARCHIVE_MALFORMED" | "E_ARCHIVE_METADATA_TOO_LARGE" | "E_CLAIM_ALREADY_PENDING" | "E_CLAIM_PENDING_ELSEWHERE" | "E_CONTENT_INTEGRITY_MISMATCH" | "E_CONTROL_CONCURRENT_MODIFICATION" | "E_CONTROL_INVALID_TARGET" | "E_CONTROL_INVALID_VALUE" | "E_CONTROL_MANAGEMENT_IMMUTABLE" | "E_CONTROL_NOT_FOUND" | "E_CONTROL_RULE_NOT_FOUND" | "E_DRY_RUN_REQUIRED" | "E_FACET_NOT_FOUND" | "E_FACET_NOT_OWNED" | "E_FORBIDDEN" | "E_GLOBAL_FACET_MUST_BE_PUBLIC" | "E_IMPERSONATION_FORBIDDEN" | "E_INTERACTIVE_SESSION_REQUIRED" | "E_INTERNAL" | "E_INVALID_CURSOR" | "E_INVALID_NAME" | "E_INVALID_VERSION" | "E_INVITATION_NOT_FOUND" | "E_LOGOUT_REQUIRES_JWT" | "E_MANIFEST_CONTENT_MISMATCH" | "E_MEMBER_NOT_FOUND" | "E_MIGRATION_ALREADY_COMPLETED" | "E_MIGRATION_BATCH_NOT_FOUND" | "E_MIGRATION_BATCH_RUNNING" | "E_MIGRATION_DEPENDENCY_UNMET" | "E_MIGRATION_NOT_FOUND" | "E_MIGRATION_RUNNING" | "E_NAME_BLOCKED" | "E_ONBOARDING_REQUIRED" | "E_ORG_FORBIDDEN" | "E_ORG_LAST_ADMIN" | "E_ORG_NAME_RESERVED" | "E_ORG_NAME_TAKEN" | "E_ORG_NOT_FOUND" | "E_PREFIX_COLLISION_RETRY_EXHAUSTED" | "E_PRIVATE_FACET_ENTITLEMENT_REQUIRED" | "E_PROFILE_CORRUPT" | "E_QUEUE_FULL" | "E_QUEUE_ITEM_NOT_FOUND" | "E_QUEUE_ITEM_NOT_PENDING" | "E_READ_ONLY" | "E_REGISTRY_UNAVAILABLE" | "E_RESERVATION_EXISTS" | "E_RESERVATION_NOT_FOUND" | "E_REVIEW_ARTIFACT_MISSING" | "E_RUN_NOT_FOUND" | "E_SCOPE_NOT_FOUND" | "E_SCOPE_NOT_OWNED" | "E_TARBALL_CORRUPTED" | "E_TARBALL_TOO_LARGE" | "E_TOKEN_EXPIRED" | "E_TOKEN_NOT_FOUND" | "E_TOKEN_REVOKED" | "E_UNAUTHENTICATED" | "E_UNDECLARED_CONTENT" | "E_UNSUPPORTED_FACET_VERSION" | "E_USERNAME_TAKEN" | "E_USER_NOT_FOUND" | "E_VERSION_EXISTS" | "E_WRITE_BANNED";
docs_url: string;
error: string;
fix: string;
+ violations?: ({
+ actual_bytes: number;
+ /** @constant */
+ field: "author";
+ /** @constant */
+ kind: "author_too_large";
+ limit_bytes: number;
+ message: string;
+ } | {
+ actual_bytes: number;
+ /** @constant */
+ field: "description";
+ /** @constant */
+ kind: "description_too_large";
+ limit_bytes: number;
+ message: string;
+ } | {
+ actual_bytes: number;
+ /** @constant */
+ kind: "aggregate_metadata_too_large";
+ limit_bytes: number;
+ message: string;
+ })[];
};
VersionListResponse: {
name: string;
@@ -1434,10 +1457,21 @@ export interface components {
};
ContentsResponse: {
content_integrity: string;
+ files: ({
+ content: string;
+ /** @constant */
+ kind: "text";
+ path: string;
+ sha256: string;
+ size_bytes: number;
+ } | {
+ /** @constant */
+ kind: "binary";
+ path: string;
+ sha256: string;
+ size_bytes: number;
+ })[];
name: string;
- resources: {
- [key: string]: string;
- };
version: string;
};
ScopeRootResponse: {
@@ -2472,7 +2506,7 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description Verified resource bodies */
+ /** @description Verified files (text content and binary metadata) */
200: {
headers: {
[name: string]: unknown;
@@ -2481,13 +2515,6 @@ export interface operations {
"application/json": components["schemas"]["ContentsResponse"];
};
};
- /** @description Not modified (If-None-Match matched the ETag) */
- 304: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
/** @description Facet or version not found */
404: {
headers: {
@@ -2687,7 +2714,7 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description Verified resource bodies */
+ /** @description Verified files (text content and binary metadata) */
200: {
headers: {
[name: string]: unknown;
diff --git a/packages/engine/src/registry/openapi.snapshot.yaml b/packages/engine/src/registry/openapi.snapshot.yaml
index 30220b52..7d266b7a 100644
--- a/packages/engine/src/registry/openapi.snapshot.yaml
+++ b/packages/engine/src/registry/openapi.snapshot.yaml
@@ -1,6 +1,6 @@
# Generated by: bun run codegen:registry
# Source: https://api.agentfacets.io/v0/openapi.yaml
-# Generated-At: 2026-07-20T14:06:08.660Z
+# Generated-At: 2026-07-24T04:29:16.224Z
# Do not edit by hand. Run `bun run codegen:registry` from packages/engine to refresh.
openapi: 3.1.0
info:
@@ -133,6 +133,7 @@ components:
- E_API_KEY_MISSING
- E_ARCHIVE_DECOMPRESSED_TOO_LARGE
- E_ARCHIVE_MALFORMED
+ - E_ARCHIVE_METADATA_TOO_LARGE
- E_CLAIM_ALREADY_PENDING
- E_CLAIM_PENDING_ELSEWHERE
- E_CONTENT_INTEGRITY_MISMATCH
@@ -191,6 +192,7 @@ components:
- E_TOKEN_REVOKED
- E_UNAUTHENTICATED
- E_UNDECLARED_CONTENT
+ - E_UNSUPPORTED_FACET_VERSION
- E_USERNAME_TAKEN
- E_USER_NOT_FOUND
- E_VERSION_EXISTS
@@ -201,6 +203,61 @@ components:
type: string
fix:
type: string
+ violations:
+ type: array
+ items:
+ anyOf:
+ - type: object
+ properties:
+ actual_bytes:
+ type: number
+ field:
+ const: author
+ kind:
+ const: author_too_large
+ limit_bytes:
+ type: number
+ message:
+ type: string
+ required:
+ - actual_bytes
+ - field
+ - kind
+ - limit_bytes
+ - message
+ - type: object
+ properties:
+ actual_bytes:
+ type: number
+ field:
+ const: description
+ kind:
+ const: description_too_large
+ limit_bytes:
+ type: number
+ message:
+ type: string
+ required:
+ - actual_bytes
+ - field
+ - kind
+ - limit_bytes
+ - message
+ - type: object
+ properties:
+ actual_bytes:
+ type: number
+ kind:
+ const: aggregate_metadata_too_large
+ limit_bytes:
+ type: number
+ message:
+ type: string
+ required:
+ - actual_bytes
+ - kind
+ - limit_bytes
+ - message
required:
- code
- docs_url
@@ -287,18 +344,51 @@ components:
properties:
content_integrity:
type: string
+ files:
+ type: array
+ items:
+ anyOf:
+ - type: object
+ properties:
+ content:
+ type: string
+ kind:
+ const: text
+ path:
+ type: string
+ sha256:
+ type: string
+ size_bytes:
+ type: number
+ required:
+ - content
+ - kind
+ - path
+ - sha256
+ - size_bytes
+ - type: object
+ properties:
+ kind:
+ const: binary
+ path:
+ type: string
+ sha256:
+ type: string
+ size_bytes:
+ type: number
+ required:
+ - kind
+ - path
+ - sha256
+ - size_bytes
name:
type: string
- resources:
- type: object
- additionalProperties:
- type: string
version:
type: string
required:
- content_integrity
+ - files
- name
- - resources
- version
ScopeRootResponse:
type: object
@@ -2621,9 +2711,9 @@ paths:
/v0/facets/{scope}/{name}/{version}/contents:
get:
operationId: getV0FacetsByScopeByNameByVersionContents
- summary: Get the verified bodies of a scoped facet version's resources
- description: Returns the verified body of each skill, agent, and command in the
- scoped facet version.
+ summary: Get the verified files of a scoped facet version
+ description: Returns every verified file in the scoped facet version except the
+ manifest, as a path-sorted list of text content and binary metadata.
tags:
- facets
parameters:
@@ -2632,13 +2722,11 @@ paths:
- *a3
responses:
"200":
- description: Verified resource bodies
+ description: Verified files (text content and binary metadata)
content:
application/json:
schema:
$ref: "#/components/schemas/ContentsResponse"
- "304":
- description: Not modified (If-None-Match matched the ETag)
"404":
description: Facet or version not found
content:
@@ -2790,11 +2878,13 @@ paths:
/v0/facets/{name}/{version}/contents:
get:
operationId: getV0FacetsByNameByVersionContents
- summary: Get the verified bodies of a version's resources
- description: Returns the verified body of each skill, agent, and command in the
- version. A concrete public version is immutable and identical for every
- caller, so it is long-cacheable; a private version is `no-store` because
- each read is authorized at request time, as is a `latest` resolution.
+ summary: Get the verified files of a version
+ description: Returns every verified file in the version except the manifest, as
+ a path-sorted list. A text file carries its decoded UTF-8 content; a
+ binary file carries only its fingerprint and byte size. A concrete
+ public version is immutable and identical for every caller, so it is
+ long-cacheable; a private version is `no-store` because each read is
+ authorized at request time, as is a `latest` resolution.
tags:
- facets
parameters:
@@ -2802,7 +2892,7 @@ paths:
- *a3
responses:
"200":
- description: Verified resource bodies
+ description: Verified files (text content and binary metadata)
content:
application/json:
schema:
diff --git a/packages/protocol/src/__tests__/lockfile-versions.test.ts b/packages/protocol/src/__tests__/lockfile-versions.test.ts
index 84600bee..045a9716 100644
--- a/packages/protocol/src/__tests__/lockfile-versions.test.ts
+++ b/packages/protocol/src/__tests__/lockfile-versions.test.ts
@@ -76,6 +76,22 @@ describe('CurrentLockfileSchema', () => {
expect(CurrentLockfileSchema(currentLockfile)).not.toBeInstanceOf(type.errors)
})
+ test('accepts single-file agent and command entries listing exactly their primary path', () => {
+ const agent = {
+ scope: 'user',
+ type: 'agent',
+ name: 'reviewer',
+ files: [{ path: 'agents/reviewer.md', integrity: HASH }],
+ }
+ const command = {
+ scope: 'project',
+ type: 'command',
+ name: 'review',
+ files: [{ path: 'commands/review.md', integrity: HASH }],
+ }
+ expect(CurrentLockfileSchema(withAssets([agent, command]))).not.toBeInstanceOf(type.errors)
+ })
+
function withAssets(assets: unknown[]): unknown {
return {
lockfileVersion: 0.2,