diff --git a/openspec/changes/support-non-asset-files/.openspec.yaml b/openspec/changes/support-non-asset-files/.openspec.yaml new file mode 100644 index 00000000..b119b635 --- /dev/null +++ b/openspec/changes/support-non-asset-files/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-13 diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/design.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/design.md new file mode 100644 index 00000000..6c207186 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/design.md @@ -0,0 +1,169 @@ +## Context + +Facet archives currently have one implicit membership rule: `facet.json` plus the conventional file for every declared skill, agent, and command. The same assumption appears in schema validation, build collection, per-file hashing, archive verification, parsed archive data, installation receipts, and the adapter SDK. Supporting files cannot therefore be added only at archive assembly; their declaration, integrity, classification, and ownership must remain consistent across the whole pipeline. + +This design treats the embedded `facet.json` as the single source of truth for archive membership and classification. The build manifest records hashes, not a second description of which paths are assets or supplementary files. A supplementary path inside a declared skill directory belongs to that skill; every other supplementary path is archive-only metadata and is never materialized. + +The change crosses the published protocol, engine, adapter SDK, first-party adapters, CLI error presentation, and the cafe registry's verifier. Existing `facetVersion: 0.1` archives must remain consumable, while an archive containing supplementary files is intentionally not consumable by a legacy verifier. + +## Goals / Non-Goals + +**Goals:** + +- Authors MUST be able to explicitly declare regular files to include in a facet archive. +- Build and verification MUST derive exactly the same canonical archive-entry set from `facet.json` and MUST reject missing, extra, unsafe, duplicate, or colliding paths. +- Every inner-archive file MUST have a per-file hash, and supplementary bytes MUST affect the archive integrity hash. +- A declared file below a declared skill directory MUST install, update, and delete as part of one skill operation. +- Supplementary files outside declared skill directories MUST remain verified archive metadata and MUST NOT acquire independent asset identity, scope, lockfile tuples, or install destinations. +- New consumers MUST continue to accept valid legacy `0.1` archives. + +**Non-Goals:** + +- This change does not add recursive discovery, glob declarations, or implicit inclusion of source-tree files. +- This change does not display README content, add `facet info`, or define a registry presentation API. +- This change does not materialize companion files for agents or commands. +- This change does not preserve symlinks, hard links, directory entries, executable bits, timestamps, ownership, or other filesystem metadata. +- This change does not make supplementary files selectable or independently installable. + +## Decisions + +### 1. `facet.json` declares an explicit list of canonical file paths + +The facet manifest SHALL gain an optional top-level `files` array of strings. Each member names exactly one source-root-relative regular file using `/` as its separator. Patterns, directories, and recursive discovery SHALL NOT be supported. + +A declared path MUST: + +- be non-empty, relative, and already canonical; +- contain no empty, `.` or `..` segment, backslash, NUL, absolute-path prefix, drive prefix, or URL-like prefix; +- resolve through existing parents to a regular file inside the facet root; +- not be a symlink or hard link; +- not be `facet.json`, `build-manifest.json`, `archive.tar.gz`, or any conventional primary asset path derived from the manifest; and +- not collide with another derived archive path by exact spelling, canonical Unicode form, portable case folding, resolved source identity, or file/directory prefix. + +Paths under `skills//` SHALL be accepted only when `` is a declared skill and the path has content below that directory. `skills//SKILL.md` remains derived from the skill declaration and MUST NOT also appear in `files`. This makes skill ownership a function of path plus the existing skill declaration rather than a second owner field that could disagree. Declared paths under `agents/` or `commands/` are permitted but remain archive-only metadata. + +Supplementary files MAY be empty and MAY contain arbitrary bytes. Asset-specific rules such as non-empty Markdown and no YAML front matter continue to apply only to primary asset files. Build SHALL hash and preserve supplementary bytes exactly. + +**Alternatives considered:** + +- Glob patterns and directory declarations were rejected because membership would depend on ambient source-tree contents and review of `facet.json` would not reveal the archive's exact file set. +- Descriptor-local companion lists were rejected because they would create two declaration mechanisms and would not cover root metadata files. +- An object per file carrying an asset kind or owner was rejected because kind and ownership are already derivable, and duplicated classification could drift. + +### 2. Archive format version 1 carries a path-to-hash table for every file + +An archive with a non-empty `files` declaration SHALL use `facetVersion: 1` in `build-manifest.json`. Version 1 SHALL replace the misleading `assets` hash map with a `files` map from canonical inner-archive path to `sha256:` computed over the exact file bytes. The map SHALL include `facet.json`, every primary asset file, and every declared supplementary file. It SHALL carry hashes only; asset/supplementary classification SHALL be derived from the embedded facet manifest. + +The inner archive entry set for version 1 SHALL equal: + +1. `facet.json`; +2. each conventional primary asset path derived from the embedded manifest; and +3. each canonical path in `facet.json.files`. + +No other entry is valid. Entries SHALL be regular files only and lexicographically ordered by canonical UTF-8 path bytes. Existing deterministic metadata rules remain unchanged. The integrity hash SHALL continue to cover the canonical uncompressed inner tar bytes, so any supplementary-file change alters integrity. + +A new producer MAY continue to emit version `0.1` when `files` is absent or empty. This preserves old-consumer interoperability for asset-only facets. A new verifier SHALL dispatch on the build-manifest version: it SHALL apply the exact legacy schema and membership rules to `0.1`, the version 1 rules above to `1`, and reject unsupported versions with structured data. It SHALL NOT reinterpret a malformed version 1 archive as `0.1`. + +This archive-version boundary is separate from release versions: publishing version 1 support requires a new major release of the protocol package because archive acceptance and the public parsed result change, and a new major release of the adapter SDK because its asset operations change. + +**Alternatives considered:** + +- Reusing `facetVersion: 0.1` was rejected because old consumers reject the expanded entry set and the build-manifest hash shape changes. +- Keeping an `assets` map and adding a second `files` map was rejected because the two maps could overlap, omit entries, or disagree about classification. +- Emitting version 1 for every asset-only build was rejected as an unnecessary compatibility loss; content that fits the legacy contract can remain legacy-encoded. + +### 3. Build and verification share one path-derivation operation + +The protocol package SHALL expose one pure operation that validates a facet manifest's file declarations and derives a tagged archive plan. The plan SHALL distinguish manifest, primary assets, skill companions, and archive-only supplementary files. Build collection and archive verification MUST consume this same operation rather than maintaining separate allowlists. + +Build SHALL resolve the plan against the source root, validate containment and regular-file identity, read every planned path as bytes, and fail with structured errors before writing `dist/` if any path is missing, unsafe, duplicated, colliding, or not a regular file. The existing cleanup of `dist/` MUST NOT occur until all declared source inputs have been validated, preventing a declaration from being destroyed before its missing-file error can be reported. + +Verification SHALL parse tar headers without normalizing them into a lossy map. It SHALL reject duplicate paths, non-regular entries, non-canonical paths, unsafe paths, and prefix collisions before exposing contents. After validating and parsing `facet.json`, it SHALL derive the expected plan and compare the expected and observed path sets for exact equality. It SHALL then require one version 1 `files` hash for every expected path, no hash for any other path, and byte-verify every hash. All expected failures SHALL remain structured result variants rather than thrown errors. + +The successful parsed result SHALL carry the verified manifest, primary assets, skill-companion bytes grouped by owning skill, and archive-only supplementary bytes as distinct tagged data. It SHALL not represent classification through optional fields whose combinations can disagree. + +**Alternatives considered:** separate engine and verifier derivation was rejected because the current outer-exclusivity drift demonstrates that duplicated membership logic is a security boundary. + +### 4. Skills use a tagged bundle contract; other assets remain single-file + +The adapter SDK's install, read, and delete requests/results SHALL become tagged unions keyed by asset type: + +- a skill variant carries its `SKILL.md` text plus a canonical map of companion paths relative to the skill root and their bytes; +- agent and command variants carry their existing single Markdown content; and +- no variant for supplementary files exists. + +This prevents an agent or command request from accidentally carrying companions and prevents a skill request from omitting its bundle shape. Metadata continues to apply to the primary asset, and adapters SHALL reconstruct tool-specific metadata only in the primary file; companion bytes SHALL be preserved without front-matter transformation. + +A skill install SHALL be one adapter operation. It MUST stage the complete replacement, remove previously owned companion paths that are absent from the new bundle, and commit or roll back without leaving a partial bundle. Skill deletion MUST remove the primary file and all recorded owned companions as one operation, while retaining unrelated files not listed as owned. Expected adapter failures MUST be structured result values. + +First-party filesystem helpers SHALL centralize containment checks, staging, commit/rollback, owned-file removal, and empty-directory pruning so adapters do not duplicate this security-sensitive behavior. Adapters MAY choose tool-specific roots and representations, but MUST NOT allow a companion path to escape the resolved skill root. + +**Alternatives considered:** + +- Passing every file as an independently installable asset was rejected because it would create false asset identity, scope, metadata, and lockfile semantics. +- Letting the engine write companions directly was rejected because adapters own all storage paths and formats. +- Deleting the entire skill directory was rejected because it can remove unowned user files; deletion is based on recorded ownership instead. + +### 5. The machine-local receipt, not the lockfile, records skill-file ownership + +The version-controlled lockfile SHALL retain its existing `{scope, type, name}` asset tuples. Supplementary files SHALL NOT appear as lockfile assets and SHALL NOT require a lockfile-version bump. + +The machine-local receipt SHALL replace its uniform asset record with a tagged union. Agent and command records retain scope, type, and name. A skill record additionally requires the complete set of owned installed paths, including its primary file and adapter-derivable companion paths. There is no optional `files` field whose presence implicitly decides the record kind. + +On update, the installer SHALL pass the prior owned set and the new verified bundle to each adapter so stale companions are removed. On facet removal, it SHALL use the receipt alone to delete all owned skill files without cache or network access. Receipt path containment and project-identity checks remain mandatory. A legacy receipt containing only skill tuples SHALL be migrated conservatively: the primary skill file is known and removable, but unknown historical companions do not exist because legacy archives could not install them. + +The install journal SHALL snapshot receipt changes and adapter operations so a failure restores the previous receipt and materialized state. Archive-only supplementary files SHALL remain in the verified archive/cache and parsed artifact but SHALL never enter the receipt. + +**Alternatives considered:** putting companion paths in the lockfile was rejected because the lockfile records facet assets, not machine- and adapter-specific materialization ownership, and because doing so would make supplementary files look independently installable. + +### 6. Rollout is consumer-first and documentation is part of the change + +Rollout SHALL occur in this order: + +1. Release version 1 verification and legacy `0.1` compatibility in every consumer, including the cafe registry, while producers still default to legacy output. +2. Release the adapter SDK major and updated first-party adapters, then update installation receipt migration. +3. Enable producer support for `facet.json.files` and version 1 archives. +4. Publish fixtures proving cross-implementation acceptance and rejection at both version boundaries. + +The authoring and build documentation SHALL warn that older builders tolerate unknown manifest fields and can therefore ignore `files`; projects using supplementary files MUST pin or require a producer version that supports archive version 1. Registries that have not deployed version 1 support will reject those archives by design. + +The following files MUST be updated together with implementation: + +- `docs/specification/archive.mdx` +- `docs/specification/build.mdx` +- `docs/specification/manifest.mdx` +- `docs/specification/integrity.mdx` +- `docs/guides/create-your-first-facet.mdx` +- `docs/guides/install-facets.mdx` +- root `README.md` + +`docs/specification/lockfile.mdx` MUST be reviewed and SHOULD explicitly state that companion ownership is receipt-only while lockfile asset tuples remain unchanged. This design does not require a lockfile schema change. + +Rollback MAY disable production of version 1 archives, but consumers and the registry MUST retain version 1 verification once such archives have been published. Already-published version 1 artifacts cannot be made consumable by legacy clients without republishing an asset-only version. + +## Risks / Trade-offs + +- **[Old builders accept but ignore the new unknown `files` field]** → Documentation MUST declare the minimum supporting producer release, examples SHOULD pin it, and CI compatibility fixtures MUST prove that only supporting producers emit the declared files. This cannot be repaired retroactively in already-released tolerant parsers. +- **[Path aliases or crafted tar headers bypass membership checks]** → One canonical path validator and archive-plan derivation MUST be shared; verifier tests MUST cover traversal, absolute and drive paths, backslashes, duplicate headers, Unicode/case aliases, prefix collisions, symlinks, hard links, and undeclared entries. +- **[A failed skill update leaves a half-written directory]** → The adapter contract MUST require stage/commit/rollback behavior, and integration tests MUST inject failure at each write/delete step. +- **[Receipt corruption causes over-deletion]** → Receipts remain untrusted; project identity, adapter-root containment, and exact owned-path validation MUST precede deletion. Unowned paths MUST never be deleted. +- **[Arbitrary companion bytes increase memory or decompression pressure]** → Existing caller-supplied decompression limits SHALL apply to the complete archive, and registry/CLI policy MAY impose total-size, per-file-size, and entry-count limits without changing archive semantics. +- **[Two supported archive versions create implementation branches]** → Version dispatch MUST occur once at parsing, with immutable fixtures for both schemas and no fallback between them. +- **[Cafe accepts producer output before it can verify it]** → Producer enablement MUST remain gated until registry version 1 verification is deployed. +- **[Conditional legacy output surprises authors]** → Build output MUST display the emitted archive version and complete file listing. + +## Migration Plan + +1. Add canonical path, archive-plan, versioned build-manifest, and parsed-result types in the protocol package with legacy fixtures unchanged. +2. Add version 1 build and verification fixtures, including byte tampering and every path-security failure class. +3. Migrate engine build/load/cache code to consume the tagged parsed result without materializing archive-only files. +4. Release the adapter SDK major, migrate first-party adapters, and add atomic skill-bundle contract tests. +5. Migrate receipt loading/writing and exercise install, update, removal, rollback, frozen install, offline removal, and pulled-lockfile drift scenarios with multi-file skills. +6. Deploy cafe verification for both archive versions before enabling publication of version 1 artifacts. +7. Update all listed documentation and enable producer support. + +Rollback SHALL stop new version 1 production and restore the prior adapter package only before multi-file skills are installed. After version 1 publication or materialization, verifier support and receipt-aware deletion MUST remain available even if authoring support is temporarily disabled. + +## Open Questions + +None. Policy limits for archive size and entry count remain consumer configuration rather than protocol-format decisions. diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/proposal.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/proposal.md new file mode 100644 index 00000000..c84983ea --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/proposal.md @@ -0,0 +1,42 @@ +## Why + +Facet authors cannot currently include documentation, licenses, development notes, or companion skill resources in a built facet because the artifact contract rejects every file that is not a declared skill, agent, command, or manifest. Facets need a safe way to carry these supporting files without falsely treating them as independently installable assets. + +## What Changes + +- The facet manifest SHALL be able to declare supplementary files, including conventional files such as `README.md`, `LICENSE`, `DEVELOPMENT.md`, and nested files belonging to a skill. +- A build SHALL include every declared supplementary file in the deterministic facet archive and SHALL integrity-protect it with the same per-file and whole-archive guarantees applied to asset files. Missing, undeclared, duplicate, or unsafe file paths SHALL fail validation rather than creating an ambiguous artifact. +- Supplementary files SHALL remain distinct from assets: they SHALL NOT acquire an asset type, adapter metadata, an independent install scope, or a lockfile asset tuple. +- When a skill is installed, declared files contained by that skill's directory SHOULD be materialized with the skill through the adapter. Supplementary files associated with command and agent paths, and files elsewhere in the facet, SHALL remain archive content and SHALL NOT be materialized. +- `README.md` SHALL be documented as a conventional supplementary file, while the manifest retains one general file-declaration mechanism rather than a second README-specific source of truth. +- **BREAKING**: Artifacts containing supplementary files extend the archive entry set beyond the current format's asset-only exclusivity rule. The protocol/archive format SHALL use an explicit compatibility boundary so older consumers do not misinterpret these artifacts; existing asset-only facets SHALL remain valid. +- Documentation covering manifests, builds, archives, integrity, authoring, and installation SHALL be updated to distinguish archived files from installable assets. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `authoring__facets`: Authors can declare, validate, and build supplementary files in addition to installable assets. +- `protocol__schemas`: Published facet and build-manifest schemas represent the complete tracked file set without classifying supplementary files as assets. +- `protocol__content-hashing`: Deterministic archives and per-file hash records cover all declared content, not only asset prompt files. +- `protocol__integrity`: Archive verification reconciles and verifies every declared supplementary file while preserving path-safety guarantees. +- `installation`: Installation materializes companion files inside installed skill directories but retains other supplementary files only as verified archive content. +- `adapter__assets`: The skill installation contract can carry a skill's declared companion-file tree without expanding command or agent installation into directory installation. + +## Non-goals + +- This change SHALL NOT add a `facet info` command or render README content in the CLI; retaining README content enables that separate future capability. +- This change SHALL NOT make arbitrary supplementary files directly installable or add new adapter-independent destination paths. +- This change SHALL NOT give commands or agents companion-directory installation semantics. +- This change SHALL NOT preserve executable permissions, symlinks, or other filesystem metadata for supplementary files. +- This change SHALL NOT automatically package untracked source-tree files; archive membership remains explicit and reviewable. + +## Impact + +The published protocol and reference implementation will change across manifest/build schemas, archive assembly and parsing, content hashing, verification, build validation, installation planning/materialization, and the adapter SDK's skill-install input. Implementations will need compatibility tests for legacy asset-only artifacts and the new archive format, plus security tests for traversal, collisions, undeclared entries, and tampering of non-asset files. + +This proposal was informed by `docs/specification/manifest.mdx`, `docs/specification/build.mdx`, `docs/specification/archive.mdx`, `docs/specification/integrity.mdx`, `docs/guides/create-your-first-facet.mdx`, `docs/guides/install-facets.mdx`, and the root `README.md`. Those pages currently describe facets and integrity in asset-only terms and SHOULD be updated with the new tracked-file and materialization boundaries. diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/adapter__assets/spec.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/adapter__assets/spec.md new file mode 100644 index 00000000..e89fa7bf --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/adapter__assets/spec.md @@ -0,0 +1,115 @@ +## MODIFIED Requirements + +### Requirement: Adapters provide asset installation + +An adapter SHALL accept a request to install an asset at a given scope. The request SHALL be tagged by asset type, and each type's payload SHALL carry exactly the data that type can hold: + +- a **skill** request carries the skill's primary content plus a map of companion paths (relative to the skill's root) to file bytes; an empty map is the valid representation of a companion-less skill; +- an **agent** or **command** request carries a single content string and structurally cannot carry companion files. + +The adapter SHALL receive the scope, asset name, per-asset metadata, and the type-tagged payload. The adapter SHALL handle all storage concerns internally — including path resolution, directory creation, metadata assembly, and file format. Companion paths SHALL be confined to the skill's resolved storage root; a companion path that would escape it SHALL be rejected as a structured failure before any write. Metadata and front-matter conventions SHALL apply only to the primary content; companion bytes SHALL be written verbatim. + +A skill installation SHALL be all-or-nothing: the adapter SHALL install the complete bundle — primary file plus every companion — and SHALL remove previously installed companion paths absent from the new bundle, committing the result as one operation. On failure the adapter SHALL leave no partial bundle: either the prior state or the complete new state remains. Installation SHALL be idempotent: installing an asset whose name already exists at that scope SHALL overwrite the existing asset, replacing its companion set with the new bundle. Expected failures SHALL be structured results, not thrown errors. + +#### Scenario: Install a skill asset with companions + +- **WHEN** the system requests an adapter to install a skill whose payload carries primary content and two companion files at a given scope +- **THEN** the adapter SHALL store the primary file and both companions at the locations appropriate for that scope +- **AND** the adapter SHALL incorporate the metadata into the stored primary asset according to its tool's conventions +- **AND** the companion bytes SHALL be stored verbatim without metadata or front-matter processing + +#### Scenario: Install a companion-less skill + +- **WHEN** the system requests an adapter to install a skill whose companion map is empty +- **THEN** the adapter SHALL store the primary file +- **AND** the operation SHALL be valid without any companion writes + +#### Scenario: An agent or command request cannot carry companions + +- **WHEN** the system constructs an install request for an agent or command +- **THEN** the request's payload SHALL carry only the single content string +- **AND** no companion data SHALL be expressible in that request + +#### Scenario: Install an asset at the user scope + +- **WHEN** the system requests an adapter to install an asset at the user scope +- **THEN** the adapter SHALL store the asset using the adapter's user-level storage root + +#### Scenario: Reinstalling a skill replaces its companion set + +- **WHEN** the system requests an adapter to install a skill whose name already exists at the given scope +- **AND** the previously installed skill included a companion the new bundle omits +- **THEN** the adapter SHALL overwrite the primary file and install the new companions +- **AND** the adapter SHALL remove the omitted previously-installed companion +- **AND** the adapter SHALL NOT produce an error for the name collision + +#### Scenario: A failed skill install leaves no partial bundle + +- **WHEN** an adapter fails partway through installing a skill bundle (for example a companion write fails) +- **THEN** the adapter SHALL report a structured failure +- **AND** the skill's storage location SHALL NOT contain a mixture of old and new bundle files + +#### Scenario: An escaping companion path is rejected + +- **WHEN** an install request carries a companion path that would resolve outside the skill's storage root (for example via `..` segments) +- **THEN** the adapter SHALL reject the request with a structured failure identifying the path +- **AND** the adapter SHALL NOT write any file + +### Requirement: Adapters provide asset reading + +An adapter SHALL accept a request to read an asset from a given scope. The adapter SHALL receive the scope, asset type, and asset name. The result SHALL be tagged by asset type: a skill read SHALL return the primary content plus the companion files present in the skill's storage, and an agent or command read SHALL return the single content string. Returned primary content SHALL be the canonical logical content — the adapter SHALL project any tool-specific storage encoding back to the content the system installed — so that callers can compare it against canonical recorded hashes. Companion bytes SHALL be returned verbatim. The adapter SHALL also return any adapter-specific metadata stored alongside the asset. + +#### Scenario: Read an existing skill with companions + +- **WHEN** the system requests an adapter to read a skill whose storage contains a primary file and companion files +- **THEN** the adapter SHALL return the primary content, the companion files, and the stored metadata + +#### Scenario: Read returns canonical logical content + +- **WHEN** an adapter stores primary content in a tool-specific format that differs from the installed canonical content +- **AND** the system reads that asset back +- **THEN** the returned primary content SHALL equal the canonical logical content the system installed, absent user modification +- **AND** a caller comparing it against the canonical recorded hash SHALL observe a match + +#### Scenario: Read a non-existent asset + +- **WHEN** the system requests an adapter to read an asset that does not exist at the given scope +- **THEN** the adapter SHALL indicate that the asset was not found + +### Requirement: Adapters provide asset deletion + +An adapter SHALL accept a request to delete an asset from a given scope. The adapter SHALL receive the scope, asset type, and asset name — and, for a skill, the set of owned companion paths to remove. Skill deletion SHALL remove the primary file and every owned companion as one operation, and SHALL NOT delete files in the skill's storage location that are not in the owned set. Deletion SHALL NOT remove an entire directory wholesale when unowned files remain in it. Expected failures SHALL be structured results. + +#### Scenario: Delete a skill and its owned companions + +- **WHEN** the system requests an adapter to delete a skill, providing its owned companion paths +- **THEN** the adapter SHALL remove the primary file and every owned companion from that scope + +#### Scenario: Deletion preserves unowned files in the skill's location + +- **WHEN** a skill's storage location contains a file that is not the primary file and not in the owned companion set +- **AND** the system requests deletion of that skill +- **THEN** the adapter SHALL NOT delete the unowned file + +#### Scenario: Delete a non-existent asset + +- **WHEN** the system requests an adapter to delete an asset that does not exist at the given scope +- **THEN** the adapter SHALL indicate that the asset was not found + +## ADDED Requirements + +### Requirement: Adapters never receive archive-only supplementary files + +Install, read, and delete requests SHALL have no representation for archive-only supplementary files (files that ship in a facet's archive but do not belong to any asset). The only non-asset file data an adapter ever receives is a skill's companion map inside a skill-tagged payload. A facet's root `README.md`, `LICENSE`, or other archive-only files SHALL never appear in any adapter request. + +#### Scenario: Installing a facet with archive-only files sends adapters only assets + +- **WHEN** the system installs a facet that ships a root `README.md` alongside its assets +- **THEN** every adapter request issued during the install SHALL describe an asset (skill, agent, or command) +- **AND** no request SHALL reference `README.md` + +#### Scenario: No request shape can carry a supplementary file + +- **WHEN** an install, read, or delete request is constructed +- **THEN** its type-tagged payload SHALL be one of the asset variants +- **AND** no variant SHALL exist for archive-only supplementary files diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/authoring__facets/spec.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/authoring__facets/spec.md new file mode 100644 index 00000000..a4599e37 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/authoring__facets/spec.md @@ -0,0 +1,431 @@ +## MODIFIED Requirements + +### Requirement: Authors can build a facet locally for validation and inspection + +The system SHALL compile a facet project into a build output directory. The build command SHALL read the manifest, validate it, verify that every declared asset file exists, is non-empty, and contains no YAML front matter, verify that every declared supplementary file (top-level and per-skill) exists and satisfies the supplementary path rules, resolve all file-based prompts to their content, run all validation checks, assemble the resolved output and declared supplementary files into a deterministic compressed archive, compute content hashes for every archive entry, and write the archive and build manifest to a `dist/` directory. All source-input validation — including missing declared supplementary files — SHALL complete before any previous `dist/` output is removed, so that a build failure never destroys prior output before its cause is reported. The build command SHALL NOT modify the manifest or any content files. The build command SHALL NOT be interactive — it SHALL behave identically in all environments. + +The build output SHALL contain a compressed archive (`.facet` file) with the manifest, all text asset files with prompts resolved to their final string content, and all declared supplementary files with their bytes preserved verbatim, and a build manifest (`build-manifest.json`) recording the archive format version and per-entry content hashes. For a scoped facet identity, whose name renders as a nested path under `dist/`, the system SHALL create any required parent directories under `dist/` before writing the built archive. The build-output write boundary SHALL create parent directories for any slash-containing archive path, so the same fix also repairs the pre-existing failure for any nested archive filename. + +The build command SHALL render its progress as a step-by-step display, showing each pipeline stage as it completes — including the archive assembly stage. On success, the system SHALL display the archive contents listing (including supplementary files), the emitted archive format version, and the archive content hash. On failure, the system SHALL indicate which stage failed and display errors with their field paths, and SHALL suggest running the editing command to fix the issues. After the display exits, the system SHALL print a brief plain-text summary to stdout — including the content hash — so it persists in terminal scroll-back. + +#### Scenario: Successful build of a valid facet + +- **WHEN** the author runs the build command in a directory with a valid manifest, all referenced files exist, and no asset files contain front matter +- **THEN** the system SHALL write a compressed archive and build manifest to `dist/` +- **AND** the archive SHALL contain the facet manifest, all text asset files with prompts resolved to their string content, and all declared supplementary files verbatim +- **AND** the build manifest SHALL contain the archive content hash and per-entry content hashes +- **AND** the system SHALL display the archive contents, the emitted archive format version, and content hash +- **AND** the system SHALL print a brief success summary to stdout including the content hash + +#### Scenario: Successful build of a facet with supplementary files + +- **WHEN** the author builds a facet declaring a top-level `README.md` and a skill companion `references/api.md` +- **THEN** the archive SHALL contain `README.md` at the archive root and the companion beneath the owning skill's directory +- **AND** the archive contents listing SHALL include both supplementary files + +#### Scenario: Successful build of a scoped facet identity + +- **WHEN** the author runs the build command for a valid facet whose name is `@julian/cowsay` +- **THEN** the system SHALL write the built archive under `dist/` without failing on the scoped name separator +- **AND** the archive SHALL contain `facet.json` at the archive root with `name` set to `@julian/cowsay` +- **AND** the archive's internal asset paths SHALL continue to be derived from asset names, not from the facet identity + +#### Scenario: Build-output write boundary creates parent directories for a nested archive path + +- **WHEN** the build-output write boundary writes an archive whose filename renders as a nested path under `dist/` (for example a scoped `@scope/name` identity, or any other slash-containing archive filename) +- **THEN** the system SHALL create the required parent directories under `dist/` before writing the archive +- **AND** the write SHALL NOT fail with a missing-directory error + +#### Scenario: Build fails on invalid manifest + +- **WHEN** the author runs the build command and the manifest fails schema validation +- **THEN** the system SHALL report the validation errors with field paths +- **AND** the system SHALL suggest running the editing command to fix the issues +- **AND** the system SHALL NOT write any output to `dist/` + +#### Scenario: Build fails on missing asset file + +- **WHEN** the author runs the build command and any asset references a file that does not exist +- **THEN** the system SHALL report which file is missing and which asset references it +- **AND** the system SHALL suggest running the editing command to fix the issues +- **AND** the system SHALL NOT write any output to `dist/` + +#### Scenario: Build fails on missing declared supplementary file before cleaning previous output + +- **WHEN** the author runs the build command and a declared supplementary file does not exist on disk +- **AND** a `dist/` directory exists from a previous build +- **THEN** the system SHALL fail with a structured error identifying the missing declared path and its declaration site +- **AND** the previous `dist/` contents SHALL remain untouched + +#### Scenario: Build fails on asset file containing front matter + +- **WHEN** the author runs the build command and an asset content file contains YAML front matter +- **THEN** the system SHALL report which file contains front matter +- **AND** the system SHALL suggest running the editing command to strip it +- **AND** the system SHALL NOT write any output to `dist/` + +#### Scenario: Build fails on empty asset content file + +- **WHEN** the author runs the build command and an asset content file referenced by the manifest is empty (zero bytes or only whitespace) +- **THEN** the system SHALL report which file is empty and which asset references it +- **AND** the system SHALL suggest running the editing command to add content +- **AND** the system SHALL NOT write any output to `dist/` + +#### Scenario: Build with no manifest + +- **WHEN** the author runs the build command in a directory with no manifest +- **THEN** the system SHALL report that no manifest was found + +#### Scenario: Build cleans previous output + +- **WHEN** the author runs the build command with valid inputs and a `dist/` directory already exists from a previous build +- **THEN** the system SHALL remove the previous `dist/` contents before writing the new output +- **AND** the removal SHALL occur only after all source-input validation has passed + +### Requirement: Build detects naming collisions between local assets + +The system SHALL detect asset naming collisions during build. Skills SHALL have unique names within the skills section, agents SHALL have unique names within the agents section, and commands SHALL have unique names within the commands section. In addition, skills and commands SHALL share one logical namespace: a facet declaring a skill and a command with the same name SHALL fail the build with a structured error identifying both colliding declarations. Agents SHALL remain a separate namespace: an agent MAY share a name with a skill or a command. Collisions SHALL cause the build to fail with an error identifying the conflicting names and their asset types. + +#### Scenario: Two skills share a name + +- **WHEN** a facet declares two skills with the same name +- **THEN** the build SHALL fail +- **AND** the error SHALL identify the collision within the skills section + +#### Scenario: Skill and command share a name + +- **WHEN** a facet declares a skill and a command with the same name +- **THEN** the build SHALL fail +- **AND** the error SHALL identify both the skill declaration and the command declaration + +#### Scenario: Skill and agent share a name + +- **WHEN** a facet declares a skill and an agent with the same name +- **THEN** the build SHALL succeed with no collision errors + +#### Scenario: Command and agent share a name + +- **WHEN** a facet declares a command and an agent with the same name +- **THEN** the build SHALL succeed with no collision errors + +#### Scenario: No collisions across distinct names + +- **WHEN** a facet declares assets with distinct names within each asset type and no skill/command overlap +- **THEN** the build SHALL succeed with no collision errors + +### Requirement: Authors can scaffold a new facet project interactively + +The system SHALL provide an interactive wizard that guides the author through creating a new facet project. The wizard SHALL collect the following required information: + +- **Name**: A valid facet identity name. The name SHALL be either an unscoped kebab-case name (`name`) or a scoped name (`@scope/name`). The system SHALL validate the name in real-time and reject invalid input. +- **Description**: A non-empty description. The system SHALL NOT allow the author to complete the wizard without providing a description. + +The wizard SHALL also collect optional information: + +- **Version**: A valid SemVer version (N.N.N format). The system SHALL default to `0.0.0`. The author MAY accept the default or change it. +- **Privacy**: A choice of whether the new facet declares private publish intent. The system SHALL default to public visibility intent. The author MAY accept the public default or choose private. +- **README**: A dedicated README step, enabled by default, whose behavior is defined by the first-class README authoring requirement. + +The wizard SHALL also allow the author to manage assets (skills, commands, and agents): + +- The author SHALL be able to add multiple named assets of any type +- The author SHALL be able to edit the name of an existing asset +- The author SHALL be able to remove an existing asset +- All asset names SHALL be validated in real-time against the single-segment asset-name convention +- Asset names SHALL be unique within their type — the system SHALL reject duplicates within the same asset type +- Skills and commands SHALL share one logical namespace — the system SHALL reject a skill and a command with the same name +- Agents MAY share a name with a skill or a command +- The first asset added to each type SHOULD default its name to the unscoped name segment of the facet identity as a suggestion + +The wizard SHALL require the author to add at least one **asset** before completing. Name, description, and at least one **asset** are all required. + +All fields SHALL remain editable throughout the wizard — the author SHALL be able to go back and change any previously entered value, including privacy and the README choice. + +Before completing, the wizard SHALL display a confirmation summary showing only the asset types that have entries, the selected privacy intent, and a preview of the files to be created — including `README.md` when README creation is enabled. The author SHALL be able to confirm or go back. + +The wizard SHALL provide an exit confirmation mechanism that prevents accidental loss of unsaved work. + +Upon confirmation, the system SHALL create a project directory containing a valid manifest and named starter files for each asset the author specified, with each starter file containing template content that guides authors on what belongs in each section. Skill starter files SHALL be created at `skills//SKILL.md`. Agent and command starter files SHALL be created at `agents/.md` and `commands/.md` respectively. All starter files SHALL contain no YAML front matter. + +When the author selects private visibility intent, the generated manifest SHALL contain `private: true`. When the author accepts public visibility intent, the generated manifest SHALL omit `private`. + +The scaffolded project SHALL be immediately buildable — running the build command on a freshly scaffolded project SHALL succeed with no errors. + +#### Scenario: Author scaffolds a scoped project with named skills + +- **WHEN** the author runs the create wizard, provides a name `@julian/cowsay` and description `Cowsay tools`, and adds a skill named `cowsay` +- **THEN** the system SHALL create a project directory containing a manifest whose `name` is `@julian/cowsay` +- **AND** a starter file SHALL be created at `skills/cowsay/SKILL.md` +- **AND** the manifest SHALL reference all starter files correctly + +#### Scenario: Author scaffolds a project with named skills + +- **WHEN** the author runs the create wizard, provides a name "viper-plans" and description "VIPER planning tools", and adds two skills named "viper-planning" and "viper-execution-rules" +- **THEN** the system SHALL create a project directory containing a manifest with the provided identity fields and skill descriptors +- **AND** starter files SHALL be created at `skills/viper-planning/SKILL.md` and `skills/viper-execution-rules/SKILL.md` +- **AND** the manifest SHALL reference all starter files correctly + +#### Scenario: Author scaffolds a minimal project accepting the default skill name + +- **WHEN** the author runs the create wizard, provides a name "code-review" and a description, then adds a skill accepting the default name suggestion +- **THEN** the system SHALL create a project with a skill named "code-review" (matching the facet name) +- **AND** the starter file SHALL be at `skills/code-review/SKILL.md` + +#### Scenario: Author cannot complete without a description + +- **WHEN** the author attempts to complete the wizard without providing a description +- **THEN** the system SHALL NOT allow completion +- **AND** the system SHALL indicate that a description is required + +#### Scenario: Scoped facet identity is accepted + +- **WHEN** the author enters `@acme/deploy-tools` as the facet name +- **THEN** the system SHALL accept the name as a valid facet identity + +#### Scenario: Invalid facet identity is rejected + +- **WHEN** the author enters `@acme/Deploy_Tools` as the facet name +- **THEN** the system SHALL indicate the name is invalid +- **AND** the system SHALL NOT accept the invalid name + +#### Scenario: Asset names are validated in real time + +- **WHEN** the author enters an asset name containing uppercase letters, spaces, underscores, slashes, or an at sign +- **THEN** the system SHALL indicate the name is invalid +- **AND** the system SHALL NOT accept the invalid name + +#### Scenario: Duplicate asset names within a type are rejected + +- **WHEN** the author attempts to add a skill with the same name as an existing skill +- **THEN** the system SHALL reject the duplicate name + +#### Scenario: Skill and command with the same name are rejected + +- **WHEN** the author attempts to add a command with the same name as an existing skill, or a skill with the same name as an existing command +- **THEN** the system SHALL reject the duplicate name +- **AND** the system SHALL indicate that skills and commands share one namespace + +#### Scenario: An agent may share a name with a skill + +- **WHEN** the author adds a skill named "viper-plans" and an agent named "viper-plans" +- **THEN** the system SHALL accept both assets without error + +#### Scenario: Author edits an existing asset name + +- **WHEN** the author selects an existing asset and changes its name to a valid, unique name +- **THEN** the system SHALL update the asset name + +#### Scenario: Author removes an asset + +- **WHEN** the author removes a previously added asset +- **THEN** the asset SHALL no longer appear in the wizard or the confirmation summary + +#### Scenario: Author exits the wizard with unsaved work + +- **WHEN** the author triggers an exit action during the wizard +- **THEN** the system SHALL confirm the author's intent to exit +- **AND** if the author confirms exit, the system SHALL not create any files or directories + +#### Scenario: Version field accepts valid SemVer input + +- **WHEN** the author sets the version to a valid SemVer value (e.g., "1.0.0" or "100.2.1") +- **THEN** the system SHALL accept the version + +#### Scenario: Version field rejects invalid input + +- **WHEN** the author enters a version that does not match the N.N.N pattern +- **THEN** the system SHALL indicate the version is invalid + +#### Scenario: Version defaults to 0.0.0 + +- **WHEN** the author does not change the version field +- **THEN** the manifest SHALL contain version `0.0.0` + +#### Scenario: New facet defaults to public visibility intent + +- **WHEN** an author creates a facet interactively and accepts the default privacy choice +- **THEN** the generated manifest SHALL omit `private` +- **AND** the confirmation summary SHALL show the facet as public + +#### Scenario: New private facet writes private true + +- **WHEN** an author creates a facet interactively and selects private visibility intent +- **THEN** the generated manifest SHALL contain `private: true` +- **AND** the confirmation summary SHALL show the facet as private + +#### Scenario: Author selects private then reverts to public before completing + +- **WHEN** an author creates a facet interactively, selects private visibility intent, and then changes the privacy choice back to public before completing the wizard +- **THEN** the generated manifest SHALL omit `private` +- **AND** the confirmation summary SHALL show the facet as public + +#### Scenario: Target directory already contains a manifest + +- **WHEN** the author runs the create wizard and a manifest already exists in the target directory +- **THEN** the system SHALL warn the author and ask for confirmation before overwriting + +## ADDED Requirements + +### Requirement: Facets declare supplementary files explicitly + +An author SHALL be able to declare non-asset files that ship in the built facet: a top-level list of exact repo-relative paths for archive-only files (for example `README.md`, `LICENSE`, `DEVELOPMENT.md`), and a per-skill list of exact paths (relative to the skill's directory) for companion files that belong to that skill. Declarations SHALL enumerate exact paths; the system SHALL NOT recursively auto-discover or auto-package undeclared files. Only declared files SHALL be included in the built archive. + +#### Scenario: A declared root file ships with the facet + +- **WHEN** an author declares `README.md` in the top-level supplementary-file list and the file exists +- **THEN** the built archive SHALL include `README.md` +- **AND** its hash SHALL be recorded in the build manifest + +#### Scenario: A declared skill companion ships beneath its skill + +- **WHEN** an author declares `references/art.md` in skill `cowsay`'s file list and the file exists at the corresponding path in the skill's directory +- **THEN** the built archive SHALL include the companion beneath the skill's directory +- **AND** the companion SHALL be associated with skill `cowsay`, not with any other skill + +#### Scenario: Undeclared files in the source tree are not packaged + +- **WHEN** the facet source tree contains a file that is neither a conventional asset file nor declared in any supplementary-file list +- **THEN** the build SHALL NOT include that file in the archive +- **AND** the build SHALL NOT warn or fail solely because the undeclared file exists + +### Requirement: Build validates supplementary file declarations + +The system SHALL validate every supplementary-file declaration during build and fail with a structured error for each violation. Validation SHALL reject: + +- a declared path that is empty, absolute, contains `.` or `..` segments, backslashes, NUL bytes, or a drive or URL-like prefix; +- a declared path that does not resolve, through existing parent directories, to a regular file inside the facet root — symlinks and hard links SHALL be rejected by resolved identity, not just by spelling; +- the exact root path `facet.json` (the embedded manifest's own path); the basename `facet.json` MAY appear at any other declared path; +- a top-level declaration that resolves under `skills/`; +- a per-skill declaration naming the skill's primary file or resolving outside its skill's directory; +- a per-skill declaration on a skill that is not declared in the manifest; +- a declaration colliding with a conventional primary asset path; +- two declarations that collide by exact spelling, canonical Unicode form, portable case folding, resolved source identity, or file/directory prefix conflict (a path declared as both a file and a parent of another entry). + +Each violation SHALL identify the declared path and the declaration site so the author can correct it without guessing. + +#### Scenario: A traversal path is rejected + +- **WHEN** an author declares a supplementary path containing `..` (for example `../secrets.txt`) +- **THEN** the build SHALL fail with a structured error identifying the unsafe path + +#### Scenario: A symlinked supplementary file is rejected + +- **WHEN** a declared supplementary path resolves to a symbolic link, or to a file outside the facet root through a symlinked parent +- **THEN** the build SHALL fail with a structured error identifying the path and the reason + +#### Scenario: A top-level declaration under skills/ is rejected + +- **WHEN** an author declares `skills/cowsay/notes.md` in the top-level supplementary-file list +- **THEN** the build SHALL fail with an error directing the author to declare the file on the owning skill instead + +#### Scenario: A declaration colliding with an asset path is rejected + +- **WHEN** an author declares a top-level supplementary file at a path that equals a conventional asset path derived from the manifest (for example `agents/reviewer.md` while agent `reviewer` is declared) +- **THEN** the build SHALL fail with a structured error identifying the collision + +#### Scenario: Case-folded or Unicode-aliased declarations are rejected + +- **WHEN** two declarations differ only by letter case or by Unicode normalization form +- **THEN** the build SHALL fail with a structured error identifying both colliding declarations + +#### Scenario: The root manifest path cannot be declared + +- **WHEN** an author declares the exact path `facet.json` as a top-level supplementary file +- **THEN** the build SHALL fail with a structured error +- **AND** a declaration of `facet.json` at a non-root path (for example inside a skill's examples directory) SHALL remain valid + +### Requirement: Asset names are validated against the single-segment convention during authoring + +The scaffolding, editing, and build workflows SHALL validate every skill, command, and agent name against the published single-segment asset-name convention (lowercase ASCII letters, digits, and hyphens; 1–64 characters; no leading, trailing, or consecutive hyphens; no slashes). A source manifest using a slash-containing or otherwise nonconforming asset name SHALL fail the build with a structured error naming the asset and the violated rule, before any output is written. Previously published legacy archives remain consumable; only rebuilding requires renaming. + +#### Scenario: A slash-namespaced asset name fails the build actionably + +- **WHEN** an author builds a facet whose manifest declares a skill named `tools/review` +- **THEN** the build SHALL fail before writing any output +- **AND** the error SHALL identify the asset name and state that current-format asset names are single segments + +#### Scenario: The wizards reject nonconforming asset names in real time + +- **WHEN** an author enters an asset name containing a slash, uppercase letter, or consecutive hyphens in the create or edit wizard +- **THEN** the system SHALL indicate the name is invalid +- **AND** the system SHALL NOT accept the invalid name + +#### Scenario: The wizards reject a skill/command name collision + +- **WHEN** an author attempts to add a command whose name equals an existing skill's name (or vice versa) in the create or edit wizard +- **THEN** the system SHALL reject the duplicate name +- **AND** the system SHALL indicate that skills and commands share one namespace + +### Requirement: Create offers first-class README authoring + +The interactive scaffolding wizard SHALL include a dedicated README step, separate from asset management. README creation SHALL be enabled by default and SHALL be optional: the author MAY disable it. The wizard SHALL seed editable `README.md` content from the facet name and description and SHALL allow the author to open and edit that content before confirmation. The confirmation summary SHALL list `README.md` explicitly when enabled. Upon confirmation with README enabled, the system SHALL write `README.md` and add its exact path to the top-level supplementary-file declaration in the same atomic apply as the rest of the scaffold. The generated content is an initial value only: subsequent identity edits SHALL NOT regenerate or overwrite authored README content. The scaffolded project SHALL remain immediately buildable. + +#### Scenario: Default scaffold includes a README + +- **WHEN** an author completes the create wizard without changing the README choice +- **THEN** the created project SHALL contain `README.md` seeded from the facet name and description +- **AND** the manifest SHALL declare `README.md` as a top-level supplementary file +- **AND** running build on the fresh project SHALL succeed + +#### Scenario: Author edits the seeded README before confirming + +- **WHEN** an author opens the README step and edits the seeded content before confirming the wizard +- **THEN** the created `README.md` SHALL contain the author's edited content + +#### Scenario: Author disables README creation + +- **WHEN** an author disables the README step and completes the wizard +- **THEN** the created project SHALL NOT contain `README.md` +- **AND** the manifest SHALL NOT declare a README + +#### Scenario: A later identity edit does not regenerate the README + +- **WHEN** an author changes the facet name or description in a later edit session +- **THEN** the existing `README.md` content SHALL be preserved unchanged unless the author explicitly edits it + +### Requirement: Edit provides a dedicated README panel for both conventional paths + +The editing command SHALL recognize the exact root paths `README.md` and extensionless `README` as first-class README files and SHALL present them in a dedicated facet-level README panel, separate from generic supplementary-file reconciliation, so neither path appears twice. For each recognized path the offered actions SHALL depend on its state: present and declared — Edit or Remove; present but undeclared — Adopt or Edit-and-Adopt; declared but missing — Scaffold at the same path or Remove Declaration; absent and undeclared — Create, defaulting to `README.md`. If both paths exist, the panel SHALL show both independently and SHALL NOT silently ignore or overwrite either. Adopt SHALL preserve existing bytes unless the author explicitly edits them. Remove SHALL queue both the file deletion and the declaration removal. All README operations SHALL be queued and applied only at the existing edit confirmation, and the confirmation summary SHALL identify the exact README path and operation. + +#### Scenario: An undeclared README is offered for adoption + +- **WHEN** an author runs edit on a facet whose root contains `README.md` that is not declared in the manifest +- **THEN** the README panel SHALL offer Adopt and Edit-and-Adopt +- **AND** choosing Adopt SHALL queue the declaration addition while preserving the file's bytes + +#### Scenario: A declared but missing README offers scaffold-or-remove + +- **WHEN** the manifest declares `README.md` but the file does not exist on disk +- **THEN** the README panel SHALL offer scaffolding a new `README.md` at that path or removing the declaration + +#### Scenario: Both README.md and README exist + +- **WHEN** a facet root contains both `README.md` and extensionless `README` +- **THEN** the README panel SHALL present each path independently with its own state and actions +- **AND** neither file SHALL be silently ignored or overwritten + +#### Scenario: README removal is transactional + +- **WHEN** an author queues removal of a declared `README.md` and then exits the edit session without confirming +- **THEN** the file SHALL remain on disk and the declaration SHALL remain in the manifest + +### Requirement: Edit reconciles supplementary-file declarations against disk + +The editing command SHALL extend its reconciliation to supplementary files. It SHALL detect undeclared files inside a declared skill's directory and offer to add each to that skill's companion declaration. It SHOULD detect common root-level supplementary files (for example `LICENSE`) and offer to declare them, while routing `README.md` and `README` exclusively through the dedicated README panel. For any declared supplementary file other than a README that has vanished from disk, edit SHALL offer scaffold-or-remove, mirroring the missing-asset flow. All supplementary reconciliation actions SHALL be queued and applied only at the edit confirmation. + +#### Scenario: An undeclared file inside a skill directory is offered for adoption + +- **WHEN** an author runs edit and `skills/cowsay/references/art.md` exists on disk but is not declared in skill `cowsay`'s file list +- **THEN** the system SHALL offer to add `references/art.md` to skill `cowsay`'s declaration +- **AND** skipping the offer SHALL leave the file on disk and undeclared + +#### Scenario: A vanished declared companion offers scaffold-or-remove + +- **WHEN** skill `cowsay` declares `references/art.md` but the file no longer exists on disk +- **THEN** the system SHALL offer to scaffold a file at that path or remove the declaration + +#### Scenario: Adopted supplementary files are added at the correct declaration site + +- **WHEN** an author adopts a discovered file inside a skill directory and a discovered root-level `LICENSE` +- **THEN** the skill file SHALL be added to that skill's per-skill declaration +- **AND** `LICENSE` SHALL be added to the top-level declaration diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/installation/spec.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/installation/spec.md new file mode 100644 index 00000000..cb41ff26 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/installation/spec.md @@ -0,0 +1,175 @@ +## MODIFIED Requirements + +### Requirement: Lockfile declares a version + +The lockfile SHALL include a top-level `lockfileVersion`. The current version SHALL be `0.2`. Version recognition SHALL use exact equality, never numeric ordering: the legacy numeric version `1` SHALL be recognized only as the previous schema, and `0.2` SHALL identify the current schema. A lockfile declaring any other version, or omitting the version, SHALL be rejected with an actionable error. + +A normal (non-frozen) install MAY migrate a verified legacy version-`1` lockfile to `0.2`, recording per-file integrity from the freshly verified content. A frozen install of a legacy lockfile SHALL retain legacy behavior and SHALL NOT rewrite the lockfile. + +#### Scenario: Missing lockfile version + +- **WHEN** a lockfile omits `lockfileVersion` +- **THEN** the system SHALL reject the lockfile + +#### Scenario: Current lockfile version is accepted + +- **WHEN** a lockfile declares `lockfileVersion: 0.2` and satisfies the current schema +- **THEN** the system SHALL accept the lockfile + +#### Scenario: A legacy lockfile migrates on a normal install + +- **WHEN** a project has a legacy version-`1` lockfile +- **AND** the user runs a normal (non-frozen) install that verifies the locked content +- **THEN** the system SHALL write the lockfile as version `0.2`, including per-file integrity records derived from the verified content + +#### Scenario: A frozen install does not rewrite a legacy lockfile + +- **WHEN** a project has a legacy version-`1` lockfile +- **AND** the user runs install in frozen-lockfile mode +- **THEN** the system SHALL apply the legacy lockfile's behavior +- **AND** the system SHALL NOT write the lockfile + +#### Scenario: An unrecognized lockfile version is rejected + +- **WHEN** a lockfile declares a `lockfileVersion` that is neither the legacy `1` nor `0.2` +- **THEN** the system SHALL fail with an error identifying the unsupported version +- **AND** the system SHALL leave the project unchanged + +### Requirement: Each facet entry lists its assets, adapter-agnostically + +Every facet entry SHALL include an `assets` array whose members carry the asset's adapter-agnostic identity — `scope`, `type`, `name` — plus a required, deterministically sorted `files` array of `{ path, integrity }` records covering every file the asset materializes. `scope` SHALL be one of `system | user | project`. `type` SHALL be one of `skill | agent | command`. Each file record's `path` SHALL be the canonical inner-archive path and its `integrity` the `sha256:` hash of that archive entry's canonical bytes. A skill entry's `files` SHALL contain the skill's primary file plus every declared companion file; an agent or command entry's `files` SHALL contain exactly its one primary file. Archive-only supplementary files SHALL NOT appear in any asset's `files` and SHALL NOT form asset entries of their own. No per-adapter fields live here — the installer applies the asset set to every selected adapter ("same thing per adapter"), and recorded hashes SHALL be adapter-agnostic canonical hashes, never adapter-encoded representations. + +#### Scenario: Valid asset entry with file records + +- **WHEN** an asset entry has `scope: "user"`, `type: "skill"`, `name: "planning"`, and a sorted `files` array pinning the skill's primary file and its declared companions +- **THEN** the system SHALL accept the entry + +#### Scenario: Unknown asset scope + +- **WHEN** an asset entry has `scope: "global"` +- **THEN** the system SHALL reject the lockfile + +#### Scenario: Unknown asset type + +- **WHEN** an asset entry has `type: "hook"` +- **THEN** the system SHALL reject the lockfile + +#### Scenario: An asset entry without file records is rejected + +- **WHEN** a `0.2` lockfile asset entry omits its `files` array +- **THEN** the system SHALL reject the lockfile + +#### Scenario: Supplementary files never become asset entries + +- **WHEN** the system writes a lockfile entry for a facet that ships archive-only supplementary files +- **THEN** those files SHALL NOT appear as members of the `assets` array +- **AND** they SHALL NOT appear inside any asset's `files` records + +## ADDED Requirements + +### Requirement: Install reconciles per-file integrity before any write + +Before materializing a facet, the system SHALL require exact agreement among: (1) the lockfile's facet-level integrity and the recomputed archive integrity; (2) the lockfile's asset identities and the verified materialization plan derived from the archive; (3) each lockfile asset's complete file path set and the files that asset actually owns in the verified archive; and (4) each recorded per-file integrity, the recomputed hash of the corresponding archive entry, and the archive's own verified per-entry hash record. Any disagreement SHALL fail the install before any asset is written, with structured failure data identifying the facet, the asset, the exact canonical path, the expected integrity, and the observed integrity when available. In frozen-lockfile mode a disagreement SHALL fail without rewriting the lockfile; in normal mode the system MAY write a new lockfile entry only after all checks pass against the newly resolved artifact. + +#### Scenario: A tampered companion file is caught before materialization + +- **WHEN** the system installs a locked facet whose archive entry for one skill companion no longer hashes to the lockfile's recorded per-file integrity +- **THEN** the system SHALL abort the install before writing any asset +- **AND** the failure SHALL identify the facet, the owning skill, the exact companion path, and the expected and observed hashes + +#### Scenario: A lockfile file set that disagrees with the archive is caught + +- **WHEN** a locked skill's recorded file path set differs from the set of files that skill owns in the verified archive (a missing companion record or an extra one) +- **THEN** the system SHALL abort the install with a structured failure identifying the divergent paths + +#### Scenario: Drift reports name the exact locked path + +- **WHEN** the system checks materialized state against the lockfile and a materialized file no longer matches its recorded canonical integrity +- **THEN** the report SHALL identify the exact locked path that drifted +- **AND** the report SHALL NOT collapse the drift to a facet-level or asset-level mismatch without the path + +### Requirement: Skill companion files materialize and remove atomically with their skill + +Declared skill companion files SHALL be installed with their owning skill as one all-or-nothing unit: after a successful install, the skill's primary file and every declared companion are present; after a failed install, no partial skill state remains. Updating a skill SHALL replace its companion set — previously installed companions absent from the new set SHALL be removed in the same operation. Removing a skill SHALL remove its primary file and every companion the system installed for it, and SHALL NOT delete files inside the skill's storage location that the system did not install. Files the install writes SHALL be committed together with the lockfile and receipt in the same transaction; a failure SHALL roll back materialized files, lockfile, and receipt together. + +#### Scenario: A skill with companions installs completely or not at all + +- **WHEN** the system installs a skill declaring three companion files and any write fails partway +- **THEN** the skill's storage location SHALL NOT retain a partial bundle +- **AND** the manifest, lockfile, and receipt SHALL remain as they were before the operation + +#### Scenario: An update removes companions dropped from the declaration + +- **WHEN** an installed skill previously included companion `references/old.md` +- **AND** the newly installed version of that skill no longer declares it +- **THEN** the update SHALL remove `references/old.md` from the skill's storage location +- **AND** the remaining companions SHALL match the new declaration exactly + +#### Scenario: Removing a skill preserves unowned files + +- **WHEN** a user removes a facet whose skill directory also contains a file the system never installed +- **THEN** the system SHALL delete the skill's primary file and its installed companions +- **AND** the system SHALL NOT delete the unowned file + +### Requirement: Archive-only supplementary files ship but are never materialized + +Files declared outside skill directories — such as a root `README.md`, `LICENSE`, or extra files alongside agents and commands — SHALL travel in the installed facet's verified archive but SHALL NOT be written to any adapter tree or other install destination. They SHALL NOT acquire an install scope, an asset type, or any independent installable identity. Their integrity SHALL remain protected by the facet-level archive integrity. + +#### Scenario: A facet with a README installs without writing it + +- **WHEN** a user installs a facet whose archive contains a declared root `README.md` +- **THEN** the install SHALL succeed +- **AND** no `README.md` SHALL be written to any adapter tree or install destination +- **AND** the facet's assets SHALL materialize normally + +#### Scenario: Supplementary files do not affect the installed asset set + +- **WHEN** two facets differ only in their archive-only supplementary files +- **THEN** their materialized asset trees SHALL be identical + +### Requirement: The receipt records the complete owned file set of each materialized asset + +For every materialized asset, the receipt SHALL record the complete set of files the system installed for it — for a skill, the primary file and every installed companion — mirroring the lockfile ownership that was successfully committed. This SHALL be sufficient to remove the asset's every installed file later without the cache, the network, or the lockfile, including after a pulled lockfile change drops the entry. The receipt SHALL remain adapter-agnostic and SHALL NOT store adapter-encoded content hashes. The receipt SHALL remain untrusted input: recorded paths SHALL be validated for containment before deletion, and a recorded path resolving outside the project's adapter trees SHALL NOT be deleted. A legacy receipt that predates companion support MAY be interpreted as recording primary files only, because no companions could have been installed under it. + +#### Scenario: Offline removal deletes every owned companion + +- **WHEN** a facet whose skill installed companion files is removed while the cache is empty and the registry unreachable +- **THEN** the system SHALL delete the skill's primary file and every companion recorded in the receipt +- **AND** the removal SHALL succeed without network access + +#### Scenario: A pulled change that drops a multi-file skill cleans up its companions + +- **WHEN** a change pulled from version control removes a facet with a multi-file skill from the manifest and lockfile +- **AND** the receipt records that skill's installed file set +- **THEN** the next install SHALL remove the primary file and every recorded companion +- **AND** no companion SHALL be left orphaned on disk + +#### Scenario: A receipt path outside the adapter trees is never deleted + +- **WHEN** a receipt records a companion path that resolves outside the project's adapter trees +- **THEN** the system SHALL NOT delete that path +- **AND** the system SHALL report the invalid entry +- **AND** valid recorded paths SHALL still be processed normally + +### Requirement: Unsupported archive formats fail install with actionable guidance + +When an install encounters a facet artifact whose declared archive format version the system does not support, the system SHALL fail with a structured error carrying the observed version and the supported versions, and SHALL leave the manifest, lockfile, receipt, and adapter state unchanged. The rendered error SHALL tell the user to update the tool: for a known newer format, it SHALL name the minimum release that supports it; for an unknown future format, it SHALL advise updating to the latest release without inventing a minimum version. + +#### Scenario: A newer known format produces upgrade guidance + +- **WHEN** a user installs a facet whose archive declares a format version this release does not support but a published newer release does +- **THEN** the install SHALL fail with a structured unsupported-version error +- **AND** the rendered message SHALL name the minimum release that supports the format +- **AND** the project SHALL remain unchanged + +#### Scenario: An unknown future format advises updating + +- **WHEN** a user installs a facet whose archive declares a format version unknown to any published release this system knows of +- **THEN** the install SHALL fail with a structured unsupported-version error +- **AND** the rendered message SHALL advise updating to the latest release + +#### Scenario: Legacy archives continue to install + +- **WHEN** a user installs a facet whose archive uses the legacy asset-only format +- **THEN** the install SHALL verify and materialize it under the legacy format's rules +- **AND** the install SHALL succeed for a valid legacy archive diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol/spec.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol/spec.md new file mode 100644 index 00000000..3d79b335 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol/spec.md @@ -0,0 +1,31 @@ +## MODIFIED Requirements + +### Requirement: Protocol requirements evolve under semantic-versioning discipline + +The protocol's published surface SHALL evolve under semantic-versioning discipline, with the pre-1.0 exception below. Within a compatibility level, requirements MAY be added, but existing requirements SHALL NOT be removed, made stricter, or changed in any way that would cause a previously-conforming system to become non-conforming. + +While a published protocol package remains pre-1.0, a backward-incompatible change SHALL be released in a new **minor** version of that package, and the minor-version release notes SHALL describe what behavior is no longer accepted. From 1.0 onward, backward-incompatible changes SHALL only be made in a new **major** version. In both regimes, the previous compatibility level SHALL remain available so existing consumers continue to function. + +#### Scenario: Adding a new optional field within a major version + +- **WHEN** a new optional field is added to a manifest schema within a minor or patch release +- **THEN** systems built against the previous version SHALL continue to be conforming +- **AND** the new field SHALL NOT be required for conformance until a future breaking release + +#### Scenario: Tightening a constraint requires a breaking release + +- **WHEN** a previously-permitted value is rejected by a new requirement (e.g., a previously valid name pattern is narrowed) +- **THEN** the change SHALL be released only as part of a breaking release — a new minor version while the package is pre-1.0, or a new major version from 1.0 onward +- **AND** the release notes SHALL describe what behavior is no longer accepted + +#### Scenario: Removing a requirement is a breaking change + +- **WHEN** an existing requirement is removed from the protocol +- **THEN** the removal SHALL only occur in a breaking release (pre-1.0 minor, or 1.0+ major) +- **AND** the previous compatibility level SHALL remain available so existing consumers continue to function + +#### Scenario: A pre-1.0 breaking change ships in a minor release + +- **WHEN** a backward-incompatible protocol change (such as a new archive format version) is published while the protocol package is pre-1.0 +- **THEN** the change SHALL ship in the package's next minor release rather than a major release +- **AND** the release notes SHALL identify the breaking change and the migration path diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__content-hashing/spec.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__content-hashing/spec.md new file mode 100644 index 00000000..9aae2086 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__content-hashing/spec.md @@ -0,0 +1,116 @@ +## MODIFIED Requirements + +### Requirement: Content hashes are computed for individual text assets + +The system SHALL compute a SHA-256 content hash for every entry that ships in the inner archive: the facet manifest, each resolved text asset (skills, agents, commands), each declared skill companion file, and each declared archive-only supplementary file. Text asset hashes SHALL be computed from the file's resolved string content encoded as UTF-8. Supplementary-file hashes (skill companions and archive-only files) SHALL be computed from the file's exact bytes, with no decoding, normalization, or transformation. The hash format SHALL be `sha256:` (per [CLI-4](https://www.notion.so/exmachina-co/CLI-4)). + +#### Scenario: Per-entry hashes computed during build + +- **WHEN** a facet is built successfully with two skills, one agent, one declared skill companion file, and a declared root `README.md` +- **THEN** the build output SHALL include a content hash for each of the two skill files, the agent file, the companion file, the `README.md`, and the facet manifest +- **AND** each hash SHALL be in `sha256:` format + +#### Scenario: Supplementary files are hashed from exact bytes + +- **WHEN** a declared supplementary file contains binary content or unusual line endings +- **THEN** its content hash SHALL be computed from the file's exact bytes +- **AND** no normalization SHALL be applied before hashing + +#### Scenario: Identical content produces identical hashes + +- **WHEN** two entries contain identical resolved content +- **THEN** their content hashes SHALL be identical + +#### Scenario: Any content change produces a different hash + +- **WHEN** any entry's content changes by even a single byte +- **THEN** the content hash SHALL differ from the previous hash + +### Requirement: Build output is assembled into a compressed archive + +The system SHALL assemble all resolved build output into a two-layer archive file with the extension `.facet`. The outer layer SHALL be an uncompressed tar containing exactly two entries: `build-manifest.json` and `archive.tar.gz`. The inner `archive.tar.gz` SHALL be a gzip-compressed tar containing the facet manifest, all resolved text asset files, all declared skill companion files at their paths beneath their skill's directory, and all declared archive-only supplementary files at their declared paths. Every inner entry SHALL be derivable from a declaration in the facet manifest; the system SHALL NOT include any file that is not so derivable. The archive filename SHALL follow the pattern `-.facet` where `name` and `version` come from the facet manifest. + +#### Scenario: Successful build produces a self-contained .facet archive + +- **WHEN** a facet named "example-facet" at version "1.0.0" is built successfully +- **THEN** the system SHALL write `dist/example-facet-1.0.0.facet` +- **AND** the `.facet` file SHALL be an uncompressed tar archive +- **AND** the tar SHALL contain exactly two entries: `build-manifest.json` and `archive.tar.gz` + +#### Scenario: Inner archive contains all declared assets and supplementary files + +- **WHEN** a facet with two skills (one declaring a companion `references/api.md`), one agent, one command, and a declared root `README.md` is built +- **THEN** the `archive.tar.gz` entry within the `.facet` file SHALL be a gzip-compressed tar +- **AND** the inner tar SHALL contain the facet manifest, both skill files, the companion file at its path beneath its skill's directory, the agent file, the command file, and `README.md` + +#### Scenario: Inner archive does not contain undeclared files + +- **WHEN** a facet is built from a source tree containing files that are neither conventional asset files nor declared supplementary files +- **THEN** the inner `archive.tar.gz` SHALL contain only the facet manifest, resolved asset files, and declared supplementary files +- **AND** the undeclared files SHALL NOT be included in the archive + +#### Scenario: Inner archive name is fixed + +- **WHEN** any facet is built, regardless of name or version +- **THEN** the inner archive entry SHALL be named `archive.tar.gz` + +### Requirement: A build manifest records content hashes + +The system SHALL produce a build manifest named `build-manifest.json` embedded inside the `.facet` archive as an entry in the outer tar. The manifest SHALL contain a `facetVersion` field set to `0.2` (number), an `archive` field set to `"archive.tar.gz"`, an `integrity` field with the integrity hash, and a `files` object mapping every inner-archive entry path — the facet manifest, every asset file, and every supplementary file — to its content hash. The key set of `files` SHALL exactly equal the set of inner-archive entry paths. The manifest SHALL NOT contain the legacy `assets` map. The manifest SHALL be a flat JSON object. Every build SHALL emit `facetVersion: 0.2`, whether or not the facet declares supplementary files; the system SHALL NOT conditionally emit the legacy format. + +#### Scenario: Build manifest is embedded in the .facet archive + +- **WHEN** a facet is built successfully +- **THEN** the `.facet` file SHALL contain a `build-manifest.json` entry in the outer tar +- **AND** the manifest SHALL contain a `facetVersion` field set to `0.2` +- **AND** the manifest SHALL contain an `archive` field set to `"archive.tar.gz"` +- **AND** the manifest SHALL contain an `integrity` field with the integrity hash +- **AND** the manifest SHALL contain a `files` object mapping every inner-archive entry path to its content hash + +#### Scenario: An asset-only facet still emits the current format + +- **WHEN** a facet declaring no supplementary files is built +- **THEN** the build manifest SHALL declare `facetVersion: 0.2` +- **AND** the `files` map SHALL cover the facet manifest and every asset file + +#### Scenario: Build manifest integrity hash matches inner archive contents + +- **WHEN** a consumer extracts `build-manifest.json` and `archive.tar.gz` from the `.facet` file, decompresses `archive.tar.gz`, and hashes the resulting tar bytes +- **THEN** the computed hash SHALL match the `integrity` value in the manifest + +#### Scenario: Build manifest file hashes match inner archive contents + +- **WHEN** a consumer extracts `archive.tar.gz` from the `.facet` file, decompresses it, and hashes each individual entry +- **THEN** each computed hash SHALL match the corresponding entry in the manifest's `files` map +- **AND** no inner-archive entry SHALL lack a corresponding `files` record +- **AND** no `files` record SHALL lack a corresponding inner-archive entry + +#### Scenario: Build manifest can be read without decompressing the inner archive + +- **WHEN** a consumer parses the outer tar of a `.facet` file +- **THEN** the consumer SHALL be able to read `build-manifest.json` directly from the outer tar entries +- **AND** the consumer SHALL NOT need to decompress `archive.tar.gz` to access the manifest + +## ADDED Requirements + +### Requirement: Supplementary file content is archived verbatim + +The system SHALL read, hash, and archive declared supplementary files (skill companions and archive-only files) as opaque bytes. The archived bytes SHALL be identical to the source file's bytes: no front-matter processing, no line-ending normalization, no character-encoding transformation, and no empty-content rejection. Binary content SHALL be permitted. + +#### Scenario: A binary supplementary file round-trips byte-identically + +- **WHEN** a facet declaring a binary supplementary file (for example an image) is built +- **THEN** the archived entry's bytes SHALL be identical to the source file's bytes +- **AND** extracting the entry SHALL reproduce the original file exactly + +#### Scenario: An empty supplementary file is permitted + +- **WHEN** a facet declares a zero-byte supplementary file that exists on disk +- **THEN** the build SHALL succeed +- **AND** the archive SHALL contain the empty entry with a hash of its (empty) bytes + +#### Scenario: Front matter in a supplementary file is preserved + +- **WHEN** a declared supplementary file begins with text that resembles YAML front matter +- **THEN** the archived bytes SHALL retain that text unmodified +- **AND** no front-matter stripping SHALL be applied diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__integrity/spec.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__integrity/spec.md new file mode 100644 index 00000000..8165a35d --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__integrity/spec.md @@ -0,0 +1,134 @@ +## MODIFIED Requirements + +### Requirement: A single archive-verification operation produces a structured result for any consumer of a built `.facet` + +A facet-compatible system SHALL expose, on its protocol surface, a single archive-verification operation that takes the bytes of a built `.facet` and produces a structured result indicating whether the archive is a valid, self-consistent facet artifact. The operation SHALL be the single, shared mechanism by which any facet-compatible system verifies a built archive before treating it as trusted. A system SHALL NOT reimplement the verification chain by stringing together lower-level primitives in a way that allows the implementation to drift from the one specified here. + +The operation SHALL dispatch on the archive's declared format version exactly once: the legacy `0.1` archive format SHALL be verified under its original rules, the current `0.2` format SHALL be verified under the rules below, and any other declared version SHALL produce a structured unsupported-version failure carrying the observed version and the supported versions. A malformed archive of one declared version SHALL NOT be reinterpreted under another version's rules. + +For a current-format archive the operation SHALL: parse the outer container into its two declared entries; decompress the inner archive; validate the raw inner entries **before** constructing any path-keyed collection, rejecting duplicate entry paths, non-regular entries (symlinks, hard links, directories, devices), and unsafe or non-canonical paths as distinct structured failures; validate the embedded facet manifest against the manifest schema; derive the complete expected entry set solely from the embedded facet manifest's declared assets and supplementary files; require the observed entry set to exactly equal the expected set; require the build manifest's per-entry hash map to have exactly one record per expected entry and no others; verify that the recomputed inner-archive content hash equals the integrity value recorded in the build manifest; verify every entry's bytes against its recorded hash; and apply the artifact content rules (no empty declared asset files, single-segment asset names, no name collisions within an asset type, no skill/command name collisions). + +The operation SHALL return a structured pass-or-fail result and SHALL NOT throw on any of these failure modes; failures SHALL be surfaced as data the caller can render or branch on. The successful result SHALL distinguish, for each verified entry, whether it is the manifest, a primary asset file, a skill companion file (and which skill owns it), or an archive-only supplementary file, so that callers cannot confuse installable assets with files that merely travel in the archive. + +Decompression is not part of the protocol surface — the protocol does not perform compression or decompression. The archive-verification operation SHALL accept a caller-supplied decompressor as an input parameter and SHALL use it to decompress the inner archive. The operation SHALL NOT itself decompress, gzip, or perform any ambient input or output. The caller-supplied decompressor SHALL be permitted, but SHALL NOT be required, to enforce a maximum decompressed size; the verification result's failure surface SHALL include a way to report that the decompressor refused to decompress an inner archive whose decompressed size exceeded the caller's allowance. + +#### Scenario: A self-consistent built archive verifies as valid + +- **WHEN** a caller invokes the archive-verification operation with the bytes of a built `.facet` whose build manifest's integrity hash matches the recomputed inner-archive content hash, whose per-entry hash map exactly matches the observed entries and their actual hashes, whose embedded facet manifest is schema-valid, whose entry set exactly equals the set derivable from the embedded manifest, and whose inner content satisfies the artifact content rules +- **THEN** the operation SHALL produce a successful result +- **AND** the successful result SHALL carry the parsed build manifest and the classification of every entry (manifest, primary asset, skill companion with its owning skill, or archive-only supplementary file) + +#### Scenario: A tampered inner archive is rejected + +- **WHEN** a caller invokes the archive-verification operation with the bytes of a built `.facet` whose inner-archive content has been modified after the build manifest was written, so that the recomputed inner-archive content hash no longer equals the build manifest's recorded integrity value +- **THEN** the operation SHALL produce a failure result identifying the integrity mismatch as the reason +- **AND** the operation SHALL NOT throw + +#### Scenario: A per-entry hash mismatch is rejected + +- **WHEN** a caller invokes the archive-verification operation with the bytes of a built `.facet` in which any entry — asset or supplementary file — does not hash to the value recorded for it in the build manifest +- **THEN** the operation SHALL produce a failure result identifying which entry paths failed and the expected and observed hashes +- **AND** the operation SHALL NOT throw + +#### Scenario: Duplicate inner entry paths are rejected before any lossy collapse + +- **WHEN** a caller invokes the archive-verification operation with a crafted archive containing two inner entries with the same path, or two paths that alias each other by Unicode normalization or case folding +- **THEN** the operation SHALL produce a failure result identifying the duplicate or aliased paths +- **AND** the later entry SHALL NOT silently replace the earlier one + +#### Scenario: Non-regular inner entries are rejected + +- **WHEN** a caller invokes the archive-verification operation with an archive containing a symlink, hard link, directory entry, or device entry in the inner archive +- **THEN** the operation SHALL produce a failure result identifying the non-regular entry +- **AND** the operation SHALL NOT throw + +#### Scenario: Unsafe inner entry paths are rejected + +- **WHEN** a caller invokes the archive-verification operation with an archive whose inner entries include a path containing `..` segments, an absolute path, a backslash, a NUL byte, or a drive prefix +- **THEN** the operation SHALL produce a failure result identifying the unsafe path +- **AND** the operation SHALL NOT throw + +#### Scenario: An invalid embedded facet manifest is rejected + +- **WHEN** a caller invokes the archive-verification operation with the bytes of a built `.facet` whose embedded facet manifest does not satisfy the manifest schema +- **THEN** the operation SHALL produce a failure result identifying the embedded manifest as invalid +- **AND** the operation SHALL NOT throw + +#### Scenario: A content rule violation in the inner archive is rejected + +- **WHEN** a caller invokes the archive-verification operation with the bytes of a built `.facet` whose inner content violates the artifact content rules — for example, a declared asset file that is empty, two assets sharing the same name within an asset type, or a skill and command sharing a name +- **THEN** the operation SHALL produce a failure result identifying the content violation +- **AND** the operation SHALL NOT throw + +#### Scenario: An unsupported declared format version is rejected with a structured failure + +- **WHEN** a caller invokes the archive-verification operation with an archive declaring a format version the system does not support +- **THEN** the operation SHALL produce a failure result carrying the observed version and the supported versions +- **AND** the operation SHALL NOT attempt to verify the archive under any other version's rules + +#### Scenario: A caller-supplied decompressor refuses to decompress + +- **WHEN** a caller invokes the archive-verification operation with a decompressor that refuses to decompress an inner archive whose decompressed size exceeds the caller's allowance +- **THEN** the operation SHALL produce a failure result indicating that the decompressor refused +- **AND** the operation SHALL NOT throw + +#### Scenario: A malformed outer container is rejected + +- **WHEN** a caller invokes the archive-verification operation with bytes that cannot be parsed as the canonical two-entry outer container +- **THEN** the operation SHALL produce a failure result identifying the malformed container +- **AND** the operation SHALL NOT throw + +### Requirement: Integrity failures are structured data + +An integrity failure SHALL be returned as structured data identifying the failing check, the expected and observed integrity values, the artifact name, and (where relevant) the entry path. Integrity failures SHALL NOT be returned as opaque error messages or raw exceptions. Entry-level failures SHALL identify the exact inner-archive path that failed, whether that path is an asset file or a supplementary file. + +#### Scenario: A facet-level failure is structured + +- **WHEN** any facet-level integrity check fails (lockfile, cache hit, archive-vs-metadata, content-vs-archive, or git lockfile-vs-built) +- **THEN** the failure SHALL be returned as structured data identifying the check that failed +- **AND** the failure SHALL include the facet name, the expected integrity value, and the observed integrity value + +#### Scenario: An entry-level failure is structured + +- **WHEN** a per-entry hash recorded in the build manifest does not match the corresponding entry's locally-computed hash — whether the entry is an asset file, a skill companion file, or an archive-only supplementary file +- **THEN** the failure SHALL be returned as structured data identifying the entry path +- **AND** the failure SHALL include the facet name, the entry path, the expected hash, and the observed hash + +## ADDED Requirements + +### Requirement: Every inner archive entry is derivable from the embedded manifest + +For a current-format archive, the set of expected inner-archive entries SHALL be derived solely from the embedded facet manifest: the manifest itself, the conventional file paths of its declared assets, its declared skill companion files, and its declared archive-only supplementary files. Verification SHALL reject an archive whose observed entries include any path not in the derived set, and SHALL reject an archive missing any path in the derived set. The build manifest SHALL NOT be a source of membership: an entry listed only in the build manifest's hash map SHALL NOT make that entry expected. + +#### Scenario: An undeclared extra entry is rejected + +- **WHEN** an archive's inner entries include a file that is neither a conventional asset path nor a declared supplementary file of the embedded manifest +- **THEN** verification SHALL produce a failure result identifying the undeclared entry path +- **AND** the archive SHALL NOT be treated as trusted + +#### Scenario: A declared entry missing from the archive is rejected + +- **WHEN** the embedded manifest declares a supplementary file that is absent from the inner archive +- **THEN** verification SHALL produce a failure result identifying the missing declared path + +#### Scenario: A build-manifest-only entry does not expand the expected set + +- **WHEN** an archive's build manifest hash map records a path that the embedded facet manifest does not derive +- **AND** the inner archive contains an entry at that path +- **THEN** verification SHALL reject the archive +- **AND** the build manifest record SHALL NOT legitimize the undeclared entry + +### Requirement: Legacy archives remain verifiable during the compatibility window + +A system SHALL continue to verify legacy `0.1` archives under the rules that were published for that format, including its per-asset hash map and its asset-only entry set, for as long as the compatibility window remains open. Withdrawal of legacy verification SHALL be a separately published breaking change. A current-format archive that fails its own rules SHALL NOT be re-verified under legacy rules, and a legacy archive SHALL NOT be verified under current-format rules. + +#### Scenario: A valid legacy archive verifies successfully + +- **WHEN** a caller invokes the archive-verification operation with a valid legacy `0.1` archive produced before the current format existed +- **THEN** the operation SHALL produce a successful result under the legacy rules + +#### Scenario: A legacy verifier rejects a current-format archive closed + +- **WHEN** a system that supports only the legacy format receives a current-format archive containing supplementary files +- **THEN** the system SHALL reject the archive rather than partially install it +- **AND** the rejection is the correct fail-closed posture for a consumer that cannot enforce the current rules diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__schemas/spec.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__schemas/spec.md new file mode 100644 index 00000000..b0b19407 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/specs/protocol__schemas/spec.md @@ -0,0 +1,216 @@ +## MODIFIED Requirements + +### Requirement: A facet manifest schema is published as part of the protocol + +The shape of a facet manifest (`facet.json`) SHALL be published as a normative schema. Any system that produces a facet manifest SHALL produce one conforming to the published schema. Any system that consumes a facet manifest SHALL validate it against the published schema before treating any value as trusted. The schema SHALL define the required fields, the permitted shapes for skills/agents/commands, the accepted facet identity grammar, the accepted asset-name grammar, the supplementary-file declarations, the optional facet privacy declaration, and the rules for unrecognized fields. + +A facet identity name SHALL be either an unscoped name (``) or a scoped name (`@/`). Each `slug` and `scope` component SHALL satisfy the same component grammar: it MUST be at least 2 characters and at most 64 characters, MUST start with a lowercase ASCII letter, MUST end with a lowercase ASCII letter or ASCII digit, MUST contain only lowercase ASCII letters, ASCII digits, and hyphens, and MUST NOT contain consecutive hyphens. Uppercase letters, non-ASCII characters, underscores, dots, spaces, plus signs, tildes, emoji, and any other character outside the component grammar SHALL be rejected rather than normalized. A facet manifest whose `name` is malformed SHALL be rejected as invalid. + +An asset name (skill, command, or agent) in a current-format manifest SHALL be a single segment following the Agent Skills `name` field convention, normatively interpreted as ASCII: 1–64 lowercase ASCII letters (`a-z`), digits (`0-9`), or hyphens, MUST NOT start or end with a hyphen, and MUST NOT contain consecutive hyphens. `/` SHALL be invalid in every asset name. The same grammar SHALL apply to all three asset types. Skill names and command names SHALL occupy one logical namespace: a manifest declaring a skill and a command with the same name SHALL be rejected with a structured error identifying both declarations. Agent names SHALL remain a separate namespace and MAY equal a skill or command name. Asset names SHALL remain local asset identifiers and SHALL NOT become scoped names. Published naming documentation SHALL identify the Agent Skills `name` convention as the external convention being implemented. + +The schema SHALL define two supplementary-file declaration sites, each enumerating exact relative paths (no patterns): + +- an optional top-level `files` list of repo-relative paths for archive-only supplementary files (for example `README.md`, `LICENSE`); entries MUST NOT resolve under `skills/`; +- an optional per-skill `files` list of paths relative to that skill's directory, declaring companion files that install and remove with the skill; entries MUST NOT name the skill's primary file and MUST resolve below the skill's directory. + +The facet manifest schema SHALL define an optional top-level `private` field. When present, `private` SHALL be a boolean. A manifest with `private: true` SHALL express the author's intent that the facet is private. A manifest with `private: false`, or with no `private` field, SHALL express public-by-default behavior. Validation SHALL NOT inject `private: false` into a manifest that omits the field; omission remains omission in validated data. Values of any non-boolean type SHALL be rejected rather than treated as unknown extension data or coerced to booleans. + +The facet manifest schema SHALL NOT document unsupported composition or server-reference fields as part of the current user-facing manifest contract. Current user-facing manifest documentation SHALL describe only supported manifest behavior and SHALL use the manifest specification page as the canonical place for facet-name grammar. + +#### Scenario: A producer emits a manifest conforming to the published schema + +- **WHEN** a system produces a `facet.json` for distribution +- **THEN** the produced manifest SHALL satisfy every requirement of the published schema +- **AND** another facet-compatible system SHALL accept the manifest after validating it + +#### Scenario: A consumer accepts valid unscoped facet identities + +- **WHEN** a system receives a `facet.json` whose `name` is `ab`, `cowsay`, `julian`, `admin-tester`, `apple-b34r`, or `f-o-s-s-o` +- **THEN** the system SHALL accept the facet identity as valid +- **AND** the accepted identity SHALL remain the facet's canonical name without normalization + +#### Scenario: A consumer accepts a valid scoped facet identity + +- **WHEN** a system receives a `facet.json` whose `name` is `@julian/cowsay` +- **THEN** the system SHALL accept the facet identity as valid +- **AND** both scoped identity components SHALL satisfy the same component grammar as unscoped facet names +- **AND** the scoped identity SHALL remain the facet's canonical name + +#### Scenario: A consumer accepts valid supplementary-file declarations + +- **WHEN** a system receives a `facet.json` declaring top-level `files: ["README.md", "LICENSE"]` and a skill whose `files` list contains `references/api.md` and `scripts/run.ts` +- **THEN** the system SHALL accept the manifest as valid +- **AND** the accepted declarations SHALL remain exact paths without expansion or normalization + +#### Scenario: A consumer rejects a slash-containing asset name in a current-format manifest + +- **WHEN** a system receives a current-format `facet.json` declaring a skill, command, or agent whose name contains `/` (for example `tools/review`) +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error identifying the malformed asset name + +#### Scenario: A consumer rejects asset names outside the single-segment grammar + +- **WHEN** a system receives a current-format `facet.json` declaring an asset named `-review`, `review-`, `re--view`, `Review`, or a name longer than 64 characters +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error identifying the malformed asset name + +#### Scenario: A consumer rejects a skill/command name collision + +- **WHEN** a system receives a `facet.json` declaring both a skill named `review` and a command named `review` +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the structured error SHALL identify both colliding declarations + +#### Scenario: An agent may share a name with a skill + +- **WHEN** a system receives a `facet.json` declaring both a skill named `review` and an agent named `review` +- **THEN** the system SHALL accept the manifest as valid + +#### Scenario: A consumer accepts omitted public privacy declaration + +- **WHEN** a system receives a `facet.json` with no `private` field +- **THEN** the system SHALL accept the manifest as public by default +- **AND** validation SHALL NOT add a `private` field to the accepted manifest data + +#### Scenario: A consumer accepts explicit public privacy declaration + +- **WHEN** a system receives a `facet.json` with `private: false` +- **THEN** the system SHALL accept the manifest as explicitly public +- **AND** the accepted manifest SHALL preserve `private: false` + +#### Scenario: A consumer accepts private privacy declaration + +- **WHEN** a system receives a `facet.json` with `private: true` +- **THEN** the system SHALL accept the manifest as declaring private publish intent +- **AND** the accepted manifest SHALL preserve `private: true` + +#### Scenario: A consumer rejects non-boolean privacy declaration + +- **WHEN** a system receives a `facet.json` whose `private` field is a string, number, object, array, or null +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error indicating that `private` must be boolean + +#### Scenario: A consumer rejects invalid slug components + +- **WHEN** a system receives a `facet.json` whose `name` is empty, `a`, `z`, `A`, `Cowsay`, `1abc`, `-abc`, `abc-`, `abc--def`, `abc_def`, `abc.def`, `abc def`, `éclair`, `gооgle` with Cyrillic homoglyphs, or any component longer than 64 characters +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error indicating that the facet identity is malformed + +#### Scenario: A consumer rejects malformed scoped facet identities + +- **WHEN** a system receives a `facet.json` whose `name` is `@scope`, `@/name`, `@scope/`, `@scope/name/extra`, or `scope/name` +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error indicating that the facet identity is malformed + +#### Scenario: A consumer rejects a manifest that violates the published schema + +- **WHEN** a system receives a `facet.json` that omits a required field or contains a field with the wrong type +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error indicating which constraint was violated + +#### Scenario: A consumer tolerates unrecognized fields + +- **WHEN** a system receives a `facet.json` containing a field not defined in the schema +- **THEN** the system SHALL accept the manifest as valid +- **AND** the system SHALL preserve the unknown field if it later re-emits the manifest + +### Requirement: A build manifest schema is published as part of the protocol + +The shape of a build manifest (`build-manifest.json`) embedded inside a `.facet` archive SHALL be published as a normative, versioned schema. Any system that produces a `.facet` archive SHALL include a build manifest conforming to the schema for the archive format version it emits. Any system that consumes a `.facet` archive SHALL validate the embedded build manifest against the schema for its declared version before trusting its values. + +The current archive format version SHALL be `0.2`. A `0.2` build manifest SHALL declare `facetVersion: 0.2`, the inner archive name, the integrity hash, and a single `files` object mapping every inner-archive entry path — the facet manifest, every primary asset file, and every supplementary file — to its `sha256:` content hash. The `files` map SHALL carry hashes only: whether an entry is an asset or a supplementary file SHALL be derived from the embedded facet manifest, never from the build manifest. A `0.2` build manifest SHALL NOT contain the legacy `assets` map. + +The legacy archive format version `0.1` (with its per-asset `assets` map) SHALL remain published as a legacy input format during the compatibility window. Consumers SHALL dispatch on the declared `facetVersion` exactly once: the legacy schema and rules apply to `0.1`, the current schema and rules apply to `0.2`, and any other version SHALL produce a structured unsupported-version failure carrying the observed version and the supported versions. A malformed manifest of one version SHALL NOT be reinterpreted under another version's schema. + +#### Scenario: A producer embeds a conforming current-format build manifest + +- **WHEN** a system produces a `.facet` archive +- **THEN** the embedded `build-manifest.json` SHALL declare `facetVersion: 0.2` +- **AND** the manifest SHALL declare the inner archive name, the integrity hash, and a `files` map covering every inner-archive entry +- **AND** the manifest SHALL NOT contain an `assets` map + +#### Scenario: A consumer rejects an archive whose build manifest violates its declared schema + +- **WHEN** a system receives a `.facet` archive whose `build-manifest.json` is missing a required field for its declared version or has a malformed integrity value +- **THEN** the system SHALL reject the archive as invalid +- **AND** the system SHALL surface a structured error identifying the violation +- **AND** the system SHALL NOT attempt to reinterpret the manifest under a different version's schema + +#### Scenario: A consumer rejects a version-schema mismatch + +- **WHEN** a system receives a `.facet` archive whose build manifest declares `facetVersion: 0.2` but contains an `assets` map, or declares `facetVersion: 0.1` but contains a `files` map +- **THEN** the system SHALL reject the archive as invalid +- **AND** the system SHALL surface a structured error identifying the violation + +#### Scenario: An unsupported archive format version is a structured failure + +- **WHEN** a system receives a `.facet` archive whose build manifest declares a `facetVersion` that is neither `0.1` nor `0.2` +- **THEN** the system SHALL produce a structured unsupported-version failure +- **AND** the failure SHALL carry the observed version and the versions the system supports + +#### Scenario: A legacy build manifest remains valid during the compatibility window + +- **WHEN** a system receives a `.facet` archive whose build manifest declares `facetVersion: 0.1` and conforms to the legacy schema +- **THEN** the system SHALL accept the build manifest under the legacy schema and rules + +### Requirement: A lockfile schema is published as part of the protocol + +The shape of a lockfile (`facets.lock`) SHALL be published as a normative, versioned schema. Any system that reads, writes, or interprets a lockfile SHALL conform to the published schema. The schema SHALL define the lockfile version, source-provenance fields, identity-and-integrity fields, the asset list with per-file integrity records, and the rules for unrecognized fields. + +The current lockfile version SHALL be `0.2`. Version dispatch SHALL use exact equality, never numeric ordering: the legacy numeric version `1` identifies the previous schema, and `0.2` identifies the current schema. In a `0.2` lockfile, every asset entry SHALL carry its adapter-agnostic identity plus a required, deterministically sorted `files` array of `{ path, integrity }` records, where `path` is the canonical inner-archive path of a materialized file and `integrity` is the `sha256:` hash of that archive entry's exact canonical bytes. A skill entry's `files` SHALL contain the skill's primary file plus every declared companion file. An agent or command entry's `files` SHALL contain exactly its one primary file. Archive-only supplementary files SHALL NOT appear in any asset's `files`; they remain protected by the entry's facet-level integrity. Companion records SHALL NOT be independent assets: they carry no scope, no asset type, and no standalone asset tuple. + +The published schema's source-provenance fields SHALL take a tagged form keyed on the source kind, so that the provenance fields meaningful for each kind are explicit. The published schema SHALL define a registry source that records the registry origin, a git source that records the repository URL and a required resolved commit, and a local source that records the resolved path. A lockfile whose entry source does not declare a recognized kind, or omits a field required for its declared kind (such as a git source without a commit), SHALL NOT satisfy the published schema. Consistent with the lockfile's tolerance of unrecognized fields, a source MAY carry additional unrecognized keys and still satisfy the published schema — forward-compatibility requires that a newer producer's extra fields not break an older consumer. + +#### Scenario: A consumer interprets a lockfile written by a different system + +- **WHEN** a system reads a `facets.lock` written by a different facet-compatible system +- **THEN** the system SHALL interpret every field per the published schema for the lockfile's declared version +- **AND** the system SHALL accept the lockfile as valid input for installation + +#### Scenario: A producer writes a lockfile that any consumer can read + +- **WHEN** a system writes a `facets.lock` after resolving facet sources +- **THEN** the resulting file SHALL declare `lockfileVersion: 0.2` and satisfy the published schema +- **AND** another facet-compatible system SHALL be able to read the file and reproduce the same install state + +#### Scenario: A skill asset entry pins its primary and companion files + +- **WHEN** a system writes a `0.2` lockfile entry for a facet whose skill `review` declares companion files +- **THEN** the skill's asset entry SHALL contain a `files` array listing the skill's primary file and every declared companion path with its `sha256:` integrity +- **AND** the `files` array SHALL be deterministically sorted + +#### Scenario: Single-file assets pin exactly one file + +- **WHEN** a system writes a `0.2` lockfile entry containing an agent or command asset +- **THEN** that asset's `files` array SHALL contain exactly one record naming the asset's primary file and its integrity + +#### Scenario: Archive-only supplementary files never appear as asset file records + +- **WHEN** a system writes a `0.2` lockfile entry for a facet that ships a root `README.md` or other archive-only supplementary file +- **THEN** no asset entry's `files` array SHALL contain that path +- **AND** the facet entry's facet-level integrity SHALL remain the record that pins it + +#### Scenario: An asset entry missing its file records is rejected + +- **WHEN** a system reads a `0.2` lockfile in which an asset entry omits its `files` array +- **THEN** the lockfile SHALL NOT satisfy the published schema +- **AND** the system SHALL reject the lockfile + +#### Scenario: Source provenance is tagged by kind + +- **WHEN** a facet-compatible system reads an entry's source provenance from a `facets.lock` +- **THEN** the source SHALL declare its kind (registry, git, or local) +- **AND** a registry source SHALL record the registry origin and SHALL NOT carry a version specifier +- **AND** a git source SHALL record the repository URL and a required resolved commit, and SHALL NOT record a symbolic ref +- **AND** a local source SHALL record the resolved path + +#### Scenario: A git source missing its required commit is rejected + +- **WHEN** a facet-compatible system reads a lockfile whose entry declares a git source with no commit +- **THEN** the lockfile SHALL NOT satisfy the published schema +- **AND** the system SHALL reject the lockfile + +#### Scenario: A source with extra unrecognized keys is accepted + +- **WHEN** a facet-compatible system reads a lockfile whose entry source declares a recognized kind with all its required fields, plus one or more unrecognized keys +- **THEN** the lockfile SHALL satisfy the published schema +- **AND** the system SHALL accept the lockfile (forward-compatibility with newer producers) diff --git a/openspec/changes/support-non-asset-files/adversarial/artifacts/tasks.md b/openspec/changes/support-non-asset-files/adversarial/artifacts/tasks.md new file mode 100644 index 00000000..4e0cad29 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/artifacts/tasks.md @@ -0,0 +1,139 @@ +> **Before executing any tasks below**, load the `viper-execution-rules` skill for the full VIPER step protocol (step types, execution rules, gating, and hard constraints). + +## 1. Protocol: Manifest Schema, Naming Grammar & Namespaces — Research + +- [ ] 1.1 Explore: the facet manifest schema (`packages/protocol/src/schemas/facet.ts`): current skill/agent/command descriptor shapes, name validation, `parseAssetNameSegment`, and where multi-segment names are parsed today +- [ ] 1.2 Explore: existing manifest validation and collision detection (`packages/protocol/src/build/validate-facets.ts`, `detect-collisions.ts`): how duplicate-name failures are structured and rendered +- [ ] 1.3 Explore: how validated manifest data flows into loaders (`packages/protocol/src/loaders/facet.ts`, `resolvePromptsFromMap`) so schema changes don't break conventional-path inference +- [ ] 1.4 Propose: the schema changes: top-level `files: string[]`, `SkillDescriptor.files?: string[]`, single-segment Agent Skills name grammar (1–64 lowercase ASCII/digit/hyphen, no leading/trailing/consecutive hyphens), skill/command shared-namespace disjointness, agents separate, and the structured error shapes for each new failure class + +## 2. Protocol: Manifest Schema, Naming Grammar & Namespaces — Implementation + +- [ ] 2.1 Implement: the two `files` declaration sites in the facet manifest schema (exact paths only, no globs), with schema docs linking the Agent Skills `name` convention and stating the normative ASCII interpretation +- [ ] 2.2 Implement: the current-format single-segment asset-name grammar as the canonical parser; keep multi-segment parsing isolated to legacy `0.1` verification only +- [ ] 2.3 Implement: skill/command shared-namespace collision validation (structured failure identifying both declarations, e.g. `skills.review` and `commands.review`); agents remain a separate namespace +- [ ] 2.4 Implement: schema-level tests: valid/invalid asset names, namespace collisions, agent name sharing, `files` shape acceptance, glob rejection +- [ ] 2.5 Verify: protocol package tests, types, and lint pass for the schema/naming changes + +## 3. Protocol: Path Grammar & Shared Archive Plan — Research + +- [ ] 3.1 Explore: `collectArchiveEntries` and the build-side membership logic (`packages/protocol/src/build/content-hash.ts`, `validate-content.ts`): how conventional asset paths are derived and hashed today +- [ ] 3.2 Explore: the Step 6b outer-exclusivity allowlist in `packages/protocol/src/integrity/validate-archive.ts`: how the verify-side membership set is constructed and where it would consume a shared derivation +- [ ] 3.3 Propose: the D3 archive-plan operation: one pure function that validates both declaration sites under the D7 grammar and returns a tagged plan (`manifest` / `primary-asset` / `skill-companion` with owning skill / `archive-only`), plus the full D7 validation rules (canonical segments, no NUL/backslash/drive/URL prefixes, regular-files-only with resolved-identity checks, root `facet.json` reservation, conventional-path collisions, site disjointness, Unicode/case-fold/prefix collision classes) as distinct structured `ValidationError`s + +## 4. Protocol: Path Grammar & Shared Archive Plan — Implementation + +- [ ] 4.1 Implement: the D7 path-grammar validators as pure protocol functions with one structured error per failure class +- [ ] 4.2 Implement: the shared archive-plan derivation returning tagged entries; classification comes only from the embedded `facet.json` +- [ ] 4.3 Implement: build-side consumption: `collectArchiveEntries` (and per-entry hashing) derives membership from the archive plan; `ArchiveEntry.content` widens to `string | Uint8Array`; supplementary bytes are read verbatim (no front-matter, normalization, or empty-content rules; binary allowed) +- [ ] 4.4 Implement: source-input validation ordering: all declared inputs (primary + supplementary) validate before any `dist/` cleanup; missing declared files produce structured errors while previous output is preserved +- [ ] 4.5 Implement: the D7 per-failure-class test matrix: traversal, absolute/drive paths, backslashes, NUL, empty/`.` segments, Unicode/case aliases, prefix collisions, symlinks, hard links, duplicates, root-`facet.json` collision, conventional-primary-path collision, missing declarations, undeclared entries, empty and binary supplementary files +- [ ] 4.6 Verify: protocol tests, types, and lint pass for the plan/grammar changes + +## 5. Protocol: Build Manifest 0.2, Verification & Tagged Results — Research + +- [ ] 5.1 Explore: the build-manifest schema and version handling (`packages/protocol/src/schemas/build.ts` or equivalent): where `facetVersion` is declared, how the `assets` map is validated, and where `FACET_ARCHIVE_VERSION` lives +- [ ] 5.2 Explore: `parseFacetArchive` and the full verification pipeline (outer container parse, decompressor injection, integrity checks, per-entry hash checks) and its structured result types +- [ ] 5.3 Explore: existing archive fixtures and how deterministic archives are produced in tests, to plan immutable `0.1` and `0.2` fixture sets +- [ ] 5.4 Propose: the versioned schema design: exact-equality `facetVersion` dispatch at parse time, `0.2` schema with a single all-entry `files` hash map (no `assets` key) and `0.1` retaining `assets` (no `files` key), structured `UNSUPPORTED_FACET_VERSION` failure carrying observed + supported versions, no cross-version fallback, and the tagged successful parse result (primary assets as text, skill companions grouped by owning skill, archive-only bytes) + +## 6. Protocol: Build Manifest 0.2, Verification & Tagged Results — Implementation + +- [ ] 6.1 Implement: the versioned build-manifest schemas with strict exact-equality dispatch and the structured unsupported-version failure +- [ ] 6.2 Implement: `0.2` build-manifest production: flat manifest with `facetVersion: 0.2`, `archive`, `integrity`, and a complete `files` hash map covering `facet.json`, every primary asset, and every supplementary file — emitted unconditionally for asset-only facets too +- [ ] 6.3 Implement: raw tar-header validation before any path-keyed map: duplicate paths, non-regular entries (symlinks, hard links, directories, devices), unsafe/non-canonical paths, and portable-alias collisions each return structured rejections +- [ ] 6.4 Implement: `0.2` verification: derive the expected set from the embedded manifest via the archive plan (never the build manifest), require exact three-way set equality (observed entries, expected plan, `files` keys), then byte-verify every entry hash; keep legacy `0.1` verification byte-for-byte unchanged +- [ ] 6.5 Implement: the tagged parsed-archive result: primary assets exposed as text (empty-content/front-matter rules apply only to them), companions grouped by owning skill as bytes, archive-only entries as bytes; current naming/namespace rules validated for `0.2`, legacy rules retained for `0.1` +- [ ] 6.6 Implement: immutable fixture sets for both archive versions plus tamper cases: undeclared entry, declared-but-missing entry, build-manifest-only entry that must not legitimize membership, per-entry hash mismatch, duplicate/alias/non-regular tar entries, unsupported `0.3` version +- [ ] 6.7 Verify: protocol tests, types, and lint pass for build-manifest and verification changes + +## 7. Protocol: Lockfile 0.2 Schema & Per-File Integrity — Research + +- [ ] 7.1 Explore: the lockfile schema (`packages/protocol/src/schemas/lockfile.ts`) and `LOCKFILE_VERSION`: current version constant, asset-entry shape, and how version dispatch works today +- [ ] 7.2 Propose: the lockfile `0.2` schema: exact-equality dispatch (legacy numeric `1` vs numeric `0.2`), required per-asset `files` arrays sorted by canonical path with `{ path, integrity }` records, skill/agent/command file-set rules, archive-only exclusion, and the structured mismatch result types install will consume + +## 8. Protocol: Lockfile 0.2 Schema & Per-File Integrity — Implementation + +- [ ] 8.1 Implement: the lockfile `0.2` schema and exact-equality version dispatch; keep `FACET_ARCHIVE_VERSION` and `LOCKFILE_VERSION` as separate constants both currently `0.2` +- [ ] 8.2 Implement: the per-file integrity record types and the structured per-file mismatch failure shape (facet, asset, canonical path, expected/actual integrity) +- [ ] 8.3 Implement: schema tests: valid multi-file skill entries, exactly-one-file agent/command entries, archive-only exclusion, missing `files` rejection, legacy numeric `1` selected exactly with no shape-sniffing, unsupported versions rejected with structured data +- [ ] 8.4 Verify: protocol tests, types, and lint pass for lockfile changes + +## 9. Engine Consumer Bridge: Loaders, Lockfile Migration & Receipt 0.2 — Research + +- [ ] 9.1 Explore: engine loaders and cache (`packages/engine/src/loaders/facet.ts`, `cache/`): where parsed archives are consumed and what changes when the parse result becomes tagged +- [ ] 9.2 Explore: lockfile I/O and the frozen-mode guard (`packages/engine/src/install/lockfile-io.ts`, `lockfile-guard.ts`): load/validate/write paths and where migration hooks in +- [ ] 9.3 Explore: the receipt module (`packages/engine/src/install/receipt.ts`): current asset-tuple schema, bootstrap, validation-before-deletion, and project-identity checks +- [ ] 9.4 Propose: the consumer bridge: loaders/cache consume the tagged result; normal installs migrate verified legacy numeric-`1` lockfiles to `0.2` while frozen mode retains legacy behavior without rewriting; receipt schema `0.2` mirrors committed lockfile asset/file ownership (legacy receipts refined to primary-only sets); unsupported-version and per-file mismatch failures flow through as structured results + +## 10. Engine Consumer Bridge: Loaders, Lockfile Migration & Receipt 0.2 — Implementation + +- [ ] 10.1 Implement: loader/cache consumption of the tagged parse result for both archive versions; supplementary bytes stay opaque end to end +- [ ] 10.2 Implement: lockfile migration: normal install rewrites a verified legacy lockfile to `0.2` only after all current checks pass; frozen legacy stays legacy; frozen `0.2` archive with a legacy lockfile fails without rewriting +- [ ] 10.3 Implement: receipt schema `0.2` with per-asset owned-file sets, bootstrap-from-lockfile, legacy refinement to primary-only ownership, and untrusted-input validation (project identity, containment, record shape) before any deletion +- [ ] 10.4 Implement: tests: legacy migration, frozen non-rewriting, receipt bootstrap/refinement, escaping-receipt-path never deleted while valid records still process +- [ ] 10.5 Verify: engine tests, types, and lint pass for the consumer bridge + +## 11. Adapter SDK: Tagged Payloads & Atomic Skill Bundles — Research + +- [ ] 11.1 Explore: the adapter SDK contract (`packages/adapter/src/types.ts`, `define-adapter.ts`): `installAsset`/`readAsset`/`deleteAsset` signatures and how results are shaped today +- [ ] 11.2 Explore: the SDK filesystem helpers (`asset-fs.ts`): write/delete/dir-pruning machinery that must grow staging, commit/rollback, and owned-path removal +- [ ] 11.3 Explore: the claude-code adapter's path resolution and storage layout to plan its migration to skill-directory bundles +- [ ] 11.4 Propose: the tagged request/result unions keyed by asset type (skill carries primary text + canonical companion-path→bytes map, empty map legal; agent/command carry one text value and structurally no companions; no supplementary variant), the atomic stage/commit/rollback bundle lifecycle with owned-path removal and empty-directory pruning, companion-root containment checks, and the canonical-content `readAsset` contract + +## 12. Adapter SDK: Tagged Payloads & Atomic Skill Bundles — Implementation + +- [ ] 12.1 Implement: the tagged install/read/delete request and result unions in the SDK types (breaking change, next minor release) +- [ ] 12.2 Implement: the SDK filesystem helpers: companion containment within the resolved skill root, staged all-or-nothing bundle replacement (removing previously-owned companions absent from the new bundle), atomic ownership-based deletion, empty-directory pruning limited to owned removals +- [ ] 12.3 Implement: `readAsset` returning canonical logical primary content (adapter encoding stripped) plus the complete owned companion byte map +- [ ] 12.4 Implement: the claude-code adapter migration onto the new helpers, including skill-directory path resolution for companions +- [ ] 12.5 Implement: injected-failure integration tests at every write/delete/commit boundary proving no partial bundle survives install or delete failure, plus unowned-file preservation and escaping-companion-path rejection tests +- [ ] 12.6 Verify: adapter SDK and claude-code tests, types, and lint pass + +## 13. Engine Producer: Build 0.2, Materialization & Install Reconciliation — Research + +- [ ] 13.1 Explore: the build pipeline (`packages/engine/src/build/pipeline.ts`, `write-output.ts`): stage ordering, dist cleanup timing, and progress display hooks +- [ ] 13.2 Explore: materialization and the install orchestrator (`packages/engine/src/install/materialize.ts`, `run-install.ts`, `journal.ts`): per-asset install flow, skip-if-identical logic, journaling for rollback, and the lockfile/receipt commit sequence +- [ ] 13.3 Explore: facet removal and drift-check flows to map where per-file integrity comparison and receipt-driven deletion change +- [ ] 13.4 Propose: the producer wiring: pipeline consumes the archive plan and emits `0.2` unconditionally; engine passes companions only inside skill-variant payloads and withholds archive-only files entirely (materialization boundary in engine, not adapters); install performs the four-way pre-materialization agreement checks (facet integrity, asset identities, complete path sets, per-file hashes vs. recomputed + build-manifest values); receipt+lockfile+materialization commit as one transaction with rollback restoring all three; per-companion skip/journal; per-locked-file drift with canonical-content comparison for transformed primaries + +## 14. Engine Producer: Build 0.2, Materialization & Install Reconciliation — Implementation + +- [ ] 14.1 Implement: build-pipeline emission of `0.2` archives via the shared archive plan, with validation-before-cleanup ordering preserved end to end +- [ ] 14.2 Implement: build output display: emitted `facetVersion`, complete inner-archive entry listing including supplementary files, archive-assembly progress stage, and integrity in the persistent summary +- [ ] 14.3 Implement: the four-way pre-materialization integrity reconciliation with structured per-path mismatch failures; frozen mode fails without rewriting; normal mode writes replacement lock entries only after all checks pass +- [ ] 14.4 Implement: lock-entry derivation from the verified archive plan's materialized subset with recomputed (never blindly copied) per-file hashes, sorted deterministically +- [ ] 14.5 Implement: materialization through skill-variant bundle payloads with per-companion skip-if-identical and journal-backed rollback; archive-only files never reach `materialize` +- [ ] 14.6 Implement: the single-transaction commit of receipt, lockfile, and materialized state, and receipt-driven removal (including pulled-lockfile cleanup) that deletes only validated owned paths +- [ ] 14.7 Implement: per-locked-file drift detection: verbatim hashing for companions, canonical-content comparison via `readAsset` for transformed primaries, reports naming the exact locked path, and single-file repair on reinstall +- [ ] 14.8 Implement: engine end-to-end tests: full build→verify→install→drift→remove cycle for a facet with companions and archive-only files, legacy `0.1` archive install compatibility, and abort-before-write on every mismatch class +- [ ] 14.9 Verify: engine tests, types, and lint pass for the producer changes + +## 15. CLI: README Authoring, Edit Reconciliation & Error Rendering — Research + +- [ ] 15.1 Explore: the `facet create` wizard (`packages/cli/src/tui/`, `packages/engine/src/scaffold/`): step/card structure, confirmation preview, and atomic apply +- [ ] 15.2 Explore: the `facet edit` workbench (`packages/engine/src/edit/`: reconcile, scanner, manifest-writer, operations): discovery phases, queued-operation model, and Apply transaction +- [ ] 15.3 Explore: CLI error rendering (`packages/cli/src/util/errors.ts` and install/build command surfaces) to place unsupported-version guidance and new validation failure rendering +- [ ] 15.4 Propose: the CLI changes: create wizard README step (default-on, seeded from name/description, editable, disableable, listed in confirmation, atomic write + declaration, never regenerated after identity edits); edit README panel for exact `README.md` and `README` with state-dependent actions (Edit/Remove, Adopt/Edit-and-Adopt, Scaffold/Remove-Declaration, Create defaulting to `README.md`), both paths independent, excluded from generic reconciliation; generic scanner additions (undeclared skill-directory companions, common root files like `LICENSE`, scaffold-or-remove for vanished declarations); and the single CLI compatibility table mapping known archive formats to minimum supporting releases with latest-release advice for unknown formats + +## 16. CLI: README Authoring, Edit Reconciliation & Error Rendering — Implementation + +- [ ] 16.1 Implement: the create-wizard README step with seeded editable content, default-on/optional behavior, confirmation listing, and atomic `README.md` write plus top-level `files` declaration +- [ ] 16.2 Implement: the edit README panel with all four state-dependent action sets for both exact conventional paths, transactional through the existing Apply confirmation +- [ ] 16.3 Implement: generic edit reconciliation: undeclared skill-companion adoption, common root-file adoption, scaffold-or-remove for missing declared supplementary files, README excluded from the generic phase +- [ ] 16.4 Implement: CLI rendering of new structured failures (path grammar, collisions, namespace, per-file integrity, unsupported version) including the compatibility-table upgrade guidance +- [ ] 16.5 Implement: CLI e2e tests: create-with-README, README panel flows, companion adoption, build/install error rendering for the new failure classes +- [ ] 16.6 Verify: CLI tests, types, and lint pass + +## 17. Documentation & Release Notes + +- [ ] 17.1 Implement: updates to `docs/specification/archive.mdx`, `build.mdx`, `manifest.mdx`, and `integrity.mdx`: membership rules and single `files` hash map, plan derivation and validation-before-cleanup, both `files` declaration fields with the minimum-producer-version warning and linked Agent Skills naming convention, all-entry and per-locked-file integrity coverage +- [ ] 17.2 Implement: updates to `docs/specification/lockfile.mdx`, `commit.mdx`, and `install.mdx`: lockfile `0.2` with per-materialized-file integrity and legacy-alpha-1 migration/stable-v1 regeneration boundary, receipt ownership and transactional reconciliation, materialization boundary and atomic skill bundles with mismatch diagnostics +- [ ] 17.3 Implement: updates to `docs/guides/create-your-first-facet.mdx`, `docs/guides/install-facets.mdx`, and root `README.md`: replace asset-only phrasing, document the README workflow and supplementary files +- [ ] 17.4 Implement: the protocol release-policy update encoding the pre-1.0 minor-release rule for breaking changes, and draft release notes describing previously-conforming behavior that is no longer accepted (multi-segment asset names, skill/command name sharing, `0.1` producer output) +- [ ] 17.5 Verify: docs build/lint and cross-check every doc claim against the implemented behavior + +## 18. Final Verification + +- [ ] 18.1 Verify: the full repo suite (`bun check`) passes across all packages, including the D7 failure-class matrix, both-version fixtures, and injected-failure adapter tests +- [ ] 18.2 Verify: implementation coverage against the change's delta specs scenario-by-scenario and confirm consumer-first sequencing constraints (cafe gate, bridge-before-producer) are documented in the release plan diff --git a/openspec/changes/support-non-asset-files/adversarial/reviews/design-review.md b/openspec/changes/support-non-asset-files/adversarial/reviews/design-review.md new file mode 100644 index 00000000..bf2b0fd4 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/reviews/design-review.md @@ -0,0 +1,94 @@ +# Design comparison: Main vs. Adversary + +This review compares **Main** at `design.md` with **Adversary** at `adversarial/artifacts/design.md`. + +## Grading bar + +The designs were graded for value delivered against the reconciled proposal, RFC 2119 precision, atomic and testable requirements, correct OpenSpec/design mechanics, explicit compatibility and rollout boundaries, security completeness at archive/path trust boundaries, and coverage sufficient to derive specs and tasks without inventing policy later. + +## Coverage summary + +Both versions correctly preserve explicit archive membership, reject globs, keep supplementary files distinct from independently installable assets, treat supplementary content as opaque bytes, materialize companions only for skills, leave the lockfile asset model unchanged, conditionally preserve legacy `0.1` output for asset-only facets, and require the registry to understand the new archive format before accepting it. + +Main is stronger on author-facing declaration ergonomics, choosing the format revision `0.2` consistently with the current `0.1` convention, concrete scaffold/edit surfaces, and explicitly keeping archive-only files out of adapter inputs. Adversary is substantially stronger at the security and lifecycle boundaries: one normalized archive plan, exhaustive collision and tar-entry validation, strict version dispatch, tagged adapter and receipt contracts, explicit atomic replacement/rollback semantics, consumer-first rollout, old-builder behavior, and protocol-package semantic versioning. + +The central difference is that Main often expresses invariants through optional parallel fields and prose (“only ever populated for `type === 'skill'`”), while Adversary encodes variants as tagged data and requires shared derivation. That difference is material because adapter operations, receipt deletion, and archive membership are security and data-loss boundaries. + +## Decision-by-decision divergences and merge recommendations + +### D1–D2: Declaration shape and exact enumeration + +**Main is stronger on ownership ergonomics.** `SkillDescriptor.files` makes companion ownership visible where the skill is declared, while a disjoint top-level `files` list cleanly expresses archive-only metadata. Adversary's single root-relative list is simpler and makes archive membership literal in one place, but skill ownership must then be inferred from path containment. + +Main overstates that invalid ownership is “structurally impossible”: both lists still contain unrestricted strings, and disjointness, path safety, declared-skill membership, and collision freedom remain runtime schema constraints. Its two declaration sites also require a single downstream normalization step or they will encourage duplicated build/verifier logic. + +**Merge recommendation:** keep Main's two authoring sites, but require one pure protocol operation to validate both and normalize them into the tagged archive plan described by Adversary (`manifest`, `primary-asset`, `skill-companion`, `archive-only`). Build, hashing, verification, parsed results, and installation MUST consume that plan. Preserve exact enumeration/no globs and Main's embedded-manifest rationale. + +### D3: Build-manifest shape and version boundaries + +**Main is stronger on the archive revision identifier:** `0.2` follows the documented numeric progression from `0.1`; Adversary's `1` is an unnecessary naming jump. Both correctly retain byte-identical `0.1` output when no supplementary files are present. + +**Adversary is stronger on hash-map authority.** Main's parallel `assets` and `files` maps duplicate classification already derivable from the embedded manifest and permit overlap/disagreement states that must be detected later. The reconciled proposal requires an unambiguous distinction, but it does not require that distinction to be duplicated in the build manifest. One all-entry path-to-hash table plus the embedded manifest and normalized archive plan is unambiguous and has one completeness rule. + +Main also calls the protocol work “additive” in Migration Plan step 1 and names only the adapter SDK major release. That conflicts with the reconciled proposal's explicit protocol/archive breaking boundary and `openspec/specs/protocol/spec.md`, which requires backward-incompatible protocol requirements to ship in a new major version. + +**Merge recommendation:** use conditional `facetVersion: 0.2`, retain the exact `0.1` schema for legacy output, and use a single `files` hash map containing every inner entry in `0.2`; derive classification only from embedded `facet.json`. Require strict one-time dispatch by version with no malformed-`0.2` fallback to `0.1`. State separately that the published protocol package ships this in a new major release, as does the adapter SDK. + +### D4 and D6: Shared derivation, path safety, and archive verification + +**Adversary is decisively stronger and exposes a blocking gap in Main.** Main covers traversal, absolute paths, backslashes, primary-path collisions, duplicates, missing declarations, and exact observed membership. It does not settle several ways those checks can be bypassed or become platform-dependent: empty/`.` segments, NUL and drive-prefixed paths, canonical Unicode/case aliases, resolved source identity, file/directory prefix collisions, symlinks/hard links, non-regular tar entries, duplicate tar headers, and parsers that first collapse entries into a lossy path map. + +Adversary also catches two operational details Main omits: source inputs must be validated before any `dist/` cleanup can destroy a declared input, and every expected parser/build failure must remain a structured result rather than an exception. + +**Merge recommendation:** add Adversary's shared archive-plan decision and exhaustive path/tar checks to Main. Specify that verification validates raw headers before constructing a map, rejects duplicate and non-regular entries, compares expected and observed canonical path sets exactly, and verifies exactly one hash per expected path. Specify regular-file containment and source-identity checks at build time, including symlink/hard-link policy and portable alias/prefix collisions. Add a test matrix for every named failure class. This is blocking before specs/tasks because it defines the supply-chain boundary. + +### D5: Opaque bytes and parsed representation + +**Main is stronger on a concrete implementation touchpoint** by widening `ArchiveEntry.content` and explicitly preserving binary/empty content. Adversary agrees on exact bytes but goes further by requiring the successful parsed result to keep primary assets, skill companions grouped by owner, and archive-only supplementary files as distinct tagged data. + +`string | Uint8Array` alone does not encode which content is prompt text and which is opaque, so downstream code can still apply the wrong transformation. + +**Merge recommendation:** keep Main's opaque-byte requirements, but make the normalized and parsed public results tagged by entry kind as Adversary requires. Text decoding/front-matter logic applies only after narrowing to a primary asset; supplementary data remains bytes. + +### D7: Adapter contract and atomic skill lifecycle + +**Adversary is decisively stronger and exposes a blocking type/atomicity defect in Main.** Main proposes optional `companions`/`companionPaths` parameters beside `assetType` and relies on the prose invariant that they are populated only for skills. That represents illegal combinations (agent with companions, skill call accidentally omitting its bundle) and gives implementations no exhaustive branch. The widened methods also do not, by themselves, guarantee that custom adapters stage a complete replacement or roll back partial writes/deletes. + +Adversary uses tagged variants keyed by asset type, makes a skill bundle one operation, requires removal of formerly owned but now absent companions, and explicitly requires stage/commit/rollback plus structured expected failures. + +**Merge recommendation:** replace Main's optional-parameter trio with tagged request/result unions. A skill variant MUST carry primary text and a canonical companion-byte map (empty is legal); agent/command variants MUST NOT carry companions. Define one atomic replacement/delete contract for the entire skill bundle, including rollback and stale-owned-file removal, and return structured failure values. Centralize containment, staging, commit/rollback, owned-path deletion, and empty-directory pruning in SDK helpers, while requiring equivalent behavior from custom-I/O adapters. Add injected-failure tests at every write/delete/commit boundary. + +### D8: Receipt ownership and drift removal + +**Adversary is stronger.** Main again uses an optional companion list whose legal presence depends on `type`, and its migration explanation is internally inconsistent: a truly legacy receipt cannot refer to companions because legacy archives could not install them, so there is no justified “one-install-cycle” unknown companion orphan. Conversely, once a supporting version installs companions, forgetting their ownership is unacceptable because offline removal can no longer be exact. + +Adversary requires a tagged receipt record and a complete owned set for skills, keeps archive-only files out of receipts, validates receipt paths as untrusted input, and couples receipt rollback to adapter rollback. + +**Merge recommendation:** parse persisted legacy records at the receipt boundary and refine them into an internal tagged union: agent/command records have no companion field; skill records require a complete canonical owned-path set (with legacy skill tuples migrated to the known primary plus an empty companion set). Persist the refined shape after a successful install. Store canonical paths relative to the adapter-owned skill root, validate containment and project identity before deletion, never delete unowned paths, and journal receipt plus materialization as one rollback unit. Remove the unsupported orphan-cycle claim. + +### D9–D10: Authoring and materialization boundary + +**Main is stronger on authoring coverage** by addressing `facet edit`, and its D10 data-flow boundary is excellent: archive-only files cannot be materialized accidentally because adapters never receive them. Adversary should have named both explicitly. + +However, Main's recommendation that `facet create` scaffold and declare a README by default makes every newly scaffolded facet opt into `0.2`, old-client rejection, and the old-builder-ignore hazard. That conflicts with consumer-first rollout and weakens the practical value of conditional legacy output. + +**Merge recommendation:** retain D10 and edit-flow discovery/add/remove support, but do not make supplementary README declaration an unconditional scaffold default. Make it an explicit author choice or gate the default until the producer minimum version and registry/consumer rollout are in place. If removal of vanished declarations is in scope, decide it now with deterministic behavior; do not leave it as “if cheap” task policy. + +### Rollout, documentation, risks, and open questions + +**Adversary is stronger on rollout.** It explicitly sequences verifier consumers and cafe before producers, requires immutable cross-version fixtures, warns that tolerant old builders may accept `files` but silently omit the bytes, and states that verifier support cannot be rolled back after `0.2` artifacts are published. Main only partially captures registry sequencing and does not address old builders ignoring the new manifest fields. + +**Main is stronger on documentation breadth** by adding install documentation and concrete authoring guidance. Its open question about registry README presentation is already a declared non-goal, and archive policy limits can be explicitly deferred to consumer configuration rather than left unresolved. The `facet edit` removal behavior needs a decision if it is to generate tasks. + +**Merge recommendation:** adopt Adversary's consumer-first rollout and rollback constraints, including minimum-producer documentation and cross-version accept/reject fixtures; retain Main's added install documentation. Close the README-presentation question as out of scope, state that size/count policy remains consumer configuration for this change, and settle edit removal behavior before task generation. + +## Blocking cross-cutting items + +1. **Security boundary:** define the shared normalized archive plan and exhaustive raw-tar/path/collision/regular-file checks before specs or implementation tasks are considered complete. +2. **Atomic lifecycle:** replace optional adapter/receipt companion fields with tagged variants and specify observable all-or-nothing install/update/delete plus rollback and offline removal behavior. +3. **Compatibility boundary:** state both archive revision (`0.2` conditionally) and package major-version requirements; require strict version dispatch, consumer-first cafe rollout, old-builder warnings, and immutable compatibility fixtures. +4. **Single source of truth:** avoid parallel build-manifest classification maps; membership/classification comes from embedded `facet.json` through the shared archive plan, while the build manifest carries hashes. + +## Overall merge recommendation + +Use Main as the structural base because its two declaration sites, `0.2` naming, authoring flow, and materialization boundary are concrete and useful. Replace its parallel hash-map, optional adapter parameters, optional receipt ownership, partial path grammar, and under-specified rollout with Adversary's single normalized plan, one all-entry hash map, tagged unions, atomic rollback contract, exhaustive security checks, and consumer-first compatibility plan. The four blocking items above should be resolved in the design before delta specs or tasks lock in weaker contracts. diff --git a/openspec/changes/support-non-asset-files/adversarial/reviews/proposal-review.md b/openspec/changes/support-non-asset-files/adversarial/reviews/proposal-review.md new file mode 100644 index 00000000..ea896460 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/reviews/proposal-review.md @@ -0,0 +1,72 @@ +# Proposal comparison: `support-non-asset-files` + +This review compares **Main** at `proposal.md` with **Adversary** at `adversarial/artifacts/proposal.md`. + +## Grading bar + +I graded the proposals for customer/developer value, correct RFC 2119 usage, atomic and testable scope, valid OpenSpec proposal mechanics, complete product-domain coverage, documentation alignment, and explicit treatment of compatibility and security boundaries. + +## Coverage comparison + +Both versions cover the same core value: authors can ship declared non-asset files; all shipped bytes remain integrity-protected; skill companion files install with the skill; files elsewhere remain archive-only; and existing product domains rather than a new capability own the behavior. Both identify the six affected domains: `authoring__facets`, `protocol__schemas`, `protocol__content-hashing`, `protocol__integrity`, `installation`, and `adapter__assets`. + +Main is stronger on current-system diagnosis, adapter lifecycle impact, concrete code impact, the registry as a second implementation, and the existing semantic-versioning authority. Adversary is stronger on explicit archive-membership validation, preserving the asset/non-asset distinction, compatibility severity, non-goals, security-test scope, and documentation breadth. + +The material divergences are: whether skill companion files are explicitly declared or implicitly discovered; whether non-asset hashes belong in a field named `assets`; whether this is merely a forward-compatibility note or an explicit protocol compatibility boundary; how multi-file skill ownership/deletion is represented; and whether required non-goals and affected installation/authoring documentation are in scope. + +## Section-by-section findings and merge recommendations + +### Why + +**Main is stronger.** It explains the end-to-end failure mode precisely: build drops unsupported files and verification rejects extra archive entries under outer exclusivity. It also distinguishes human-facing root files from Agent Skills companion resources, making the developer value concrete. + +**Merge recommendation:** keep Main's `Why`, while retaining Adversary's concise statement that supporting files are not independently installable assets. That distinction should frame the whole change. + +### What Changes — declaration and archive membership + +**Adversary is stronger.** Main says the manifest declares non-asset files, but separately says every file under `skills//` SHALL ship. That leaves a material ambiguity: are skill descendants individually declared, declared by a directory/glob, or automatically discovered? Automatic discovery conflicts with Adversary's stronger and safer rule that archive membership remains explicit and reviewable. Adversary also names missing, undeclared, duplicate/colliding, and unsafe paths as validation failures; Main names undeclared files and later mentions missing files, but does not establish the complete validation boundary. + +**Merge recommendation:** state one source of truth for membership: every supplementary archive entry, including every skill companion file, MUST be derivable from an explicit manifest declaration. Defer the declaration syntax to design, but require missing files, undeclared entries, path traversal/unsafe paths, and collisions or duplicate resolved paths to fail validation. Do not imply recursive auto-discovery unless that is an intentional product decision. + +### What Changes — integrity and schema semantics + +**Main is stronger on the observable guarantee** because it explicitly requires per-entry hashes plus inclusion in the bytes covered by content integrity. **Adversary is stronger on type semantics** because it requires schemas to represent the complete tracked file set without classifying supplementary files as assets. Main's statement that the build manifest's `assets` map will cover non-asset entries contradicts the proposal's own asset/non-asset distinction and prematurely fixes a design choice. + +**Merge recommendation:** keep Main's two-layer integrity guarantee and expanded outer-exclusivity derivation set. Replace the `assets`-map commitment with a requirement that the build manifest represent and hash every tracked entry while preserving an unambiguous distinction between installable assets and supplementary files; settle the exact schema shape in design. + +### What Changes — installation and adapter lifecycle + +**Main is stronger.** It identifies install, read, and delete behavior, third-party adapter breakage, and drift removal for multi-file skills. Adversary adds an important boundary: supplementary files do not gain independent install scope, adapter metadata, or lockfile asset tuples. + +**Merge recommendation:** combine these. Require skill companion files to be installed and removed atomically with their owning skill through the adapter contract, with receipt/ownership data sufficient for drift removal. Explicitly prohibit supplementary files from becoming independently addressable assets. Leave the exact adapter payload and receipt schema to design. + +### What Changes — compatibility + +**Adversary is stronger.** Main accurately notes that old consumers reject the new archives and cites `openspec/specs/protocol/spec.md`, but labels this only a forward-compatibility note. That is insufficient when the existing protocol says backward-incompatible changes require a new major version. Adversary correctly marks the archive-entry expansion as breaking and requires an explicit compatibility boundary while preserving legacy asset-only validity. + +**Merge recommendation:** mark the protocol/archive change as **BREAKING**, require the design to choose an explicit protocol or archive-version boundary, and require compatibility tests proving that legacy asset-only artifacts remain valid. Keep Main's explicit note that other implementations, including the cafe registry, must adopt the new verification contract before accepting new-format archives. + +### Capabilities + +Both versions select valid existing product domains and avoid inventing a feature-domain. Main is stronger on installation receipt behavior; Adversary is stronger on schema semantics and path-safety coverage. + +**Merge recommendation:** retain all six domains, but revise `protocol__schemas` so it does not assert that non-assets belong in an `assets` map. Carry explicit membership/path validation under `authoring__facets`, complete-entry hashing under `protocol__content-hashing`, complete-entry reconciliation under `protocol__integrity`, atomic multi-file skill lifecycle under `installation`, and the corresponding consumer contract under `adapter__assets`. + +### Non-goals + +**Adversary is decisively stronger. Main has no `## Non-goals` section, violating the mandatory proposal rule in `openspec/config.yaml`.** Adversary correctly excludes README display/`facet info`, arbitrary independent installation destinations, command/agent directory-install semantics, filesystem metadata such as symlinks and executable bits, and automatic packaging of untracked files. + +**Merge recommendation:** add a `## Non-goals` section containing those five boundaries. This is required before the proposal is conforming. + +### Impact and documentation + +**Main is stronger on code and ecosystem impact:** it names affected modules, third-party adapters, error rendering, and the cafe registry. **Adversary is stronger on verification and documentation completeness:** it calls for traversal, collision, undeclared-entry, and tamper tests and cites the authoring/install guides plus root `README.md`, not only protocol reference pages. Since authoring and installation behavior change, those guides are materially affected. + +**Merge recommendation:** retain Main's code and registry impact, add Adversary's security/compatibility test matrix, and explicitly scope review or updates for `docs/guides/create-your-first-facet.mdx`, `docs/guides/install-facets.mdx`, and root `README.md` alongside the four protocol pages. Keep Main's conditional review of `docs/specification/lockfile.mdx`, but make it unconditional if receipt or lockfile semantics change. + +## Blocking cross-cutting items + +1. **Proposal mechanics:** Main MUST gain the required `Non-goals` section before it can be accepted. +2. **Membership source of truth:** the change MUST settle whether skill companion files are explicitly declared or implicitly discovered; later specs cannot be atomic or testable while both readings remain possible. +3. **Protocol compatibility:** the artifact MUST acknowledge the change as backward-incompatible and establish that design will choose an explicit major/version boundary rather than relying on a forward-compatibility note. +4. **Asset identity and ownership:** supplementary-file hash records and multi-file skill receipts MUST preserve the distinction between assets and non-assets and define ownership sufficient for safe deletion without creating independent supplementary-file asset tuples. diff --git a/openspec/changes/support-non-asset-files/adversarial/reviews/specs-review.md b/openspec/changes/support-non-asset-files/adversarial/reviews/specs-review.md new file mode 100644 index 00000000..ce1c8d67 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/reviews/specs-review.md @@ -0,0 +1,93 @@ +# Comparison Review: `specs` — support-non-asset-files + +**Main**: `openspec/changes/support-non-asset-files/specs/**/*.md` (7 capability delta specs) +**Adversary**: `openspec/changes/support-non-asset-files/adversarial/artifacts/specs/**/*.md` (7 capability delta specs) + +Both versions were derived from the same reconciled proposal and design. Both landed on the **identical capability set**: `protocol`, `protocol__schemas`, `protocol__content-hashing`, `protocol__integrity`, `authoring__facets`, `installation`, `adapter__assets`. No capability-selection divergence — the interesting divergences are in delta mechanics, requirement placement, and coverage depth. + +## Grading bar + +- **Value-centric** (spec-governance): observable behavior, no internal module/class names, no domain-name subjects. +- **RFC 2119**: normative keywords throughout. +- **Atomic + testable**: one concern per requirement; scenarios concrete enough to be test cases. +- **Delta mechanics**: MODIFIED headers match existing requirement headers exactly; MODIFIED blocks carry full replacement content (nothing silently lost at archive time); ADDED used where behavior is new. +- **Coverage**: every proposal/design commitment (D1–D12) represented in the right capability. + +Both versions pass value-centric, RFC 2119, and scenario-format checks. Both use matching MODIFIED headers throughout. The material differences are below. + +## The one systemic difference: rewrite vs. copy-and-edit + +**Main condensed-rewrites its MODIFIED requirements**; **Adversary copy-and-edits the original text verbatim**. Main's rewrites are tighter and more readable, but MODIFIED blocks *replace* the original at archive time, so every dropped clause is a silent spec regression. I found concrete drops in Main (details per capability below): + +1. **Receipt requirement** (installation): Main's rewrite drops (a) the original's frozen-mode ordering clause — "in frozen-lockfile mode, this cleanup applies only after the frozen consistency check passes; an orphaned entry the check rejects fails before any cleanup" — and (b) the per-project isolation/concurrency clause ("two distinct projects never share a receipt and concurrent operations in different projects never contend on one"). +2. **Edit detects new files** (authoring): Main drops "all items SHALL be resolved before proceeding to editing" (the all-at-once reconciliation-gate behavior). + +These are the only clause-level losses I could find, but the reconciler should diff each Main MODIFIED block against the live spec before syncing — that is the systematic hazard of the rewrite style. Adversary's copy-and-edit style has no such losses but produces bulkier deltas and, in two places, left a stale original requirement *unmodified* when it actually conflicts with the change (see authoring and content-hashing below) — the mirror-image failure, and the worse one. + +## Per-capability comparison + +### protocol — Main stronger + +Both modify the semver-discipline requirement with the pre-1.0 minor / post-1.0 major rule. Main additionally: constrains **patch** releases ("patch releases SHALL NOT remove, tighten, or incompatibly change requirements from their minor release") and adds a "removing legacy artifact support is breaking" scenario that directly anchors the future `0.1` deprecation. Adversary's version keeps the original scenario headers but adds nothing Main lacks. +**Merge**: take Main as-is. + +### protocol__schemas — Main slightly stronger; take two Adversary details + +Same three requirements modified by both (facet manifest, build manifest, lockfile). Substantively convergent on: single-segment Agent Skills asset-name grammar with ASCII interpretation, shared skill/command namespace, agents separate, two exact-path declaration sites with disjoint regions, `0.2` build manifest with all-entry `files` map and no `assets`, exact-equality version dispatch with no cross-version fallback, structured unsupported-version failures, lockfile `0.2` per-asset `files` records with archive-only exclusion. + +Divergences: +- Main pushes the **full D7 path/collision grammar into manifest validation** (unsafe paths, Unicode/case-fold aliases, prefix conflicts rejected by any manifest consumer). Adversary keeps schema constraints lighter (site rules + exact paths) and carries the grammar in authoring build validation + verification. Main's placement is stronger: the embedded manifest is the trust root, so *every* consumer validating it should reject unsafe declarations — this matches D3's "the plan operation validates both declaration sites." +- Main states archive-format and lockfile-format version constants are **interpreted independently even when numerically equal** (D10); Adversary omitted this. Take Main. +- Adversary's lockfile modification includes an "asset entry missing its `files` array is rejected" scenario at the schema level (Main has it only in installation) — harmless duplication; optional. +- Adversary's manifest text requires naming documentation to link the Agent Skills convention (D9's doc mandate); Main links the convention inline in the requirement itself, which is arguably sufficient. Optional. + +**Merge**: Main as base. Nothing blocking from Adversary; optionally add the schema-level missing-`files` rejection scenario. + +### protocol__content-hashing — Main stronger; one placement note + +Main modified all six existing requirements; Adversary modified three and ADDED a verbatim-bytes requirement. Main caught a requirement Adversary **missed**: "Build output contains the self-contained archive" still mandates "remove previous `dist/` contents before writing" with no validation-ordering qualifier — Main's modification ("previous output removed only after all source input validation succeeds", plus the invalid-rebuild-preserves-output scenario) is necessary; Adversary only fixed this ordering in `authoring__facets`, leaving the protocol-side requirement in conflict. Main also updated the display requirement (emitted `facetVersion` + full entry listing) where that requirement actually lives. + +Adversary's ADDED "Supplementary file content is archived verbatim" lives on the protocol side; Main's equivalent ("Build ships supplementary files as opaque bytes") lives in `authoring__facets`. Coverage is equivalent; Main's modified "Content hashes…" requirement already states supplementary hashes use exact bytes with no transformation, so the interop property is on the protocol surface either way. +**Merge**: take Main as-is. + +### protocol__integrity — near parity; take two Adversary security scenarios + +Both rewrote the archive-verification operation to full depth: exact-once version dispatch, no fallback, raw-entry validation before lossy collapse, manifest-derived membership, exact set equality, per-entry hash verification, tagged success result (primary / companion-with-owner / archive-only), structured failures, decompressor contract preserved. Main adds a sharp "empty primary rejected / empty supplementary allowed" scenario and folds unsupported-version shape into the structured-failures requirement. Good. + +Adversary has two scenarios Main lacks, both genuinely adversarial: +1. **A build-manifest-only entry does not expand the expected set** — a crafted archive whose build manifest records an extra path *and* whose inner tar contains that file must still be rejected; the hash map must never legitimize membership. Main's prose implies this (triple set equality), but the explicit attack scenario is the test a security reviewer wants. +2. **Alias-duplicate tar entries** — two inner paths that collide only by Unicode normalization or case folding, rejected at raw-entry validation. Main rejects duplicates "with the same path" and handles aliases at manifest validation, but aliased *tar* paths are a distinct smuggling vector on case-insensitive filesystems. + +Adversary's ADDED "Legacy archives remain verifiable during the compatibility window" is mostly redundant with Main's in-requirement legacy handling; its "legacy verifier fails closed on current archives" scenario documents ecosystem posture but isn't testable against the current system — skip it. +**Merge**: Main as base; add the two scenarios above to Main's verification requirement (or as an ADDED membership requirement). + +### authoring__facets — Main clearly stronger; restore one clause, keep Adversary's fidelity check + +Main's coverage is broader: it modified **"Content files contain no front matter"** (supplementary files exempted — without this, any supplementary file containing front-matter-like bytes *violates the live spec*; Adversary missed this entirely, its worst gap), modified **"Edit parses front matter"** (supplementary/README bytes never parsed or stripped), modified **"Edit is transactional"** to enumerate README/companion deltas, and its ADDED declaration requirement explicitly **rejects glob patterns** (D2) where Adversary deliberately under-specified ("docs/**" would just be a missing file — Main's clear rejection is the better author experience). Main also permits inner paths named `build-manifest.json`/`archive.tar.gz` (D7 allowance Adversary dropped) and specs missing-`LICENSE` scaffold-or-remove. + +Adversary's advantages are mechanical, not substantive: its scaffold-wizard and build MODIFIED blocks are verbatim-copy-and-edit, so no original clause is lost; Main's condensed rewrites drop "all items SHALL be resolved before proceeding to editing" from edit reconciliation and compress several scenario texts. Adversary's per-skill-declaration-on-undeclared-skill check is structurally vacuous under D1's shape (companion lists live *inside* skill descriptors) — ignore it. +**Merge**: Main as base. Restore the "all items resolved before proceeding" clause to Main's edit-reconciliation requirement; spot-diff Main's other condensed rewrites against the live spec for further quiet drops. + +### installation — Main stronger; take Adversary's upgrade-guidance specificity, restore two receipt clauses + +Main integrates per-file verification into the existing "Integrity is verified before any asset is written" requirement (cleaner than Adversary's parallel ADDED requirement, which leaves the original untouched and overlapping), modifies "Removing a facet uninstalls it" (Adversary left it stale at asset granularity), adds the per-locked-file drift requirement with canonical-read comparison and "reinstall repairs one drifted file", and its lockfile-version modification carries two D10 details Adversary dropped: **frozen mode requires a `0.2` lockfile for a `0.2` archive**, and the **stable-v1 numeric-`1` reclamation path** (delete-and-regenerate guidance, no shape sniffing). + +Adversary wins on one point: its unsupported-version requirement specs the concrete guidance behavior — for a **known** newer format, name the minimum release that supports it; for an **unknown** format, advise updating to latest without inventing a minimum (D4). Main's "SHALL direct the user to upgrade when a newer consumer may support the format" is materially vaguer. + +Main's receipt rewrite drops two normative clauses from the original (frozen-cleanup ordering; per-project receipt isolation/concurrency) — see the systemic section. +**Merge**: Main as base; replace Main's unsupported-version guidance sentence with Adversary's known/unknown split; restore the two dropped receipt clauses. + +### adapter__assets — Main slightly stronger + +Convergent on the tagged payload contract (skill = primary + companion byte map, empty map valid; agent/command = single content, structurally companion-free; no supplementary variant), atomic bundle replacement with removal of dropped companions, verbatim companion bytes, canonical logical read content, containment rejection, unowned-file preservation. Main additionally: folds "archive-only files are withheld from adapters" into the modified "Asset methods are the only interface" requirement (better home than Adversary's standalone ADDED requirement), specs **directory pruning limited to directories emptied by owned-file removal**, and adds a **failed-deletion-restores-prior-bundle** scenario (delete-side atomicity; Adversary only specced install-side rollback). +**Merge**: take Main as-is. + +## Blocking items before archive + +1. **Restore dropped clauses in Main's condensed MODIFIED blocks** — confirmed drops: receipt frozen-cleanup ordering, receipt per-project isolation/concurrency (installation), "all items resolved before proceeding to editing" (authoring). The reconciler should also diff every other Main MODIFIED block against the live spec; the rewrite style makes silent loss easy. +2. **Add the two Adversary security scenarios to `protocol__integrity`**: build-manifest-only entries cannot legitimize membership; alias-duplicate (Unicode/case-fold) tar entries rejected at raw-entry validation. +3. **Adopt Adversary's known/unknown upgrade-guidance split** in installation's unsupported-version requirement. + +## Overall merge recommendation + +**Keep Main as the base across all seven capabilities.** Main caught two conflicts Adversary missed outright (front-matter prohibition vs. supplementary bytes in `authoring__facets`; `dist/` cleanup ordering in `protocol__content-hashing`), integrates changes into the requirements where they live rather than adding parallel overlapping ones, and carries several design details Adversary dropped (patch-release constraint, version-constant independence, frozen `0.2`-lockfile gate, stable-v1 reclamation, glob rejection, outer-filename allowance, delete-side rollback). Fold in the three blocking items above — they are small, targeted edits, all strengthening Main's weakest spots: silent clause loss from condensed rewrites and two missing adversarial test scenarios at the trust boundary. diff --git a/openspec/changes/support-non-asset-files/adversarial/reviews/tasks-review.md b/openspec/changes/support-non-asset-files/adversarial/reviews/tasks-review.md new file mode 100644 index 00000000..0ee19b02 --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/reviews/tasks-review.md @@ -0,0 +1,74 @@ +# Comparison Review: `tasks` + +**Main:** `openspec/changes/support-non-asset-files/tasks.md` (7 blocks, 14 numbered groups, 62 tasks) +**Adversary:** `openspec/changes/support-non-asset-files/adversarial/artifacts/tasks.md` (9 blocks + final verification, 18 numbered groups, 71 tasks) + +Both were derived from the same reconciled proposal, design, and delta specs; the adversarial version was authored blind to Main. + +## Grading bar + +Scoped to a tasks artifact: + +1. **Artifact mechanics** — required preamble, checkbox format the apply phase can parse, VIPER type prefixes, Research/Implementation block pattern, Explore→Propose→Implement→Verify ordering rules, Step Types legend. +2. **Dependency ordering** — consumer-first sequencing per the design's Migration Plan; no task depends on work scheduled later. +3. **Atomicity and verifiability** — each task completable in one session, with a clear done condition; Verify gates after every Implement batch. +4. **Coverage** — every design decision (D1–D12), every delta-spec requirement, the Migration Plan steps, and the Article III documentation obligations map to at least one task. +5. **Parallelizable research** — Explore steps scoped to independent topics. + +## Coverage comparison + +Both versions cover the same core arc in the same dependency-safe order: protocol schemas/naming → archive plan and path grammar → versioned build manifest and verification → lockfile `0.2` → engine consumer bridge → adapter SDK bundles → producer/build pipeline → materialization/install reconciliation → create/edit README authoring → docs. Both satisfy the preamble rule, checkbox format, typed prefixes, the Research/Implementation split, and the Explore→Propose gate before every Implementation group. Neither has a dependency-order defect. + +The material divergences: + +| Area | Main | Adversary | +|---|---|---| +| Step Types legend | Present (verbatim) | **Missing** | +| Block granularity | 7 coarse blocks | 9 finer blocks (protocol split into 4) + dedicated final-verification block | +| First-party adapter migration | claude-code, **opencode, and codex** (6.3) | claude-code only (12.4) | +| Producer readiness gate | Explicit in-plan gate task (10.1) recording consumer + registry readiness before `0.2` emission | Only a documentation cross-check in final verify (18.2) | +| Registry client / cache audit | Explicitly updated (4.5) | Loaders/cache covered (10.1) but registry download not named | +| Docs scope | Audit-first Research block; adds publish, terminology, troubleshooting, skills, and **custom-adapter guide** pages beyond the design's list; schema-derived generated references; changeset metadata (13–14) | Flat block limited to the design's enumerated doc list (17) | +| Protocol release-policy update | Implicit (release notes 14.4, adapter release metadata 6.5) | **Explicit task** encoding the pre-1.0 minor-release rule in the permanent policy (17.4) | +| Full-cycle e2e test | Spread across blocks; no single full-cycle task | **Explicit** build→verify→install→drift→remove cycle incl. legacy `0.1` install (14.8) | +| Spec-coverage sweep | Strict OpenSpec validation in 14.5 | Scenario-by-scenario delta-spec coverage check (18.2) | +| D7 failure-class test matrix | Referenced generically ("full build failure-class matrix", 10.6) | Classes **enumerated inline** (4.5) | +| Edit/create implementation realism | Deep: headless create, focus management, state snapshotting, stable structured reconciliation identities replacing string-parsed keys, tagged operation variants (11–12) | Requirement-level only (15–16) | +| Explore scoping | Flow-scoped, some broad ("trace X, Y, Z, and W across protocol and engine") | File-anchored, more independently parallelizable | + +## Divergence judgments + +1. **Step Types legend — Main stronger, and this is a compliance defect in Adversary.** The VIPER planning rules require the legend verbatim at the top of every plan. Main includes it; Adversary relies on the preamble's skill reference alone. Main's form is correct. + +2. **First-party adapter migration — Main stronger, materially.** The design says "adapter SDK … + first-party adapters; claude-code migrates" and the repo carries a first-party adapter list beyond claude-code. Adversary followed the proposal's Impact section (which names only claude-code) and would leave opencode and codex uncompiled against a breaking SDK contract. Main's 6.3 is the correct scope. + +3. **Producer readiness gate — Main stronger.** The Migration Plan makes cafe a *hard gate* before producer enablement. Main turns that into a checkable in-plan task (10.1: "do not enable `0.2` producer output if either consumer class is not ready"), which is exactly how a sequencing constraint should surface in a checklist. Adversary only verifies that the constraint is *documented* (18.2) — weaker, because nothing in its plan blocks flipping the producer on. + +4. **Docs breadth — Main stronger.** Main's docs Research block audits `docs/` rather than trusting the design's enumeration, and it caught the custom-adapter contract page the design's Migration Plan omitted — precisely the Article III behavior ("existing documentation MUST always be considered"). Its schema-derived reference generation task also honors the project's single-source-of-truth rule. Adversary's flat block reproduces the design list faithfully but adds nothing; for a breaking change touching this many surfaces, a docs audit is warranted. + +5. **Edit/create implementation realism — Main stronger.** Main's 11.3/12.5 anticipate real workbench constraints (exhaustive UI switches representing two independent README paths, replacing string-parsed reconciliation keys with tagged structured identities). These translate the repo's illegal-states-unrepresentable rule into concrete tasks. Adversary restates the spec requirements without this depth. + +6. **Full-cycle e2e test — Adversary stronger.** Main's tests are thorough per-block but no single task proves the whole pipeline end to end (build a facet with companions + archive-only files, verify, install, drift, repair, remove) plus a legacy `0.1` install through the same path. That integration seam — where protocol, engine, adapter, and CLI meet — is where per-block tests miss regressions. Worth one explicit task. + +7. **Protocol release-policy update — Adversary stronger, minor.** D4 says the permanent protocol release policy "SHALL be updated by this change to encode the pre-1.0 rule." The `protocol` delta spec carries the requirement, and spec sync will land it, but neither Main task explicitly performs/verifies the policy-text update outside release notes. Adversary's 17.4 names it. + +8. **Spec-coverage sweep — Adversary slightly stronger.** Main runs strict OpenSpec validation (structural); Adversary's 18.2 additionally walks the delta specs scenario-by-scenario against the implementation. For a 7-capability change this is a cheap, high-value final gate. + +9. **D7 matrix enumeration — Adversary slightly stronger.** Enumerating the failure classes inline (traversal, NUL, Unicode/case aliases, prefix collisions, links, duplicates, reserved root `facet.json`, …) makes the task's done-condition self-contained; Main's "full failure-class matrix" requires the executor to reopen design D7. Low cost to inline. + +10. **Block granularity — wash.** Adversary's four protocol blocks give more frequent Verify gates; Main's two blocks honor the "closely related groups SHOULD be combined" rule. Adversary's lockfile-schema block (7/8) is thin (one Explore); Main's protocol explores are broad multi-topic traces that partially defeat parallel-subagent scoping. Neither is wrong; no change recommended on structure. + +## Merge recommendation + +**Retain Main as the base.** It is legend-compliant, broader on adapters and docs, encodes the producer hard gate as a task, and is more implementation-aware in the authoring block. Fold in from Adversary: + +- **Block 8 or 10 (tests):** add one explicit end-to-end cycle task — build a facet with skill companions and archive-only files, verify, install, detect + repair single-file drift, remove via receipt offline; run the same install path against an immutable legacy `0.1` archive (Adversary 14.8). +- **Block 10 (10.6):** inline the D7 failure-class enumeration into the test-matrix task so its done-condition is self-contained (Adversary 4.5). +- **Block 14:** add or extend a task to update the permanent protocol release policy encoding the pre-1.0 minor-release rule (or explicitly note it lands via spec sync of the `protocol` delta) (Adversary 17.4). +- **Block 14 (14.5):** extend the final Verify with a scenario-by-scenario coverage check of all seven delta specs against the implementation (Adversary 18.2). + +Nothing in Adversary's structure (4-way protocol split, flat docs block, separate final block) should displace Main's organization. + +## Blocking items + +None. All four merge items are additive; no Main task is incorrect or mis-ordered. The one compliance defect found (missing Step Types legend) is in the adversarial artifact, not Main, and requires no action. diff --git a/openspec/changes/support-non-asset-files/adversarial/state.json b/openspec/changes/support-non-asset-files/adversarial/state.json new file mode 100644 index 00000000..02ff67da --- /dev/null +++ b/openspec/changes/support-non-asset-files/adversarial/state.json @@ -0,0 +1,70 @@ +{ + "change": "support-non-asset-files", + "schema": "spec-driven", + "entries": { + "proposal": { + "artifactId": "proposal", + "mainPaths": ["proposal.md"], + "adversarialPaths": ["adversarial/artifacts/proposal.md"], + "reviewPath": "adversarial/reviews/proposal-review.md", + "dependencies": [], + "status": "reconciled", + "authoredAt": "2026-07-13T17:57:17Z", + "comparedAt": "2026-07-13T17:59:26Z", + "reconciledAt": "2026-07-13T18:14:00Z", + "notes": "Adopted all 7 review findings: added mandatory Non-goals section; explicit manifest declaration as single source of archive membership (syntax deferred to design); full validation boundary (missing/undeclared/unsafe/colliding paths); dropped premature build-manifest assets-map commitment in favor of asset/supplementary distinction; atomic multi-file skill lifecycle with drift-removal receipts; supplementary files prohibited from becoming independently addressable assets; compatibility upgraded from forward-compat note to BREAKING with explicit version boundary + legacy compat tests; Impact extended with security/compat test matrix, both guides, and root README. Partially adopted lockfile.mdx wording (review required, update conditional on design outcome)." + }, + "design": { + "artifactId": "design", + "mainPaths": ["design.md"], + "adversarialPaths": ["adversarial/artifacts/design.md"], + "reviewPath": "adversarial/reviews/design-review.md", + "dependencies": ["proposal"], + "status": "reconciled", + "authoredAt": "2026-07-13T18:10:26Z", + "comparedAt": "2026-07-13T18:19:40Z", + "reconciledAt": "2026-07-13T18:41:00Z", + "notes": "Adopted all 10 review findings (nothing material rejected; Main's structural base retained per the review itself). Added D3 shared tagged archive-plan derivation as single source of membership/classification; 0.2 build manifest replaces parallel assets+files maps with one all-entry hash map + strict version dispatch; protocol package and adapter SDK each ship the breaking change in their next pre-1.0 minor release; D7 grammar expanded with exhaustive path/tar checks (NUL, drive prefixes, Unicode/case-fold aliases, prefix collisions, symlink/hardlink identity, duplicate headers, non-regular entries, dist/ cleanup ordering) + per-class test matrix; D8 adapter contract rewritten to tagged payload unions with atomic stage/commit/rollback skill bundles; D9 receipt rewritten to tagged records with complete owned path sets, legacy tuples refined to empty companion sets (removed the inconsistent one-install-cycle orphan claim), receipt+materialization as one rollback unit; D11 scaffold writes a declared README.md by default and edit gains scaffold-or-remove for vanished declarations; Migration Plan reordered consumer-first with cafe as hard gate and irreversibility note; old-builder-ignores-files hazard added to Risks; all three Open Questions closed as decisions." + }, + "specs": { + "artifactId": "specs", + "mainPaths": [ + "specs/adapter__assets/spec.md", + "specs/authoring__facets/spec.md", + "specs/installation/spec.md", + "specs/protocol/spec.md", + "specs/protocol__content-hashing/spec.md", + "specs/protocol__integrity/spec.md", + "specs/protocol__schemas/spec.md" + ], + "adversarialPaths": [ + "adversarial/artifacts/specs/adapter__assets/spec.md", + "adversarial/artifacts/specs/authoring__facets/spec.md", + "adversarial/artifacts/specs/installation/spec.md", + "adversarial/artifacts/specs/protocol/spec.md", + "adversarial/artifacts/specs/protocol__content-hashing/spec.md", + "adversarial/artifacts/specs/protocol__integrity/spec.md", + "adversarial/artifacts/specs/protocol__schemas/spec.md" + ], + "reviewPath": "adversarial/reviews/specs-review.md", + "dependencies": ["proposal"], + "status": "reconciled", + "authoredAt": "2026-07-20T02:29:51Z", + "comparedAt": "2026-07-20T02:34:01Z", + "reconciledAt": "2026-07-20T02:59:20Z", + "notes": "Retained Main as the base across all seven capability specs. Adopted the two archive trust-boundary scenarios, known-versus-unknown upgrade guidance, and fidelity restorations for semantic-version removal, flat build-manifest shape, verifier I/O purity, authoring reconciliation/template/path/summary behavior, receipt isolation/frozen ordering, and adapter request identity. Rejected redundant schema-level and overlapping standalone requirements from the adversarial version. Strict OpenSpec validation passed; all adversarial artifacts and the comparison review were retained." + }, + "tasks": { + "artifactId": "tasks", + "mainPaths": ["tasks.md"], + "adversarialPaths": ["adversarial/artifacts/tasks.md"], + "reviewPath": "adversarial/reviews/tasks-review.md", + "dependencies": ["specs", "design"], + "status": "reconciled", + "authoredAt": "2026-07-20T04:29:40Z", + "comparedAt": "2026-07-20T04:31:25Z", + "reconciledAt": "2026-07-20T14:02:42Z", + "notes": "Retained Main as the structural base. Adopted an explicit full-cycle and legacy-0.1 end-to-end test, an inline D7 failure-class matrix, an explicit permanent protocol release-policy obligation through the authoritative delta/spec-sync path, and a final scenario-by-scenario coverage and rollout-order audit. Rejected the adversarial plan's alternative block structure and narrower adapter, documentation, producer-gate, and authoring coverage. Strict OpenSpec validation passed; the adversarial artifact and comparison review were retained." + } + } +} diff --git a/openspec/changes/support-non-asset-files/design.md b/openspec/changes/support-non-asset-files/design.md new file mode 100644 index 00000000..5e4992f3 --- /dev/null +++ b/openspec/changes/support-non-asset-files/design.md @@ -0,0 +1,250 @@ +## Context + +The facet pipeline enforces one file per asset end to end. Build collects only `facet.json` plus conventional asset paths (`collectArchiveEntries`, `packages/protocol/src/build/content-hash.ts`); archive verification rejects every other inner-tar entry at Step 6b outer-exclusivity (`packages/protocol/src/integrity/validate-archive.ts`); the adapter contract carries exactly one content string per asset (`installAsset(scope, type, name, content, metadata)`, `packages/adapter/src/types.ts`); and the machine-local receipt records only `(scope, type, name)` asset tuples (`packages/engine/src/install/receipt.ts`). + +The reconciled proposal introduces **supplementary files**: manifest-declared non-asset files that ship in the archive, are integrity-protected like everything else, and — only when they live inside a skill's directory — materialize atomically with that skill. Files elsewhere (root-level `README.md`, `LICENSE`, extras near agents/commands) ship but never touch disk at install time. + +Constraints inherited from the proposal: + +- Every archive entry MUST be derivable from an explicit declaration in the embedded `facet.json` (the outer-exclusivity trust root). No auto-discovery. +- Supplementary files MUST NOT become independently addressable assets (no asset type, adapter metadata, install scope, or lockfile asset tuples). +- The change is **BREAKING** at the archive-format level and MUST have an explicit version boundary; legacy asset-only archives MUST remain valid. + +A single principle organizes this design: **the embedded `facet.json` is the sole source of truth for archive membership and entry classification.** The build manifest records hashes, never a second description of which paths are assets or supplementary files. Every stage — build collection, hashing, verification, parsing, installation — derives membership and classification from the manifest through one shared operation (D3), so the classifications cannot drift apart. + +## Goals / Non-Goals + +**Goals:** + +- Define the manifest declaration shape, archive/build-manifest representation, verification rules, materialization boundary, and adapter contract for supplementary files. +- One shared derivation of the archive-entry set, consumed by build and verification alike — no duplicated allowlist logic. +- Preserve deterministic output within each archive format. Every build produced after the format transition MUST emit canonical `facetVersion: 0.2` output; consumers MUST continue accepting valid legacy `0.1` archives during the compatibility window. +- Make lockfile `0.2` the adapter-agnostic source of truth for every materialized logical file and its canonical per-file integrity, while the machine-local receipt records what this machine owns for rollback and offline removal. +- Make illegal states unrepresentable at the security- and data-loss-critical boundaries (adapter operations, receipt records, parsed archive results) via tagged unions, not optional fields plus prose invariants. + +**Non-Goals:** + +- No `facet info` command or README rendering (future capability; this change only makes the bytes available). +- No companion-directory semantics for agents or commands. +- No filesystem metadata preservation (exec bits, symlinks, hard links, timestamps, ownership) for supplementary files. +- No glob/pattern declaration in v1 (see D2). +- No registry-side (cafe) implementation — sequencing is a hard constraint in the Migration Plan, but the code is out of this repo. + +## Decisions + +### D1: Declaration shape — per-skill `files` plus top-level `files` + +The facet manifest gains two declaration sites, each owning a disjoint region of the tree: + +- **`SkillDescriptor.files?: string[]`** — companion files for one skill, as paths **relative to the skill directory** (e.g. `references/art.md` resolves to `skills/cowsay/references/art.md`). These are the only supplementary files that materialize, and they install/remove atomically with their owning skill. +- **Top-level `files?: string[]`** — repo-relative paths for everything else (`README.md`, `LICENSE`, `agents/notes.md`, `ideas.txt`). Shipped and hashed, never materialized. Top-level entries MUST NOT resolve under `skills/` — skill companions have exactly one declaration site. + +Rationale: ownership (which skill do these files belong to?) is the load-bearing semantic — it drives materialization, atomic lifecycle, and receipt bookkeeping. Giving each tree region exactly one declaration site makes ownership unambiguous at the point of declaration. The lists themselves are unrestricted strings, so disjointness, path safety, declared-skill membership, and collision freedom are enforced as schema-narrowing constraints by the shared archive-plan operation (D3) — the schema shape makes ownership *unambiguous*, and the validator makes it *checked*; neither claim substitutes for the other. + +*Alternative considered:* a single top-level `files` list with ownership derived by `skills//` path prefix. Rejected: one declaration site is simpler, but ownership becomes an inferred invariant instead of a declaration-site fact, and a path under `skills//` needs a bespoke validation rule instead of failing the obvious "companion declared on a skill that exists" check. + +### D2: Explicit per-file enumeration; no globs + +Both `files` lists enumerate exact paths. The embedded `facet.json` is the trust root for outer exclusivity; a glob (`skills/cowsay/**`) in the embedded manifest would let an attacker add undeclared files to a materialized skill directory while still "deriving" from the manifest — precisely the supply-chain hole Step 6b exists to close. Authoring ergonomics are addressed by the edit flow (D11), not by the artifact format. + +*Alternative considered:* glob expansion at build time (source manifest has globs, embedded manifest gets exact paths). Rejected for v1: the archive currently embeds the source `facet.json` verbatim (its per-file hash equals the source file's), and rewriting it at build breaks that property. MAY be revisited as a pure authoring convenience later. + +### D3: One shared archive-plan derivation + +The protocol package SHALL expose a single pure operation that validates both declaration sites (per D7's grammar) and derives a **tagged archive plan**: every planned entry is classified as exactly one of `manifest`, `primary-asset`, `skill-companion` (carrying its owning skill), or `archive-only`. Build collection, per-entry hashing, archive verification, the parsed archive result, and installation MUST all consume this one operation. No stage maintains its own membership or classification logic. + +Rationale: today's outer-exclusivity check and `collectArchiveEntries` already construct membership independently — the exact duplicated-allowlist drift this change would otherwise multiply across four more call sites. Membership is a security boundary; it gets one implementation. + +*Alternative considered:* separate build-side and verify-side derivations (the status quo, extended). Rejected: duplicated membership logic at a trust boundary is how producers and verifiers drift into accepting different sets. + +### D4: Build manifest — unconditional 0.2 output; strict version dispatch; pre-1.0 minor releases + +Every build produced after this change SHALL emit `facetVersion: 0.2`, whether or not the facet declares supplementary files. Asset-only facets therefore use the same current format as facets with supplementary files. Producers SHALL NOT conditionally emit `0.1`; `0.1` remains a legacy input format supported by consumers during a compatibility window and MAY be deprecated by a separate future change. "After this change" means from the producer release (Migration Plan step 5) onward: the earlier consumer-side bridge release intentionally continues emitting `0.1` while `0.2` verification deploys everywhere, and that sequencing is not an exception to this rule but the path to enabling it. + +The `0.2` build manifest SHALL replace the `assets` map with a single `files` map: canonical inner-tar path → `sha256:`, covering every entry — `facet.json`, primary asset files, and supplementary files. The map carries hashes only. Asset/supplementary classification is NEVER read from the build manifest; it is derived from the embedded `facet.json` via the archive plan (D3). The completeness rule is single: the `files` key set MUST exactly equal the observed inner-tar entry set. + +Verifiers SHALL dispatch on `facetVersion` exactly once, at parse time: the exact legacy schema and rules apply to `0.1`; the rules above apply to `0.2`; any other version returns a structured `UNSUPPORTED_FACET_VERSION` failure carrying the observed version and supported versions. A malformed `0.2` manifest MUST NOT be reinterpreted as `0.1` — no fallback between versions. A `files` key in a `0.1` manifest (or an `assets` key in `0.2`) fails schema validation, making the illegal combinations unrepresentable in validated data. + +The CLI SHALL render unsupported-version failures as upgrade guidance. For a known format transition, one CLI-side compatibility table SHALL map the facet format to the minimum supporting CLI release, producing guidance such as: “This facet uses archive format 0.2, which this CLI does not support. Update agent-facets to or later.” For an unknown future format, the CLI SHALL advise updating to the latest release without inventing a minimum version. Already-released CLIs cannot be retrofitted and MAY continue showing their existing generic validation error; a consumer-first bridge release SHOULD add this handling before any producer emits `0.2`. + +This archive-format boundary is distinct from package release versioning. While `@agent-facets/protocol` and `@agent-facets/adapter` remain pre-1.0, breaking contract changes SHALL increment each package's minor version rather than its major version. This change therefore ships in the next minor release of each package. The permanent protocol release policy SHALL be updated by this change to encode the pre-1.0 rule; after 1.0, breaking changes SHALL require a major release. + +*Alternatives considered:* conditional `0.1`/`0.2` producer output (rejected: creates two current producer modes and prolongs ambiguity about which format a newly built facet uses; the user-facing rule is simpler when all new output is `0.2`); parallel `assets` + `files` maps (rejected: duplicates classification already derivable from the embedded manifest and violates the single-source-of-truth principle); dropping `0.1` verification immediately (rejected: existing published facets remain valid and require a compatibility window); package major releases (rejected: project policy uses minor releases for breaking changes while packages remain pre-1.0). + +### D5: Verification — raw-header validation, exact set equality, fail-closed + +Verification of a `0.2` archive SHALL: + +1. Validate raw tar entries **before** constructing any path-keyed map (a lossy map silently collapses duplicate paths — a smuggling vector). Duplicate paths, non-regular entries (symlinks, hard links, directories, devices), and unsafe or non-canonical paths (per D7) are each structured rejections. The same raw validation applies to the **outer** container before either required entry is selected: duplicate, portable-alias, or non-regular outer entries are rejected rather than letting parser-dependent collapse decide which `build-manifest.json` or `archive.tar.gz` is authoritative. JSON artifacts consumed during verification (the build manifest and the embedded `facet.json`) SHALL reject duplicate object member names before schema validation — `JSON.parse`'s last-key-wins behavior would otherwise let two parsers see different hash maps in one document. +2. Validate the embedded `facet.json` and derive the expected entry set via the archive plan (D3) — never from the build manifest. +3. Compare expected and observed canonical path sets for **exact equality** (undeclared extra entries and declared-but-missing entries are both rejections, as today). +4. Require exactly one `files` hash per expected path and no hash for any other path, then byte-verify every entry against its hash. + +Every expected failure mode SHALL remain a structured result variant — no thrown errors escape the verification contract. Older verifiers fail closed on `0.2` archives (unknown entries → outer-exclusivity rejection), which is the correct security posture for a consumer that cannot enforce the new rules. + +### D6: Supplementary files are opaque bytes; parsed results are tagged + +Supplementary content is read, hashed, archived, and written **verbatim**: no front-matter merge, no line-ending normalization, no empty-content rule, binary permitted. `ArchiveEntry.content` widens to `string | Uint8Array` (hashing and `nanotar` already accept bytes). + +`string | Uint8Array` alone does not encode which content is prompt text and which is opaque, so the successful parsed/verified archive result SHALL carry entries as **tagged data**: primary assets, skill companions grouped by owning skill, and archive-only supplementary bytes are distinct variants (mirroring the D3 plan). Text decoding and front-matter reconciliation apply only after narrowing to a primary asset; supplementary data stays bytes end to end. Classification via optional fields whose combinations can disagree is prohibited. + +### D7: Path validation grammar + +At build (inside the D3 operation) and at archive verification, every declared supplementary path MUST satisfy: + +- non-empty, relative, and already canonical: no empty, `.`, or `..` segments; no backslashes; no NUL bytes; no absolute-path, drive, or URL-like prefixes; +- **portable across supported filesystems**: no control bytes (0x00–0x1F) or the characters `<`, `>`, `:`, `"`, `|`, `?`, `*` in any segment; no segment equal (case-insensitively, with or without an extension) to a Windows-reserved device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`, `LPT1`–`LPT9`); no segment ending in a dot or a space — otherwise an archive valid on Linux fails only at materialization on a supported Windows client; +- **regular files only** at build time: the declared path MUST resolve through existing parents to a regular file inside the facet root — symlinks and hard links are rejected (resolved source identity is checked, not just spelling); +- the exact root path `facet.json` is excluded because it is the authoritative embedded manifest; the basename `facet.json` MAY appear at any other path (for example, `skills/example/examples/facet.json`); +- supplementary entries MUST NOT collide with any conventional primary asset path derived from the manifest; names owned only by the outer archive, including `build-manifest.json` and `archive.tar.gz`, MAY appear as supplementary inner-archive paths; +- site rules: top-level entries MUST NOT resolve under `skills/`; per-skill entries MUST NOT be `SKILL.md` and MUST resolve below their skill's directory; +- collision-free across the whole planned entry set, where collision includes: exact spelling, canonical Unicode form (NFC/NFD aliases), portable case folding (case-insensitive filesystems), resolved source identity, and file/directory prefix conflicts (`foo` as a file vs. `foo/bar`). + +Missing declared files fail the build with structured errors, and **all source inputs SHALL be validated before any `dist/` cleanup runs** — a declared input must never be destroyed before its missing-file error can be reported. Each failure class above maps to a distinct structured `ValidationError`, and the test suite SHALL carry a matrix with at least one case per class (traversal, absolute/drive paths, backslashes, NUL, empty/`.` segments, Unicode/case aliases, Windows-reserved names and trailing dot/space, forbidden portable characters, prefix collisions, symlinks, hard links, duplicates, exact-root-`facet.json` collisions, conventional-primary-path collisions, undeclared entries, missing declarations, tampered bytes). + +### D8: Adapter contract — tagged asset payloads, atomic skill bundles (BREAKING) + +The adapter SDK's install, read, and delete requests/results SHALL become **tagged unions keyed by asset type**: + +- the **skill** variant carries the `SKILL.md` text plus a canonical map of companion paths (relative to the skill root) to bytes — an empty map is legal and is how a companion-less skill is expressed; +- **agent** and **command** variants carry their existing single content string and structurally cannot carry companions; +- no variant exists for supplementary files (they never reach adapters — D12). + +Ownership is **engine-supplied, per operation**: adapters never persist ownership metadata or infer it from disk. A skill install request carries the new bundle plus the engine-verified set of previously-owned companion paths (from the lockfile/receipt), so replacement removes exactly the owned paths absent from the new bundle. A skill delete request carries the engine-verified owned companion path set; only those paths (plus the primary) are deleted. A skill read request carries the owned companion path set to return, so reads cannot sweep user files into ownership. Agent and command variants structurally carry no ownership sets. Every engine-supplied companion path — new or previously owned — SHALL be validated as relative, canonical, and confined below the resolved skill root **before any filesystem access**, in all three operations; a malformed or escaping path rejects the whole request. + +*Alternative considered:* adapter-managed ownership metadata (a manifest file inside the skill directory). Rejected: it duplicates ownership state the engine already owns, turns adapter storage into a trusted input, and breaks the D10 rule that the receipt mirrors the lockfile as the single ownership record. + +This replaces the earlier optional-parameter shape (`companions?` beside `assetType`), which represented illegal combinations (an agent with companions; a skill call silently omitting its bundle) and policed them only by prose. Tagged variants give implementations an exhaustive branch. + +**Adapter API identifier: `0.0` → `0.1` (hard cutover, no bridge).** The `adapter__sdk` spec binds identifier `0.0` to "the current positional method contract." Replacing that contract with the tagged unions above is exactly the event that identifier is designed to signal, so the SDK's canonical `ADAPTER_API_VERSION` SHALL advance from `0.0` to `0.1`. That constant is the single source of truth: `SUPPORTED_ADAPTER_APIS` SHALL continue to derive from it (yielding a `{0.1}` support set), first-party adapters SHALL derive their package/runtime declaration from it, and no consumer SHALL hardcode the token. The compatibility machinery classifies by exact-identifier equality and cannot inspect method signatures, so a `0.0`-declaring adapter built against the tagged contract and one built against the positional contract are indistinguishable to the verifier — leaving the identifier at `0.0` would let a positional adapter and a tagged CLI both classify as `supported` yet be wire-incompatible, the precise silent-incompatibility failure the identifier exists to prevent. A `0.0` adapter therefore SHALL be well-formed but unsupported by a `0.1` CLI and SHALL fail closed before any contract method or state write, surfacing the existing reinstall diagnostic. + +*Alternatives considered:* a dual `{0.0, 0.1}` support window with runtime dispatch between positional and tagged calls (rejected: it requires the CLI to carry and translate both call shapes indefinitely, and the verifier cannot tell which shape a `0.0` bundle actually implements, so the "support" would be unsound; a single-token cutover with fail-closed `0.0` and explicit reinstall is simpler and safe); leaving the identifier at `0.0` and relying on package semver (rejected: `adapter__sdk` forbids inferring the API identifier from package versions, and semver cannot gate the wire contract). The adapter API axis is independent of the archive `facetVersion` and lockfile/receipt versions; a consumer classifies each separately. + +A skill install SHALL be **one adapter operation with all-or-nothing semantics**: stage the complete replacement bundle, remove previously-owned companion paths absent from the new bundle, and commit — or roll back leaving no partial bundle. Skill deletion likewise removes the primary file plus all recorded owned companions as one operation, never touching unowned files. Expected failures are structured result values. These atomicity guarantees cover **handled failures within a running operation** (staged writes plus journal-driven rollback); they are not a durable write-ahead log. Recovery from a process crash mid-operation is defined as **idempotent re-install convergence**: re-running install compares every locked per-file integrity against disk and repairs or completes the bundle, so a crash can leave at most a state the next run converges from — never a state that deletes unowned files. + +The SDK's filesystem helpers SHALL centralize the security-sensitive machinery — companion-path containment within the resolved skill root, staging, commit/rollback, owned-path removal, empty-directory pruning — so adapters built on the helpers (including `claude-code`) inherit correct behavior. Custom-I/O adapters MUST satisfy the same observable contract. Integration tests SHALL inject failures at every write/delete/commit boundary. Front-matter reconciliation applies only to the primary file; companion bytes are written verbatim. + +Engine's skip-if-identical logic extends per-companion: unchanged companions are skipped; changed ones are journaled with previous bytes for rollback. + +*Alternatives considered:* optional `companions?` parameters (rejected: optional-fields-as-discriminator, see above); separate per-companion install/delete methods (rejected: multiplies journal entries and failure surfaces, and permits a partially-companioned skill between calls); engine writing companions directly (rejected: adapters own all storage paths and formats); deleting the whole skill directory (rejected: destroys unowned user files — deletion is ownership-based). + +### D9: Asset names follow Agent Skills, remain single-segment, and share defined namespaces + +The canonical `0.2` asset-name grammar SHALL follow the Agent Skills `name` field convention: https://agentskills.io/specification#name-field. Facets normatively interprets the specification's enumerated character ranges as ASCII: an asset name MUST contain 1–64 lowercase ASCII letters (`a-z`), digits (`0-9`), or hyphens; MUST NOT start or end with a hyphen; and MUST NOT contain consecutive hyphens. `/` is invalid in every asset name. + +Facets SHALL apply this same grammar to skills, commands, and agents. Applying the Agent Skills grammar to commands and agents is a Facets extension that gives all asset types one naming convention and one protocol validator. Protocol schemas, validator comments, generated schema documentation, and user-facing naming documentation MUST link to the Agent Skills `name` field as the external convention being implemented while stating Facets' normative ASCII interpretation. + +A skill named `review` is represented by the top-level directory `skills/review/`, whose required primary file is `skills/review/SKILL.md`. The manifest skill name, installed directory name, and materialized `SKILL.md` name metadata MUST agree. Declared companion paths beneath that root MAY contain directories of arbitrary safe depth, such as `scripts/run.ts`, `references/api.md`, or `assets/logo.png`; those path separators are not part of the skill name. A command named `review` is represented by `commands/review.md`, and an agent named `review` by `agents/review.md`. + +Skills and commands SHALL occupy one logical namespace: the skill-name and command-name sets MUST be disjoint. A facet declaring both skill `review` and command `review` fails with a structured collision error identifying `skills.review` and `commands.review`. Agents remain in a separate namespace and MAY share a name with a skill or command. + +The single-segment grammar and shared namespace SHALL be validated before archive planning, adapter selection, or filesystem writes. Local `0.2` builds and `0.2` archive verification MUST consume the same protocol validation. Legacy `0.1` verification SHALL retain the previous multi-segment and cross-type-collision rules so existing archives remain consumable; there is no fallback from an invalid `0.2` manifest to the `0.1` grammar. + +`parseAssetNameSegment` becomes the canonical current-format asset-name parser. Multi-segment parsing remains isolated to the legacy `0.1` verifier and MUST NOT appear in current manifest types or authoring APIs. Internally composed or slash-namespaced assets are not part of the `0.2` model; any future composition design must preserve single-segment asset identities rather than encoding hierarchy into names. + +*Alternatives considered:* retaining slash-separated internal names (rejected: conflates asset identity with filesystem hierarchy and contradicts the one-directory/one-file source model); using a Facets-only naming grammar without citing Agent Skills (rejected: loses the shared ecosystem convention even though the effective constraints align); interpreting “Unicode lowercase alphanumeric” beyond the specification's explicit `a-z` and `0-9` ranges (rejected: Unicode category and normalization behavior would make portable validation weaker and less deterministic); validating skill/command collisions only after adapter selection (rejected: facet validity would vary by adapter); putting agents in the shared namespace (rejected: agents do not occupy the skill/command invocation namespace). + +### D10: Lockfile 0.2 pins every materialized file; receipt mirrors machine ownership + +`facets.lock` SHALL use `lockfileVersion: 0.2` for the current alpha schema. Version dispatch MUST use exact equality, never numeric ordering: legacy numeric `1` identifies the previous alpha schema, while numeric `0.2` identifies this schema. `FACET_ARCHIVE_VERSION` and `LOCKFILE_VERSION` SHALL remain separate constants that both currently equal `0.2`; their equality is release alignment, not a permanent invariant, because archive and resolution formats may evolve independently. + +Every lockfile asset entry SHALL contain its adapter-agnostic identity (`scope`, `type`, `name`) plus a required, deterministically sorted `files` array. Each file record SHALL be `{ path, integrity }`, where `path` is the canonical inner-archive path and `integrity` is the `sha256:` hash of that archive entry's exact canonical bytes. + +- A skill entry's `files` SHALL contain `skills//SKILL.md` plus every declared companion beneath `skills//`. +- An agent entry's `files` SHALL contain exactly `agents/.md`. +- A command entry's `files` SHALL contain exactly `commands/.md`. +- Archive-only supplementary entries, including root `README.md`, SHALL NOT appear in `assets[].files` because they are not materialized; the facet-level integrity continues to pin them. +- Companion files remain subordinate file-integrity records inside their owning skill entry. They SHALL NOT become independent assets, acquire scopes, or receive standalone asset tuples. + +The lock writer SHALL derive `assets[].files` from the verified D3 archive plan's materialized subset. For every included path, it SHALL persist the recomputed hash that has already been reconciled with the 0.2 build manifest's `files` map; it MUST NOT trust or blindly copy a self-declared build-manifest value. + +Before any materialization, install SHALL require exact agreement among: + +1. the lockfile facet-level integrity and the recomputed archive integrity; +2. the lockfile asset identities and the verified materialization plan; +3. every lockfile asset's complete file path set and the files owned by that planned asset; +4. every lockfile per-file integrity, the recomputed archive-entry hash, and the corresponding verified build-manifest hash. + +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. + +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. + +A current loader SHALL recognize legacy numeric lockfile version `1` only as the previous alpha schema. Normal install MAY migrate a verified legacy lockfile to `0.2`; frozen legacy installs retain legacy behavior and do not rewrite. A `0.2` archive requires a `0.2` lockfile. When the stable lockfile v1 schema is eventually released, support for legacy-alpha numeric `1` SHALL be removed rather than reinterpreted or shape-sniffed: an old alpha lockfile SHALL fail with an actionable instruction to delete and regenerate it. The future stable v1 schema then owns numeric `1` exclusively. + +*Alternatives considered:* facet-level integrity alone (rejected: cryptographically protects the archive but cannot directly attribute drift to one materialized file); companion paths without per-file hashes (rejected: records ownership but not file-level integrity); adapter-encoded hashes in the lockfile (rejected: makes a portable facet resolution vary by adapter and machine); independent companion asset tuples (rejected: companions have no independent identity or scope); permanently coupling lockfile and archive version constants (rejected: they describe different artifacts and will eventually diverge); preserving legacy numeric `1` after stable v1 launches (rejected: one version identifier cannot safely select two schemas). + +### D11: `README.md` is first-class in create and edit; extensionless `README` is supported + +`README.md` SHALL be the preferred conventional facet document. `facet create` SHALL generate `README.md` by default in both interactive and headless invocations: a flag-driven headless create writes the same seeded, declared `README.md` unless the author passes an explicit opt-out flag, so the two entry points never produce different manifests by default. The exact extensionless path `README` SHALL also be recognized as a first-class README by `facet edit` and the build/manifest workflow. Both remain normal top-level supplementary-file declarations in `facet.json.files`; the manifest SHALL NOT gain a README-specific field or duplicate source of truth. + +The interactive `facet create` wizard SHALL include a dedicated README step or card, separate from asset management. README SHALL be enabled by default but optional. The wizard SHALL seed editable `README.md` content from the facet name and description, allow the author to open and edit that content before confirmation, and allow the author to disable README creation. The confirmation preview SHALL list `README.md` explicitly. On apply, the wizard SHALL atomically write `README.md` and add its exact path to top-level `files`. The generated template is an initial value only; later identity edits MUST NOT silently regenerate or overwrite authored README content. + +The `facet edit` wizard SHALL show the exact root paths `README.md` and `README` in a dedicated facet-level README panel rather than generic supplementary-file reconciliation. For each recognized path, behavior depends on its current state: + +- present and declared: offer Edit or Remove; +- present but undeclared: offer Adopt or Edit-and-Adopt; +- declared but missing: offer Scaffold at that same path or Remove Declaration; +- absent and undeclared: offer Create, defaulting to `README.md`. + +If both `README.md` and `README` exist, the dedicated panel SHALL show both independently; neither file is silently ignored or overwritten. Adopt SHALL preserve existing bytes unless the author explicitly edits them. Remove SHALL queue both file deletion and declaration removal. Scaffold/Create SHALL queue the file write and declaration addition. All README operations remain transactional: no file or manifest change occurs until the existing Apply confirmation, and the confirmation summary SHALL identify the exact README path and operation. + +`facet edit`'s generic scanner SHALL still detect undeclared files inside declared skill directories and offer to add them to that skill's `files`. It SHOULD detect other common root-level supplementary files such as `LICENSE`, but `README.md` and `README` SHALL be routed only through the dedicated README panel so they do not appear twice. + +For any declared supplementary file other than `README.md` or `README` that has vanished from disk, edit SHALL offer scaffold-or-remove, mirroring the existing missing-asset flow. + +*Alternatives considered:* always requiring README (rejected: first-class does not mean mandatory); generating extensionless `README` by default (rejected: `README.md` is the preferred authored format); adding a README-specific manifest field (rejected: duplicates top-level `files` membership); silently regenerating README after identity edits (rejected: destroys authored documentation); treating README only as a generic discovered file (rejected: misses the intended first-class authoring experience). + +### D12: Materialization boundary is engine logic, not adapter logic + +Engine passes companions only inside skill-variant payloads (D8); archive-only supplementary files never reach `materialize`. Adapters never see non-skill supplementary files, so the "ships but does not materialize" rule cannot be violated by an adapter bug — the data simply isn't handed over. + +### D13: Adapter-compatibility preflight precedes archive dispatch and materialization + +The `installation` spec's "Facet operations require compatible selected adapters before mutation" gate runs *before* archive-version dispatch (D4), per-file integrity reconciliation (D5/D10), the tagged skill-bundle contract (D8), and any project or materialized-state write. A selected adapter declaring the positional `0.0` API is unsupported by a `0.1` CLI and SHALL cause the operation to fail at this preflight — before the archive is even parsed for `facetVersion` — with the reinstall diagnostic. Ordering the adapter-API check ahead of the archive-format check keeps the two version axes independent: a `0.2` archive with an incompatible `0.0` adapter fails on the adapter, not the archive, and the user is told to reinstall the adapter rather than shown an archive-format message. This ordering is stated once here and referenced by the `installation` delta rather than duplicated per requirement. + +*Alternative considered:* checking archive format first (rejected: it would surface an archive-upgrade message when the real, blocking problem is an incompatible adapter, and would parse untrusted archive bytes before confirming the adapter that will consume them is even loadable). + +## Risks / Trade-offs + +- **[Old builders silently ignore `files`]** — manifest validation tolerates unrecognized fields, so an old CLI builds a facet that *declares* supplementary files but *omits* the bytes, with no error. → Documentation MUST state the minimum producer version for `0.2`; examples SHOULD pin it; compatibility fixtures MUST prove which producer versions emit declared files. This hazard cannot be repaired retroactively in already-shipped tolerant parsers — docs and fixtures are the only lever. +- **[Path aliases or crafted tar headers bypass membership checks]** → One shared plan derivation (D3) + the D7 grammar + raw-header validation before lossy maps (D5); the per-failure-class test matrix is mandatory. +- **[A failed skill update leaves a half-written directory]** → Atomic stage/commit/rollback in the adapter contract (D8) with injected-failure tests at every write/delete/commit boundary. +- **[Receipt corruption causes over-deletion]** → Receipts are untrusted (D10): containment + project-identity checks precede deletion; unowned paths are never deleted. +- **[Lockfile growth from per-file integrity]** → Only materialized files are copied into `assets[].files`; archive-only metadata remains covered by facet integrity. File records are canonical and sorted for stable, reviewable diffs. +- **[Legacy alpha numeric `1` conflicts with future stable v1]** → Current releases explicitly classify it as legacy-alpha-1 and migrate to `0.2`; stable v1 removes that parser and emits an actionable delete-and-regenerate error for old-shape numeric-1 files. +- **[Adapter transformations obscure raw drift comparison]** → Lockfile hashes remain canonical and adapter-agnostic; `readAsset` must project installed primary content back into its canonical logical form. Adapter-specific bytes never enter version-controlled resolution state. +- **[Generated README content overwrites author edits]** → The template is applied only on explicit Create or Scaffold. Identity changes and ordinary edit sessions MUST preserve existing README bytes unless the author chooses to edit them. +- **[All newly built facets require a 0.2-capable consumer]** → Verification support SHALL deploy consumer-first, including cafe, before producer enablement. A bridge CLI SHOULD recognize `0.2` and render the D4 upgrade message; older pre-bridge CLIs may still show a generic validation failure. Immutable fixtures MUST prove that new consumers continue accepting valid `0.1` archives. +- **[Two supported archive versions create implementation branches]** → Version dispatch happens exactly once at parse time, with no cross-version fallback and immutable fixtures for both schemas (D4/D5). Removing `0.1` support requires a separate future deprecation change. +- **[Arbitrary companion bytes inflate archives / decompression pressure]** → Size/count limits are consumer and registry policy, not protocol format (see Open Questions closure); existing decompression handling applies to the whole archive. +- **[Unsupported format surprises installers]** → Build output SHALL display the emitted `facetVersion` and complete entry listing; install SHALL return a structured unsupported-version failure, and the CLI SHALL render actionable upgrade guidance from its single compatibility table. +- **[Third-party adapter breakage]** → SDK-helper adapters inherit companion support via the helpers; only custom-I/O adapters must implement the widened contract. The tagged unions make the migration mechanical and exhaustively checkable. +- **[A positional `0.0` adapter is silently accepted by a tagged CLI]** → The identifier bump to `0.1` (D8) makes the wire-contract change visible to exact-identifier compatibility; a `0.0` adapter is unsupported and fails closed before any contract method or state write. Fixtures MUST prove a positional `0.0` bundle is rejected by a `{0.1}` CLI. +- **[Old `0.0` adapters break at the CLI cutover]** → Consumer-first release ordering (Migration Plan step 4 before step 5): SDK and all three first-party adapters publish `0.1` before any CLI requires it; existing `0.0` CLIs keep selecting compatible `0.0` releases. Recovery from a broken install is the reinstall command the compatibility diagnostic already surfaces. +- **[Two declaration sites could confuse authors]** → The disjointness rule (D1/D7) yields a precise error pointing at the correct site; edit-flow detection (D11) writes declarations to the right place automatically. +- **[Existing source manifests use slash-namespaced assets or duplicate skill/command names]** → Their published `0.1` archives remain consumable, but rebuilding as `0.2` fails with actionable validation errors before output is changed. Authors MUST rename the assets; invalid `0.2` manifests are never interpreted using legacy rules. + +## Migration Plan + +Consumer-first: verification support ships everywhere before any producer can emit the new format. + +1. **protocol (next minor release)**: adopt the pre-1.0 breaking-release policy; add the D3 plan operation, D7 path grammar, D9 current-format asset-name grammar and shared namespace validation, D4 versioned build-manifest schemas with strict dispatch and structured unsupported-version results, D5 verification, D6 tagged parsed results, and D10 lockfile `0.2` with exact legacy-1/current-0.2 dispatch, per-file integrity schemas, and mismatch result types — plus immutable fixtures for both archive versions. +2. **engine/CLI bridge (consumer side)**: loaders and cache consume the tagged parsed result; verified legacy lockfiles and receipts migrate during normal installs while frozen legacy state retains legacy behavior; unsupported versions and per-file integrity mismatches render actionable diagnostics. Producers still emit `0.1` in this bridge release. +3. **cafe registry (out of repo, hard gate)**: deploys `0.2` verification while retaining `0.1`. Producer enablement MUST NOT ship before this lands. +4. **adapter SDK (next minor release) + first-party adapters**: tagged payload unions, atomic bundle helpers (D8), injected-failure tests; the SDK bumps `ADAPTER_API_VERSION` `0.0`→`0.1` and every first-party adapter migrates and declares package/runtime API `0.1` — claude-code, opencode, and codex. This SDK/adapter release publishes `0.1` to npm *before* step 5. Until it does, existing `0.0` CLIs keep selecting the highest compatible `0.0` adapter release; no CLI whose supported set is `{0.1}` ships until all three first-party adapters have published `0.1`. +5. **engine/CLI producer release**: every build emits archive `0.2` and writes only lockfile `0.2`; materialize passes skill bundles; receipts write the `0.2` ownership shape; create and edit ship the D11 first-class `README.md`/`README` authoring flow; generic supplementary-file detection/add/remove flows land. +6. **docs** (Article III): update `docs/specification/archive.mdx` (membership rules, single `files` hash map, version dispatch), `build.mdx` (plan derivation, validation-before-cleanup, displayed version), `manifest.mdx` (both `files` fields, minimum-producer-version warning, linked Agent Skills naming convention, single-segment asset names versus nested companion paths, shared skill/command namespace), `integrity.mdx` (all-entry and per-locked-file coverage), `lockfile.mdx` (lockfile `0.2`, per-materialized-file integrity, legacy-alpha-1 migration, stable-v1 regeneration boundary), `commit.mdx` (receipt ownership and transactional reconciliation), `install.mdx` (materialization boundary, atomic skill bundles, mismatch diagnostics), `docs/guides/create-your-first-facet.mdx` and `docs/guides/install-facets.mdx` (asset-only phrasing, README workflow), root `README.md`. + +Before the future stable lockfile v1 release, legacy-alpha-1 parsing SHALL be removed and replaced with actionable delete-and-regenerate guidance for old-shape numeric-1 files. + +Rollback: before any `0.2` artifact is published, producer rollout MAY revert to the bridge release. After publication or multi-file skill materialization, producer emission MAY be paused, but consumers MUST retain `0.1` and `0.2` verification plus receipt-aware deletion. Removing `0.1` support requires a separately reviewed deprecation change. For the adapter API axis, rollback means restoring/reinstalling compatible adapter and CLI releases: because compatibility is exact-identifier and cannot be inferred from package semver, a `0.1` CLI cannot be made to accept a `0.0` adapter by changing versions — recovery is reinstalling a `0.1` adapter (or downgrading the CLI to a `0.0` release paired with `0.0` adapters), never a version bump alone. + +## Open Questions + +None remaining — the three raised during drafting are closed as decisions: + +- Registry README presentation: out of scope (proposal non-goal); the all-entry `files` map makes it cheap for a future change. +- Archive size/entry-count limits: consumer and registry configuration policy, not protocol format, for this change. +- Edit behavior for vanished declared files: decided in D11 (scaffold-or-remove, mirroring the missing-asset flow). diff --git a/openspec/changes/support-non-asset-files/proposal.md b/openspec/changes/support-non-asset-files/proposal.md new file mode 100644 index 00000000..3d8500b7 --- /dev/null +++ b/openspec/changes/support-non-asset-files/proposal.md @@ -0,0 +1,62 @@ +## Why + +The facet pipeline enforces a one-file-per-asset invariant end to end: build collects only `facet.json` plus conventional asset paths, and archive verification rejects every other inner-tar entry ("outer exclusivity"). Real facets need more than prompts — a README, a LICENSE, development notes — and skills routinely ship supporting files (references, scripts, templates) alongside `SKILL.md` per the Agent Skills convention. Today those files are silently dropped at build and hard-rejected at verification, so authors cannot ship them at all. They need a way to travel with the facet without being treated as independently installable assets — that distinction frames the whole change. + +## What Changes + +- **Facets can track non-asset files.** The facet manifest gains a way to declare files that are not skills, agents, or commands (e.g. `README.md`, `LICENSE`, `DEVELOPMENT.md`, `ideas.txt`). Every supplementary archive entry — including every skill companion file — MUST be derivable from an explicit manifest declaration; archive membership stays explicit and reviewable, with no recursive auto-discovery. Declaration syntax (per-file vs. pattern) is a design decision. Missing declared files, undeclared entries, unsafe paths (traversal, absolute, backslashes), and colliding resolved paths SHALL fail build validation. +- **Asset identity is simplified in 0.2.** Skill, command, and agent names MUST follow the Agent Skills `name` field convention as a single ASCII segment; slash-namespaced assets are no longer valid in current-format manifests. Skills and commands SHALL share one logical namespace and MUST NOT use the same name; agents remain separate. Legacy 0.1 archives retain their legacy validation during the compatibility window. +- **Integrity covers every file, not just assets.** Non-asset files MUST be hashed per-entry in the build manifest and included in the tar bytes that produce the content-integrity hash. Verification MUST recompute hashes for all entries, asset or not. +- **Lockfile integrity becomes file-addressable.** Lockfile `0.2` SHALL record the verified canonical path and integrity of every materialized primary or companion file inside its owning asset entry. Install MUST reconcile those records against recomputed archive hashes and fail with the exact mismatching path. Archive-only supplementary files remain protected by facet-level integrity without becoming lockfile assets. +- **Outer exclusivity is relaxed, not abandoned.** Every inner-tar entry MUST still be derivable from the embedded `facet.json` — the derivable set expands to include declared non-asset files. Undeclared extra files remain a rejection; the supply-chain rationale for the rule is preserved. +- **Skill directories become multi-file.** Declared files under `skills//` (beyond `SKILL.md`) SHALL be shipped and SHALL install and remove atomically with their owning skill through the adapter contract. Adapter install, read, and delete SHALL receive engine-verified owned-companion path sets from the lockfile and receipt, so adapters never infer ownership from disk; receipt data stays sufficient for offline drift removal. **BREAKING** for the adapter SDK: the contract is one content string per asset today, so multi-file skills change the interface for third-party adapters and require migrating every first-party adapter (claude-code, opencode, codex). +- **The adapter API identifier advances from `0.0` to `0.1`.** Identifier `0.0` names the current positional contract; the tagged request/result contract SHALL increment it to `0.1`. The SDK SHALL stamp `0.1`, first-party adapters SHALL declare package and runtime API `0.1`, and the CLI supported set SHALL be exactly `{0.1}`. There is no positional/tagged bridge: a `0.0` adapter stays well-formed but SHALL be unsupported by a `0.1` CLI and SHALL fail closed — before any contract method or state mutation — with the existing reinstall guidance. This axis is independent of the archive and lockfile/receipt versions. The SDK and all three first-party adapters SHALL publish `0.1` before any CLI release requiring `0.1`; existing `0.0` CLIs keep selecting the highest compatible `0.0` release from npm. +- **Everything else ships but does not materialize.** Non-asset files outside skill directories — root-level files like `README.md`, or extras under `agents/` and `commands/` — SHALL NOT be written to disk at install time. They travel with the archive as facet metadata for future surfaces (e.g. a `facet info` command, registry listings). This change makes README shippable and verifiable, not displayed. Supplementary files SHALL NOT become independently addressable assets: no asset type, adapter metadata, install scope, or lockfile asset tuples. +- **README receives first-class authoring support.** Create SHALL offer editable `README.md` content enabled by default. Edit SHALL expose dedicated create/edit/adopt/scaffold/remove actions for both exact conventional paths, `README.md` and extensionless `README`. README remains optional and uses the same top-level supplementary-file declaration mechanism as every other archive-only file. +- **BREAKING (protocol/archive format).** All builds produced after this change SHALL use `facetVersion: 0.2`; consumers SHALL continue accepting legacy `0.1` archives during a compatibility window. Unsupported versions MUST produce structured failures that the CLI renders as actionable upgrade guidance. Protocol and adapter packages SHALL use their next minor releases while pre-1.0; removing `0.1` support is a separate future change. The archive `facetVersion` (`0.1`→`0.2`), the lockfile/receipt version (`0.2`), and the adapter API identifier (`0.0`→`0.1`) are three independent axes; a consumer classifies each separately and never infers one from another. + +## Non-goals + +- No `facet info` command or README rendering in the CLI — shipping README enables that future capability; this change does not build it. +- No independent installation of supplementary files: files outside skill directories gain no install destinations of their own, and skill companions install only as atomic parts of their owning skill's bundle, never with an independent lifecycle. +- No companion-directory install semantics for commands or agents — only skills materialize companion files. +- No preservation of filesystem metadata (executable bits, symlinks) for supplementary files. +- No automatic packaging of untracked source-tree files; archive membership remains explicitly declared. + +## Capabilities + +### New Capabilities + +None — this change modifies existing domains rather than introducing a new one. + +### Modified Capabilities + +- `protocol`: pre-1.0 breaking protocol changes increment the package minor version; 1.0-and-later breaking changes require a major version. +- `protocol__schemas`: the facet manifest schema gains a declaration for non-asset files; current-format asset names follow the single-segment Agent Skills convention and skill/command names are disjoint; the build manifest represents and hashes every tracked entry; lockfile `0.2` requires canonical path/integrity records for every materialized file inside its owning asset entry. +- `protocol__content-hashing`: archive assembly collects declared non-asset files and skill-directory files at their source paths; per-entry hashes are recorded for all entries. +- `protocol__integrity`: the outer-exclusivity derivation set expands to declared non-asset files and skill-directory files; verification requirements apply to every inner-tar entry. +- `authoring__facets`: build resolves, validates, and archives supplementary files; create/edit provide dedicated transactional `README.md` and extensionless `README` authoring plus generic supplementary-file reconciliation; missing declarations, unsafe paths, collisions, slash-containing asset names, and skill/command name collisions are structured failures. +- `installation`: materialization requirements change — skill-directory files install with their skill; non-asset files elsewhere are shipped but never written to disk; install reconciles lockfile `0.2` per-file hashes before writes and reports exact drift paths; the machine-local receipt mirrors committed ownership for offline removal and rollback; the adapter-compatibility preflight (every selected adapter must declare a CLI-supported API before mutation) now rejects positional `0.0` adapters before archive dispatch or any write. +- `adapter__assets`: the adapter install/read/delete contract extends from one file per asset to multi-file skills, and its tagged request/result shapes are bound to adapter API `0.1`. +- `adapter__sdk`: the SDK's canonical adapter API identifier advances from `0.0` to `0.1` for the tagged contract; exact-identifier compatibility now classifies `0.0` as well-formed but unsupported by a `0.1`-only consumer. +- `adapter__management`: verification, listing, runtime loading, and npm resolution treat `0.1` as the supported adapter API and positional `0.0` as unsupported; a `0.1` CLI selects compatible `0.1` releases while existing `0.0` CLIs retain the highest compatible `0.0` release. + +## Impact + +**Code** + +- `packages/protocol`: `schemas/facet.ts` (manifest declaration), `loaders/facet.ts` (`resolvePromptsFromMap` currently ignores non-conventional paths), `build/content-hash.ts` (`collectArchiveEntries` collects only conventional asset paths), `integrity/validate-archive.ts` (Step 6b outer-exclusivity allowlist), `build/validate-content.ts`. +- `packages/engine`: `loaders/facet.ts` (disk reads limited to conventional paths), `build/pipeline.ts`, `install/materialize.ts` (one content string per asset; asset-tuple receipt). +- `packages/adapter`: `types.ts` (`installAsset`/`deleteAsset` single-file contract), `asset-fs.ts` (single-file write/delete with dir pruning), `api-version.ts` (`ADAPTER_API_VERSION` bumps `0.0`→`0.1`, the single source of truth every consumer derives from). +- `packages/adapters/claude-code`, `packages/adapters/opencode`, `packages/adapters/codex`: every first-party adapter migrates to the tagged multi-file contract, including skill-directory path resolution and ownership-set handling, and each declares package/runtime API `0.1` (prepack `facetAdapterApiVersion` injection). +- `packages/engine`: `adapters/api-compatibility.ts` (`SUPPORTED_ADAPTER_APIS` still derives from `ADAPTER_API_VERSION`, yielding `{0.1}`), plus verify/loader/inspection paths (positional `0.0` bundles fail closed as unsupported). +- `packages/cli`: `build`/`install` surfaces largely unchanged; error rendering for new validation failures; compatibility diagnostics report `0.1` as supported. +- Tests: compatibility coverage for legacy asset-only artifacts and the new archive format, plus security coverage for traversal, collisions, undeclared entries, and tampering with non-asset files. + +**Documentation (Article III)** + +This proposal was informed by `docs/specification/archive.mdx` (path safety, manifest completeness, outer exclusivity), `docs/specification/build.mdx` (steps 2 and 5), `docs/specification/manifest.mdx` (text-asset paths), and `docs/specification/integrity.mdx` (hashes, receipt tuples). All four SHALL be updated here, with the authoring and installation guides (`docs/guides/create-your-first-facet.mdx`, `docs/guides/install-facets.mdx`) and root `README.md`, which describe facets in asset-only terms today. `docs/specification/lockfile.mdx` SHALL be updated for lockfile `0.2`, per-file integrity, legacy-alpha-1 migration, and the canonical-hash/receipt-ownership distinction. `docs/specification/commit.mdx` SHALL be updated for receipt file-ownership records and transactional reconciliation. The adapter version-negotiation docs (`docs/guides/custom-adapters.mdx`, `docs/guides/install-facets.mdx`, and the adapter install/list/troubleshooting surfaces) SHALL be updated from `0.0` to `0.1` — the tagged contract, the CLI-supported-API values, and the reinstall guidance for old positional `0.0` adapters. + +**Systems** + +The cafe registry (separate implementation of the spec) will need the same relaxed verification rule before it can accept archives containing non-asset files. diff --git a/openspec/changes/support-non-asset-files/specs/adapter__assets/spec.md b/openspec/changes/support-non-asset-files/specs/adapter__assets/spec.md new file mode 100644 index 00000000..64be8406 --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/adapter__assets/spec.md @@ -0,0 +1,118 @@ +## MODIFIED Requirements + +### Requirement: Adapters provide asset installation + +An adapter SHALL accept an installation request whose content shape is determined by asset type. Every installation request SHALL identify its scope and asset name. A skill request SHALL carry the primary `SKILL.md` text, per-asset metadata, a canonical map from companion paths relative to the skill root to opaque bytes, and the caller-verified set of previously-owned companion paths; an empty companion map and an empty previously-owned set SHALL each be valid. Adapters SHALL NOT persist ownership metadata or infer ownership from disk contents; ownership data arrives in each request from the caller's lockfile and receipt records. Agent and command requests SHALL each carry one text content value and per-asset metadata and SHALL NOT carry companions or ownership sets. No installation request SHALL represent archive-only supplementary files. + +The adapter SHALL own path resolution, containment, directory creation, metadata assembly, storage format, and rollback. Before any filesystem access, every supplied companion path — new or previously owned — SHALL be validated as relative, canonical, and confined below the resolved skill root; a request containing a malformed or escaping path SHALL be rejected without reading, writing, or deleting anything. Skill installation SHALL replace the complete owned bundle atomically: the new primary and companions SHALL all commit, with previously-owned companion paths absent from the new request removed, or the prior bundle SHALL remain intact. Removal during replacement SHALL be limited to the supplied previously-owned set. Companion bytes SHALL be stored verbatim; metadata or front-matter transformation SHALL apply only to the primary file. Unowned files SHALL NOT be removed. Expected failures SHALL be returned as structured results. Atomicity SHALL cover handled failures within one operation; recovery from an interrupted operation is the caller's idempotent re-install, so installation SHALL remain idempotent and convergent. + +#### Scenario: Install a skill with companions + +- **WHEN** a skill request contains primary content and companions `references/api.md` and `assets/logo.png` +- **THEN** the adapter SHALL store the primary and both companions below that skill's storage location +- **AND** companion bytes SHALL be byte-identical to the request +- **AND** primary metadata SHALL NOT be inserted into companion files + +#### Scenario: Install a skill with no companions + +- **WHEN** a skill request contains an empty companion map +- **THEN** the adapter SHALL install the primary as a valid companion-less skill + +#### Scenario: Install an asset at user scope + +- **WHEN** installation requests user scope +- **THEN** the adapter SHALL use its user-level storage root + +#### Scenario: Reinstall replaces the owned skill bundle + +- **WHEN** an existing skill is reinstalled with one previously owned companion omitted from the new bundle but present in the request's previously-owned set +- **THEN** the adapter SHALL replace the primary and current companions +- **AND** remove the omitted owned companion +- **AND** preserve every file not named in the previously-owned set + +#### Scenario: Failed skill installation leaves no partial bundle + +- **WHEN** writing, deleting, or committing any part of a skill bundle fails +- **THEN** the adapter SHALL return structured failure data +- **AND** the complete prior bundle SHALL remain intact + +#### Scenario: Escaping companion path is rejected + +- **WHEN** a skill request contains an absolute companion path or a path that escapes the skill root — in the new bundle or in the previously-owned set +- **THEN** the adapter SHALL reject the request without reading, writing, or deleting any file + +#### Scenario: Agent and command contain one primary value + +- **WHEN** an agent or command is installed +- **THEN** its request SHALL contain exactly one primary text value and no companion map + +### Requirement: Adapters provide asset reading + +An adapter SHALL accept a type-specific read request at a given scope. Every read request SHALL identify its scope, asset type, and asset name. A skill read request SHALL additionally carry the caller-verified owned companion path set; reading a skill SHALL return canonical logical primary content, stored metadata, and the bytes of exactly the requested owned companion paths. The adapter SHALL NOT enumerate the skill directory to discover companions, so unowned files can never be swept into a read result. Every requested companion path SHALL be validated as relative, canonical, and confined below the skill root before any filesystem access. Reading an agent or command SHALL return canonical logical primary content and metadata without companions. Canonical primary content SHALL exclude adapter-specific storage encoding so callers can compare it with portable integrity records. + +#### Scenario: Read an existing multi-file skill + +- **WHEN** the system reads an installed skill supplying its two owned companion paths +- **THEN** the adapter SHALL return canonical primary content, metadata, and both requested companion byte values + +#### Scenario: Read returns only requested owned companions + +- **WHEN** a skill directory contains an unowned `notes.txt` absent from the request's owned path set +- **THEN** the read result SHALL NOT include `notes.txt` + +#### Scenario: Read transformed primary content canonically + +- **WHEN** an adapter stores front matter or other adapter-specific encoding around a primary asset +- **THEN** its read result SHALL return the canonical logical content without that encoding + +#### Scenario: Read a non-existent asset + +- **WHEN** the requested asset does not exist at that scope +- **THEN** the adapter SHALL return a structured not-found result + +### Requirement: Adapters provide asset deletion + +An adapter SHALL accept a type-specific deletion request at a given scope. Every deletion request SHALL identify its scope, asset type, and asset name. A skill deletion request SHALL additionally carry the caller-verified owned companion path set; deleting a skill SHALL remove its primary file and exactly the supplied owned companion paths as one atomic operation, SHALL preserve every other file, and SHALL prune only directories left empty by owned-file removal. Before any filesystem access, every supplied owned path SHALL be validated as relative, canonical, and confined below the resolved skill root; a request containing a malformed or escaping path SHALL be rejected without deleting anything. Deleting an agent or command SHALL remove its single primary asset. Expected deletion failures and missing assets SHALL be returned as structured results. + +#### Scenario: Delete an existing multi-file skill + +- **WHEN** deletion targets a skill and the request supplies its two owned companion paths +- **THEN** the adapter SHALL remove the primary and both supplied owned files as one operation + +#### Scenario: Escaping owned path is rejected before deletion + +- **WHEN** a deletion request's owned path set contains `../outside.md` or an absolute path +- **THEN** the adapter SHALL reject the request as structured failure data +- **AND** SHALL delete nothing + +#### Scenario: Skill deletion preserves unowned file + +- **WHEN** the skill directory contains `notes.txt` that is not recorded as owned +- **THEN** skill deletion SHALL leave `notes.txt` unchanged +- **AND** SHALL leave any directory needed to contain it + +#### Scenario: Failed deletion restores the prior bundle + +- **WHEN** deletion fails after one owned file has been staged for removal +- **THEN** the adapter SHALL return structured failure data +- **AND** the prior complete bundle SHALL remain available + +#### Scenario: Delete a non-existent asset + +- **WHEN** the requested asset does not exist at that scope +- **THEN** the adapter SHALL return a structured not-found result + +### Requirement: Asset methods are the only interface for asset storage + +The system SHALL perform every primary-asset and skill-companion storage operation through the selected adapter's install, read, and delete operations. It SHALL NOT directly inspect or modify adapter asset directories. The adapter SHALL own its tool's storage format, roots, path resolution, metadata conventions, and skill-bundle lifecycle. Archive-only supplementary files SHALL never be passed to an adapter. The tagged request/result shapes of these install, read, and delete operations constitute the current adapter method contract and SHALL be identified by adapter API `0.1`; a consumer SHALL NOT invoke them on an adapter declaring the superseded positional API `0.0`. + +#### Scenario: System delegates primary and companion installation + +- **WHEN** a skill primary and companions must be installed for an adapter +- **THEN** the system SHALL submit one skill installation request +- **AND** SHALL NOT write any requested file directly into the adapter tree + +#### Scenario: Archive-only supplementary file is withheld + +- **WHEN** a verified archive includes root `README.md` +- **THEN** the system SHALL NOT include that file in any adapter request diff --git a/openspec/changes/support-non-asset-files/specs/adapter__management/spec.md b/openspec/changes/support-non-asset-files/specs/adapter__management/spec.md new file mode 100644 index 00000000..5f1da817 --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/adapter__management/spec.md @@ -0,0 +1,121 @@ +## MODIFIED Requirements + +### Requirement: The system loads installed adapter bundles at runtime + +The system SHALL inspect installed adapter bundles before returning them for use. A compatible installation SHALL provide a verified adapter whose runtime API is supported by the current CLI. If any installed entry is incompatible or broken, loading SHALL fail with all collected failures instead of silently skipping entries. No adapter contract method SHALL be invoked for an entry before its compatibility has been established. A runtime declaration of the superseded positional adapter API `0.0` SHALL be treated as unsupported by a CLI whose supported set is the tagged-contract API `0.1`, and SHALL fail closed before any contract method is invoked. + +#### Scenario: Load compatible installed adapters for a build + +- **WHEN** the system runs a build command +- **AND** every installed adapter is valid and declares a supported API +- **THEN** the system SHALL load each verified adapter +- **AND** pass the loaded adapter objects to the build pipeline + +#### Scenario: Incompatible installed adapter blocks a build + +- **WHEN** the system runs a build command +- **AND** an installed adapter has a missing, malformed, or unsupported API declaration +- **THEN** the build SHALL fail with an actionable adapter compatibility diagnostic +- **AND** no adapter contract method SHALL be invoked + +#### Scenario: Positional 0.0 adapter is unsupported at load + +- **WHEN** an installed adapter declares runtime API `0.0` +- **AND** the CLI supports only the tagged-contract API `0.1` +- **THEN** loading SHALL fail with an actionable compatibility diagnostic +- **AND** no adapter contract method SHALL be invoked + +#### Scenario: No adapters installed during build + +- **WHEN** the system runs a build command +- **AND** no adapters are installed +- **THEN** the build SHALL proceed +- **AND** any adapter metadata in the manifest SHALL produce warnings for unknown adapters + +### Requirement: npm adapter installs select the highest compatible package version + +The system SHALL accept npm adapter package selectors in the exact `MAJOR.MINOR.PATCH`, major-wildcard `MAJOR.*`, minor-wildcard `MAJOR.MINOR.*`, bare wildcard `*`, and `latest` forms. A bare package name or first-party alias SHALL act as an implicit unconstrained selector. For a non-exact request, the system SHALL select the highest stable package version that satisfies the selector and declares an adapter API supported by the current CLI. For an exact request, the system SHALL consider only that package version and SHALL NOT silently substitute another release. The `latest` selector SHALL denote the same unconstrained candidate set as a bare package name or `*`; the system SHALL resolve it to the highest stable version that declares a supported adapter API and SHALL NOT consult the npm `latest` distribution tag during selection. + +The npm `latest` distribution tag SHALL continue to advance according to normal publishing policy. Compatibility selection SHALL NOT require moving, pinning, or withholding that tag. + +A CLI that supports only the tagged-contract API `0.1` SHALL select the highest stable release declaring `0.1` and SHALL skip releases declaring the superseded positional API `0.0`. A CLI that supports only `0.0` SHALL correspondingly skip `0.1` releases and select the highest compatible `0.0` release, so the two axes advance independently across the release window. + +#### Scenario: Bare package skips a newer incompatible release + +- **WHEN** a user installs an npm adapter by bare package name +- **AND** the newest package release declares an unsupported adapter API +- **AND** an older stable release declares an API supported by the CLI +- **THEN** the system SHALL install the highest stable release that declares a supported API + +#### Scenario: Tagged CLI selects the 0.1 release over a 0.0 release + +- **WHEN** a user installs an npm adapter by bare package name +- **AND** the package publishes both a `0.0` release and a newer `0.1` release +- **AND** the CLI supports only the tagged-contract API `0.1` +- **THEN** the system SHALL install the `0.1` release +- **AND** the system SHALL NOT select the `0.0` release + +#### Scenario: Positional CLI retains the highest compatible 0.0 release + +- **WHEN** a user installs an npm adapter by bare package name +- **AND** the package publishes both a `0.0` release and a newer `0.1` release +- **AND** the CLI supports only the positional API `0.0` +- **THEN** the system SHALL install the highest stable `0.0` release +- **AND** the system SHALL NOT select the `0.1` release + +#### Scenario: Wildcard constrains compatible selection + +- **WHEN** a user installs an npm adapter with a supported wildcard selector +- **THEN** the system SHALL select the highest stable package version that both satisfies the wildcard and declares a supported adapter API + +#### Scenario: Exact incompatible release is not substituted + +- **WHEN** a user requests an exact npm adapter package version whose declared adapter API is missing, malformed, or unsupported +- **THEN** installation SHALL fail for that exact release +- **AND** the system SHALL NOT install another package version instead + +#### Scenario: Unsupported range syntax is rejected + +- **WHEN** a user supplies a caret, tilde, comparator, OR, hyphen, prerelease, or `x`-style npm adapter selector +- **THEN** the system SHALL reject the selector +- **AND** the error SHALL identify the accepted exact, wildcard, and `latest` forms + +#### Scenario: No compatible release is available + +- **WHEN** no package version satisfying the user's selector declares an adapter API supported by the CLI +- **THEN** installation SHALL fail before downloading an adapter bundle +- **AND** the failure SHALL identify the package and requested selector +- **AND** the failure SHALL identify the CLI's supported APIs +- **AND** the failure SHALL identify the newest considered release and its missing, malformed, or unsupported declaration + +### Requirement: Compatibility failures provide actionable diagnostics + +When an adapter cannot be selected, verified, or loaded because its API declaration is missing, malformed, unsupported, or inconsistent with package metadata, the system SHALL return structured failure data. User-facing diagnostics SHALL identify the affected adapter or package, the found declaration when one exists, the adapter APIs supported by the CLI, and the best available compatible-install command. When the installation retains original source provenance, that command SHALL use the recorded source. When provenance is unavailable, the command SHALL use the best available identifier — a first-party alias, or otherwise the installed adapter name — and the diagnostic SHALL indicate that the original install source is unavailable. Compatibility failures SHALL NOT be reported as “no adapters installed” or as an unknown facet metadata schema. + +#### Scenario: Unsupported installed adapter reports recovery + +- **WHEN** an installed adapter declares an API not supported by the CLI +- **THEN** the command SHALL fail with a diagnostic identifying the adapter and its declared API +- **AND** the diagnostic SHALL list the APIs supported by the CLI +- **AND** the diagnostic SHALL provide the best available compatible-install command + +#### Scenario: Positional 0.0 adapter reports a reinstall command + +- **WHEN** an installed adapter declares the positional API `0.0` +- **AND** the CLI supports only the tagged-contract API `0.1` +- **THEN** the diagnostic SHALL identify the adapter and its declared `0.0` API +- **AND** the diagnostic SHALL list `0.1` among the APIs supported by the CLI +- **AND** the diagnostic SHALL provide the best available compatible-install command to reinstall a `0.1` adapter + +#### Scenario: Multiple installed failures are reported together + +- **WHEN** more than one installed adapter is incompatible or broken +- **THEN** loading SHALL fail with all collected adapter failures +- **AND** each compatibility failure SHALL retain its own repair information + +#### Scenario: Installation without provenance reports a best-available repair + +- **WHEN** an installed adapter without retained source provenance is found incompatible or broken +- **AND** its name does not match a first-party alias +- **THEN** the diagnostic SHALL provide a best-available compatible-install command derived from the installed adapter name +- **AND** the diagnostic SHALL indicate that the original install source is unavailable diff --git a/openspec/changes/support-non-asset-files/specs/adapter__sdk/spec.md b/openspec/changes/support-non-asset-files/specs/adapter__sdk/spec.md new file mode 100644 index 00000000..2694408c --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/adapter__sdk/spec.md @@ -0,0 +1,65 @@ +## MODIFIED Requirements + +### Requirement: Adapter authors can define an adapter using the SDK + +An adapter author SHALL be able to create an adapter by importing the SDK and calling a factory function with a definition object. The factory SHALL validate the definition shape and return an adapter object. The definition SHALL accept a name, a function to build per-asset adapter metadata (validating and enriching with defaults), and asset install/read/delete methods. + +The SDK SHALL expose `0.1` as the canonical adapter API identifier for the current tagged request/result method contract. Every adapter returned by the factory SHALL carry that identifier in a required, readonly `apiVersion` field. The factory definition SHALL NOT require or accept an author-supplied API identifier, so adapter authors cannot create a conflicting declaration and do not repeat the SDK-owned value. If a value is nonetheless supplied for `apiVersion`, such as through untyped input, the factory SHALL ignore it; the returned adapter SHALL always carry the SDK's canonical identifier and SHALL NOT reflect the author-supplied value. + +#### Scenario: Author creates a valid adapter + +- **WHEN** an author calls the factory function with a complete definition +- **THEN** the factory SHALL return a valid adapter object with all provided properties and methods +- **AND** the returned adapter SHALL declare the canonical adapter API `0.1` + +#### Scenario: Author provides an invalid definition + +- **WHEN** an author calls the factory function with a definition missing required fields +- **THEN** the factory SHALL throw an error describing which fields are missing + +#### Scenario: Author does not declare the API version + +- **WHEN** an author creates an adapter with the SDK factory +- **THEN** the definition SHALL NOT require the author to provide an API identifier +- **AND** the returned adapter SHALL carry the SDK's canonical API identifier + +#### Scenario: Consumer reads the canonical API identifier + +- **WHEN** an adapter publisher or compatibility-aware consumer imports the SDK's canonical adapter API identifier +- **THEN** the exported value SHALL be `0.1` + +### Requirement: Adapter API compatibility uses exact contract identifiers + +An adapter API identifier SHALL use the canonical `MAJOR.MINOR` decimal form without signs, suffixes, build metadata, or leading zeroes other than zero itself. Compatibility-aware consumers SHALL distinguish missing, malformed, unsupported, and supported identifiers. They SHALL determine compatibility by exact identifier equality and SHALL NOT infer compatibility from CLI versions, SDK package versions, adapter package versions, or semantic-version ordering. + +The tagged request/result method contract SHALL be identified by adapter API `0.1`. The earlier positional method contract SHALL remain identified by `0.0`; a consumer that supports only the tagged contract SHALL classify `0.0` as a well-formed but unsupported identifier and SHALL NOT treat its numeric proximity to `0.1` as compatibility. + +#### Scenario: Exact supported identifier is compatible + +- **WHEN** an adapter declares API `0.1` +- **AND** the consumer supports API `0.1` +- **THEN** the adapter API SHALL be classified as supported + +#### Scenario: Superseded positional identifier is unsupported + +- **WHEN** an adapter declares the positional-contract API `0.0` +- **AND** the consumer supports only the tagged-contract API `0.1` +- **THEN** the adapter API SHALL be classified as unsupported +- **AND** numeric proximity to `0.1` SHALL NOT make it compatible + +#### Scenario: Different well-formed identifier is unsupported + +- **WHEN** an adapter declares a well-formed API identifier that is not in the consumer's support set +- **THEN** the adapter API SHALL be classified as unsupported +- **AND** numeric proximity to a supported identifier SHALL NOT make it compatible + +#### Scenario: Invalid identifier is malformed + +- **WHEN** an adapter declares an identifier with a patch component, suffix, build metadata, sign, or disallowed leading zero +- **THEN** the adapter API SHALL be classified as malformed + +#### Scenario: API identifier is independent of package versions + +- **WHEN** the CLI, an adapter package, or the Adapter SDK package changes semantic version without changing the tagged adapter call contract +- **THEN** the adapter API identifier SHALL remain `0.1` +- **AND** the package-version change SHALL NOT imply a different adapter API compatibility result 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 new file mode 100644 index 00000000..4baff809 --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/authoring__facets/spec.md @@ -0,0 +1,717 @@ +## ADDED Requirements + +### Requirement: Facet manifests declare supplementary files explicitly + +Authors SHALL be able to declare supplementary files without representing them as independently installable assets. A top-level `files` array SHALL enumerate exact repository-relative paths for archive-only files and MUST NOT contain paths below `skills/`. Each skill descriptor MAY contain a `files` array of exact paths relative to that skill's directory; those paths MUST resolve below the skill directory and MUST NOT name `SKILL.md`. Omitted or empty arrays SHALL be valid. Glob and pattern declarations SHALL NOT be expanded or accepted. + +#### Scenario: Top-level supplementary files are declared + +- **WHEN** an author declares top-level `files` as `README.md` and `LICENSE` +- **THEN** the system SHALL accept both as archive-only supplementary declarations + +#### Scenario: Nested skill companions are declared + +- **WHEN** skill `review` declares `references/api.md`, `scripts/run.ts`, and `assets/logo.png` +- **THEN** the system SHALL accept all three paths as companions owned by that skill + +#### Scenario: Companion-less skill remains valid + +- **WHEN** a skill omits `files` or declares an empty array +- **THEN** the system SHALL accept the skill as having no companions + +#### Scenario: Top-level declaration cannot own a skill companion + +- **WHEN** an author places `skills/review/references/api.md` in top-level `files` +- **THEN** the system SHALL reject the declaration +- **AND** the error SHALL identify the owning skill's `files` array as the correct declaration site + +#### Scenario: Skill declaration cannot name its primary file + +- **WHEN** skill `review` declares `SKILL.md` in its `files` array +- **THEN** the system SHALL reject the declaration + +#### Scenario: Patterns are rejected + +- **WHEN** an author declares `docs/**` or `references/*.md` +- **THEN** the system SHALL reject the declaration as not being an exact path + +### Requirement: Build validates supplementary file declarations and path safety + +Before changing previous build output, the system SHALL validate that every declared supplementary path is non-empty, relative, canonical, and inside its permitted declaration region; resolves through existing parents to a regular file inside the facet root; and does not collide with any primary asset or other inner content-archive entry. Collision checking applies to the inner content archive only; the fixed outer wrapper entry names (`build-manifest.json`, `archive.tar.gz`) remain permitted as inner paths. Paths containing empty, `.` or `..` segments, backslashes, NUL bytes, or absolute, drive, or URL-like prefixes SHALL be rejected. Paths SHALL be portable: segments containing control bytes or `<`, `>`, `:`, `"`, `|`, `?`, `*`, segments equal to a Windows-reserved device name (case-insensitively, with or without an extension), and segments ending in a dot or space SHALL be rejected. Symbolic and hard links SHALL be rejected. The exact root path `facet.json` SHALL be reserved, while that basename MAY appear below another directory. Collisions SHALL include exact duplicates, Unicode-normalization aliases, portable case-fold aliases, resolved-source aliases, and file/directory prefix conflicts. Each failure SHALL be structured data identifying the path and declaration site. + +#### Scenario: Missing supplementary file is rejected + +- **WHEN** `files` declares `LICENSE` but no such file exists +- **THEN** the build SHALL fail with structured data identifying `LICENSE` + +#### Scenario: Traversal and absolute paths are rejected + +- **WHEN** a declaration contains `../secret`, `/secret`, `C:/secret`, or `https://example.com/file` +- **THEN** the build SHALL fail with structured data identifying the unsafe path + +#### Scenario: Backslash and empty segments are rejected + +- **WHEN** a declaration contains `docs\guide.md`, `docs//guide.md`, or `docs/./guide.md` +- **THEN** the build SHALL reject the non-canonical path + +#### Scenario: Link source is rejected + +- **WHEN** a declared path resolves through a symbolic or hard link +- **THEN** the build SHALL fail before writing output + +#### Scenario: Root manifest path is reserved + +- **WHEN** top-level `files` declares the exact path `facet.json` +- **THEN** the build SHALL reject the declaration + +#### Scenario: Manifest basename below a companion directory is permitted + +- **WHEN** skill `review` declares `examples/facet.json` and that regular file exists +- **THEN** the build SHALL accept the declaration + +#### Scenario: Primary asset path collision is rejected + +- **WHEN** an agent named `reviewer` is declared and top-level `files` also declares `agents/reviewer.md` +- **THEN** the build SHALL fail with structured collision data identifying both declarations + +#### Scenario: Inner path may match an outer archive filename + +- **WHEN** top-level `files` declares a regular source file named `build-manifest.json` or `archive.tar.gz` +- **THEN** the build SHALL accept that inner-archive path + +#### Scenario: Portable aliases are rejected + +- **WHEN** declarations include paths that differ only by case or Unicode normalization +- **THEN** the build SHALL fail with structured collision data + +#### Scenario: Windows-reserved declaration is rejected + +- **WHEN** a declaration contains `references/con`, `aux.txt`, `notes:draft.md`, or a segment ending in a dot or space +- **THEN** the build SHALL fail with structured data identifying the non-portable segment + +#### Scenario: File and directory prefix conflict is rejected + +- **WHEN** declarations include both `docs` as a file and `docs/guide.md` +- **THEN** the build SHALL fail with structured collision data + +#### Scenario: Validation failure preserves previous build output + +- **WHEN** supplementary-file validation fails and `dist/` contains a previous successful build +- **THEN** the previous output SHALL remain unchanged + +### Requirement: Build ships supplementary files as opaque bytes + +The system SHALL read, hash, and archive supplementary files byte-for-byte. Supplementary content SHALL NOT undergo front-matter parsing, line-ending normalization, empty-content validation, or text decoding. Empty and binary supplementary files SHALL be permitted, and every supplementary entry SHALL receive its own content hash. + +#### Scenario: Binary companion is preserved + +- **WHEN** a skill declares binary file `assets/logo.png` +- **THEN** the archived entry SHALL be byte-identical to the source file + +#### Scenario: Empty supplementary file builds successfully + +- **WHEN** a declared supplementary file contains zero bytes +- **THEN** the build SHALL succeed and record that entry's hash + +#### Scenario: Front-matter-like supplementary content is preserved + +- **WHEN** a declared supplementary file begins with valid YAML front matter +- **THEN** the build SHALL archive those bytes unchanged + +### Requirement: Edit provides dedicated README authoring + +`README.md` SHALL be the preferred conventional facet document, and the exact extensionless root path `README` SHALL also receive first-class support. Both SHALL remain ordinary top-level `files` declarations rather than using a README-specific manifest field. The edit workflow SHALL display them in a dedicated facet-level README panel and SHALL NOT duplicate them in generic file reconciliation. + +For each exact path, the panel SHALL offer Edit or Remove when present and declared; Adopt or Edit-and-Adopt when present and undeclared; Scaffold at the same path or Remove Declaration when declared and missing; and Create when absent and undeclared, defaulting to `README.md`. If both paths exist, the system SHALL display and manage both independently. Adopt SHALL preserve existing bytes unless the author explicitly edits them. Remove SHALL queue file deletion and declaration removal together; Scaffold and Create SHALL queue file creation and declaration addition together. No operation SHALL change disk or manifest state before Apply. + +#### Scenario: Present declared README can be edited or removed + +- **WHEN** `README.md` exists and appears in top-level `files` +- **THEN** the README panel SHALL offer Edit and Remove +- **AND** Remove SHALL queue both file deletion and declaration removal + +#### Scenario: Present undeclared README is adopted without byte changes + +- **WHEN** `README.md` exists but is not declared and the author chooses Adopt +- **THEN** Apply SHALL add `README.md` to top-level `files` +- **AND** the existing file bytes SHALL remain unchanged + +#### Scenario: Missing extensionless README keeps its path + +- **WHEN** top-level `files` declares `README` but the file is missing +- **THEN** the panel SHALL offer Scaffold at `README` or Remove Declaration + +#### Scenario: Create defaults to README dot md + +- **WHEN** neither conventional README path exists or is declared +- **THEN** Create SHALL default to `README.md` + +#### Scenario: Both conventional README paths are independent + +- **WHEN** both `README.md` and `README` exist +- **THEN** the panel SHALL display both independently +- **AND** neither SHALL be ignored or overwritten implicitly + +#### Scenario: README operation appears in confirmation + +- **WHEN** an author queues a README operation +- **THEN** the confirmation SHALL identify the exact path and operation +- **AND** exiting before Apply SHALL leave the file and manifest unchanged + +## MODIFIED Requirements + +### Requirement: Valid facet manifests are accepted + +The system SHALL accept a facet manifest that conforms to the manifest schema. A valid manifest has a name, a version, and at least one text asset or composed facet. The name SHALL be either an unscoped kebab-case facet identity or a scoped `@scope/name` identity. A manifest MAY include an optional top-level `private` boolean and supplementary-file declarations. Skills, agents, and commands SHALL use descriptors with required descriptions and optional platform metadata; prompt content SHALL be inferred from conventional paths rather than descriptor references. + +Current-format skill, agent, and command names SHALL be single segments of 1–64 lowercase ASCII letters, digits, or hyphens, with no leading, trailing, or consecutive hyphens. Skills SHALL use `skills//SKILL.md`, agents `agents/.md`, and commands `commands/.md`. Skills and commands SHALL use disjoint names; agents MAY share a name with either. + +#### Scenario: Minimal valid manifest with a skill + +- **WHEN** an author provides a name, version, and one valid skill descriptor with a description +- **THEN** the system SHALL accept the manifest + +#### Scenario: Valid manifest with a scoped facet identity + +- **WHEN** an author provides name `@julian/cowsay`, a version, and one valid skill descriptor +- **THEN** the system SHALL accept the manifest + +#### Scenario: Manifest with all sections + +- **WHEN** an author provides identity fields, skill, agent, and command descriptors, composed facets, server references, and supplementary declarations +- **THEN** the system SHALL accept the manifest + +#### Scenario: Manifest with only composed facets is valid + +- **WHEN** an author provides `name`, `version`, and `facets` but no local skills, agents, or commands +- **THEN** the system SHALL accept the manifest + +#### Scenario: Manifest with private publish intent is valid + +- **WHEN** an author provides `private: true`, valid identity fields, and at least one text asset +- **THEN** the system SHALL accept and preserve `private: true` + +#### Scenario: Manifest with explicit public publish intent is valid + +- **WHEN** an author provides `private: false`, valid identity fields, and at least one text asset +- **THEN** the system SHALL accept and preserve `private: false` + +#### Scenario: Manifest with omitted privacy remains public by default + +- **WHEN** an author omits `private` from an otherwise valid manifest +- **THEN** the system SHALL accept the manifest +- **AND** loaded data SHALL NOT synthesize `private` + +#### Scenario: Valid current-format asset name is accepted + +- **WHEN** an author declares assets named `a`, `code-review`, and `review2` +- **THEN** the system SHALL accept those names + +### Requirement: Authors can scaffold a new facet project interactively + +The system SHALL provide an interactive wizard that collects a valid facet identity, non-empty description, optional SemVer version defaulting to `0.0.0`, and privacy intent defaulting to public. The author SHALL be able to add, rename, and remove multiple skills, commands, and agents. Asset names SHALL be validated in real time using the current single-segment grammar. Names SHALL be unique within each type; skills and commands SHALL additionally be unique across their shared namespace, while agents MAY share names with skills or commands. The first asset of each type SHOULD suggest the unscoped facet-name segment. At least one asset SHALL be required. + +The wizard SHALL include a dedicated README step separate from asset management. README creation SHALL be enabled by default but optional. The wizard SHALL seed editable `README.md` content from the facet name and description, permit editing before confirmation, and permit disabling creation. Changing identity fields after README editing SHALL NOT silently regenerate or overwrite the edited content. The confirmation SHALL show privacy, declared assets, and every file to be created, including `README.md` when enabled. + +All fields SHALL remain editable. Exit confirmation SHALL prevent accidental loss. Upon Apply, the system SHALL atomically create the project manifest and starter files. Skill files SHALL use `skills//SKILL.md`; agent and command files SHALL use their flat conventional paths. Starter asset files SHALL contain no YAML front matter. Every starter asset file SHALL contain template content that guides the author about what belongs in each section. When README is enabled, Apply SHALL write exact `README.md` content and declare that path in top-level `files`. Private intent SHALL write `private: true`; public intent SHALL omit `private`. The resulting project SHALL be immediately buildable. + +#### Scenario: Author scaffolds a scoped project with a named skill + +- **WHEN** the author provides `@julian/cowsay`, description `Cowsay tools`, and skill `cowsay` +- **THEN** the system SHALL create a manifest named `@julian/cowsay` +- **AND** create `skills/cowsay/SKILL.md` + +#### Scenario: Author scaffolds a project with multiple named skills + +- **WHEN** the author provides `viper-plans` and skills `viper-planning` and `viper-execution-rules` +- **THEN** the manifest SHALL contain both descriptors +- **AND** both conventional skill files SHALL be created + +#### Scenario: Author accepts the default skill name + +- **WHEN** the author names the facet `code-review` and accepts the first skill-name suggestion +- **THEN** the skill SHALL be named `code-review` +- **AND** its file SHALL be `skills/code-review/SKILL.md` + +#### Scenario: Author cannot complete without a description + +- **WHEN** the author attempts completion without a description +- **THEN** completion SHALL be blocked with a description-required message + +#### Scenario: Scoped facet identity is accepted + +- **WHEN** the author enters `@acme/deploy-tools` +- **THEN** the system SHALL accept the facet identity + +#### Scenario: Invalid facet identity is rejected + +- **WHEN** the author enters `@acme/Deploy_Tools` +- **THEN** the system SHALL reject the identity and explain the constraint + +#### Scenario: Asset names use the current grammar + +- **WHEN** the author enters an asset name with uppercase letters, spaces, underscores, slashes, leading or trailing hyphens, consecutive hyphens, or more than 64 characters +- **THEN** the wizard SHALL reject the name and explain the constraint + +#### Scenario: Duplicate asset names within a type are rejected + +- **WHEN** the author adds a skill with the same name as an existing skill +- **THEN** the wizard SHALL reject the duplicate + +#### Scenario: Skill and command cannot share a name + +- **WHEN** the author adds skill `review` and then command `review` +- **THEN** the wizard SHALL reject the command name as a shared-namespace collision + +#### Scenario: Agent may share a name with a skill + +- **WHEN** the author adds skill `review` and agent `review` +- **THEN** the wizard SHALL accept both + +#### Scenario: Author edits an existing asset name + +- **WHEN** the author changes an asset to a valid name free of applicable collisions +- **THEN** the wizard SHALL update the asset name + +#### Scenario: Author removes an asset + +- **WHEN** the author removes a previously added asset +- **THEN** the asset SHALL no longer appear in the wizard or confirmation + +#### Scenario: Author exits with unsaved work + +- **WHEN** the author confirms exit before Apply +- **THEN** no files or directories SHALL be created + +#### Scenario: Version accepts valid SemVer + +- **WHEN** the author enters `1.0.0` or `100.2.1` +- **THEN** the wizard SHALL accept the version + +#### Scenario: Version rejects invalid input + +- **WHEN** the author enters a value outside the `N.N.N` pattern +- **THEN** the wizard SHALL identify the version as invalid + +#### Scenario: Version defaults to zero + +- **WHEN** the author does not change the version +- **THEN** the manifest SHALL contain `0.0.0` + +#### Scenario: New facet defaults to public intent + +- **WHEN** the author accepts the default privacy choice +- **THEN** the manifest SHALL omit `private` +- **AND** confirmation SHALL show public intent + +#### Scenario: New private facet writes private true + +- **WHEN** the author selects private intent +- **THEN** the manifest SHALL contain `private: true` +- **AND** confirmation SHALL show private intent + +#### Scenario: Author reverts private choice before completion + +- **WHEN** the author selects private and then returns to public before Apply +- **THEN** the manifest SHALL omit `private` + +#### Scenario: Target directory already contains a manifest + +- **WHEN** a manifest exists in the target directory +- **THEN** the wizard SHALL warn and require confirmation before overwriting + +#### Scenario: README is created by default + +- **WHEN** the author accepts the default README choice +- **THEN** confirmation SHALL list `README.md` +- **AND** Apply SHALL write `README.md` and add it to top-level `files` + +#### Scenario: README may be disabled + +- **WHEN** the author disables README creation +- **THEN** no README file or declaration SHALL be created + +#### Scenario: Edited README content is preserved + +- **WHEN** the author edits seeded README content and later changes the facet name +- **THEN** Apply SHALL write the author's edited content without regenerating it + +### 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. + +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. + +#### Scenario: Successful build of a valid facet + +- **WHEN** a valid facet has all primary and supplementary source files +- **THEN** the system SHALL write the `.facet` archive and build manifest to `dist/` +- **AND** the archive SHALL contain the manifest and every declared primary and supplementary entry +- **AND** the build manifest SHALL record the integrity and a hash for every entry +- **AND** the display SHALL show the format version, complete entry listing, and integrity + +#### Scenario: Successful build of a scoped facet identity + +- **WHEN** the author builds facet `@julian/cowsay` +- **THEN** the archive SHALL be written below `dist/` without failing on the slash +- **AND** embedded `facet.json` SHALL preserve the scoped name + +#### Scenario: Build output creates nested parent directories + +- **WHEN** an archive filename renders as a nested path below `dist/` +- **THEN** the system SHALL create required parent directories before writing + +#### Scenario: Build includes supplementary files at canonical paths + +- **WHEN** a facet declares top-level `README.md` and skill companion `references/api.md` +- **THEN** the archive SHALL contain `README.md` and `skills//references/api.md` + +#### Scenario: Build fails on invalid manifest + +- **WHEN** the manifest fails schema validation +- **THEN** the system SHALL report errors with field paths and write no new output + +#### Scenario: Build fails on missing primary asset + +- **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 + +- **WHEN** a primary asset file contains YAML front matter +- **THEN** the system SHALL identify that file and require front-matter removal + +#### Scenario: Build fails on empty primary asset + +- **WHEN** a primary asset file is empty or whitespace-only +- **THEN** the system SHALL identify the file and require content + +#### Scenario: Empty supplementary file remains valid + +- **WHEN** a declared supplementary file is empty +- **THEN** the build SHALL NOT fail merely because that file is empty + +#### Scenario: Build with no manifest + +- **WHEN** the build runs where no facet manifest exists +- **THEN** the system SHALL report that no manifest was found + +#### Scenario: Valid build cleans previous output + +- **WHEN** every source input validates and `dist/` contains a previous build +- **THEN** the system SHALL remove previous output before writing new output + +#### Scenario: Invalid build preserves previous output + +- **WHEN** any source input fails validation and `dist/` contains a previous build +- **THEN** the previous output SHALL remain unchanged + +### Requirement: Build detects naming collisions between local assets + +The system SHALL reject duplicate names within each asset type. It SHALL also reject any name shared by a skill and command because those types occupy one logical namespace. Agents SHALL remain separate and MAY share a name with a skill or command. Collision failures SHALL identify every conflicting declaration. + +#### Scenario: Two skills share a name + +- **WHEN** a facet declares two skills with the same name +- **THEN** the build SHALL fail and identify both skill declarations + +#### Scenario: Skill and command share a name + +- **WHEN** a facet declares skill `review` and command `review` +- **THEN** the build SHALL fail with structured collision data identifying both declarations + +#### Scenario: Skill and agent share a name + +- **WHEN** a facet declares skill `review` and agent `review` +- **THEN** the build SHALL succeed without a naming collision + +#### Scenario: Command and agent share a name + +- **WHEN** a facet declares command `review` and agent `review` +- **THEN** the build SHALL succeed without a naming collision + +#### Scenario: Distinct applicable names do not collide + +- **WHEN** all names are distinct within their applicable namespaces +- **THEN** the build SHALL succeed without naming-collision errors + +### Requirement: Authors can edit a facet project interactively + +The system SHALL provide an interactive authoring workbench for identity, privacy, assets, README, and supplementary-file reconciliation. It SHALL scan conventional asset paths and declared supplementary regions, present detected drift before editing, and reject an invalid source manifest with actionable errors. Asset names SHALL use the current single-segment grammar; skills and commands SHALL be unique across their shared namespace while agents remain separate. + +The workbench SHALL display current privacy intent. Setting or retaining private intent SHALL write `private: true`; changing private to public SHALL omit `private`; leaving a public facet unchanged SHALL preserve whether public intent was omitted or explicitly `false`. + +#### Scenario: Author edits facet identity fields + +- **WHEN** the author opens a facet for editing +- **THEN** current name, description, and version SHALL be editable +- **AND** absent version SHALL default to `0.0.0` + +#### Scenario: Author inspects private manifest as private + +- **WHEN** source contains `private: true` +- **THEN** edit SHALL show private intent and permit switching to public + +#### Scenario: Author inspects omitted privacy as public + +- **WHEN** source omits `private` +- **THEN** edit SHALL show public intent + +#### Scenario: Author inspects explicit false as public + +- **WHEN** source contains `private: false` +- **THEN** edit SHALL show public intent + +#### Scenario: Omitted public privacy remains omitted + +- **WHEN** public intent was omitted and remains public +- **THEN** Apply SHALL continue omitting `private` + +#### Scenario: Explicit public false is preserved + +- **WHEN** source contains `private: false` and remains public +- **THEN** Apply SHALL preserve `private: false` + +#### Scenario: Private facet changes to public omission + +- **WHEN** the author changes private intent to public +- **THEN** Apply SHALL omit `private` + +#### Scenario: Public facet changes to private + +- **WHEN** the author changes public intent to private +- **THEN** Apply SHALL write `private: true` + +#### Scenario: Author changes facet scope + +- **WHEN** the author changes `@julian/cowsay` to `@acme/cowsay` +- **THEN** the system SHALL treat it as a normal local identity edit + +#### Scenario: Author creates a new skill + +- **WHEN** the author adds skill `code-review` +- **THEN** Apply SHALL create `skills/code-review/SKILL.md` and its descriptor + +#### Scenario: Author creates a new agent + +- **WHEN** the author adds agent `reviewer` +- **THEN** Apply SHALL create `agents/reviewer.md` and its descriptor + +#### Scenario: Author creates a new command + +- **WHEN** the author adds command `run-review` +- **THEN** Apply SHALL create `commands/run-review.md` and its descriptor + +#### Scenario: Author deletes an asset + +- **WHEN** the author queues an asset deletion +- **THEN** Apply SHALL remove its descriptor and its conventional primary file +- **AND** for a skill, Apply SHALL additionally remove only that skill's declared companion files +- **AND** undeclared files inside the skill directory SHALL remain on disk unchanged + +#### Scenario: Asset names are validated during edit + +- **WHEN** the author enters an invalid asset name or a skill/command shared-namespace collision +- **THEN** the workbench SHALL reject the name with an actionable error + +### Requirement: Edit detects new files on disk and offers to add them + +The system SHALL detect undeclared conventional assets, undeclared files inside declared skill directories, and common root-level supplementary files. It SHALL present all generic discoveries together in one reconciliation phase with per-item actions, and every discovery SHALL be resolved before asset or manifest editing begins. Asset additions SHALL require descriptions; supplementary-file adoptions SHALL not. Skipped files SHALL remain on disk and undeclared. `README.md` and `README` SHALL be excluded from generic reconciliation and shown only in the dedicated README panel. + +#### Scenario: New skill directory is discovered + +- **WHEN** `skills/code-review/SKILL.md` exists but is undeclared +- **THEN** edit SHALL offer skill `code-review` for addition or skip + +#### Scenario: New agent file is discovered + +- **WHEN** `agents/reviewer.md` exists but is undeclared +- **THEN** edit SHALL offer agent `reviewer` for addition or skip + +#### Scenario: New command file is discovered + +- **WHEN** `commands/start-review.md` exists but is undeclared +- **THEN** edit SHALL offer command `start-review` for addition or skip + +#### Scenario: Multiple new files are discovered + +- **WHEN** three generic discoveries exist +- **THEN** edit SHALL present all three and permit any combination of actions + +#### Scenario: Asset addition requires a description + +- **WHEN** the author chooses to add a discovered asset +- **THEN** edit SHALL require a description before accepting it + +#### Scenario: Unselected file remains on disk + +- **WHEN** the author skips a discovery +- **THEN** its bytes SHALL remain unchanged and no declaration SHALL be added + +#### Scenario: Empty discovered primary file is selectable + +- **WHEN** a discovered conventional asset file is empty +- **THEN** edit SHALL still allow the author to select and repair it + +#### Scenario: Undeclared skill companion is discovered + +- **WHEN** declared skill `review` contains undeclared `references/api.md` +- **THEN** edit SHALL offer to add that relative path to the skill's `files` + +#### Scenario: Common root file is discovered + +- **WHEN** undeclared root `LICENSE` exists +- **THEN** generic reconciliation SHALL offer top-level adoption + +#### Scenario: README is not duplicated in generic reconciliation + +- **WHEN** undeclared root `README.md` exists +- **THEN** it SHALL appear only in the README panel + +### Requirement: Edit detects missing files and offers scaffold-or-remove + +The system SHALL detect missing conventional asset files and missing declared supplementary files. For each missing primary asset, it SHALL offer to remove the asset declaration or scaffold its starter file. For each missing supplementary file other than `README.md` or `README`, it SHALL offer to remove the exact declaration or scaffold a replacement at the exact declared path. Conventional README paths SHALL use the dedicated README panel. + +#### Scenario: Missing skill primary is removed + +- **WHEN** skill `code-review` is declared without `skills/code-review/SKILL.md` and the author chooses Remove +- **THEN** Apply SHALL remove the skill descriptor + +#### Scenario: Missing skill primary is scaffolded + +- **WHEN** skill `code-review` is declared without its primary and the author chooses Scaffold +- **THEN** Apply SHALL create `skills/code-review/SKILL.md` and preserve the descriptor + +#### Scenario: Missing agent primary is removed + +- **WHEN** agent `reviewer` is declared without `agents/reviewer.md` and the author chooses Remove +- **THEN** Apply SHALL remove the agent descriptor + +#### Scenario: Missing skill companion is reconciled + +- **WHEN** skill `review` declares missing `references/api.md` +- **THEN** edit SHALL offer Scaffold at that exact path or Remove Declaration + +#### Scenario: Missing root supplementary file is reconciled + +- **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 + +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. + +#### Scenario: Front matter name pre-fills asset name + +- **WHEN** `skills/skill/SKILL.md` contains `name: typescript-best-practices` +- **THEN** edit SHALL pre-fill that name and require confirmation + +#### Scenario: Missing front matter name uses conventional name + +- **WHEN** `skills/code-review/SKILL.md` has no front-matter name +- **THEN** edit SHALL pre-fill `code-review` + +#### Scenario: Front matter description pre-fills description + +- **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 + +#### Scenario: Supplementary front matter-like bytes are preserved + +- **WHEN** an adopted supplementary file begins with valid YAML front matter +- **THEN** Apply SHALL preserve those bytes unless the author explicitly edits them + +### Requirement: Edit is transactional with confirmation + +All identity, privacy, asset, README, supplementary-file, and manifest changes SHALL remain queued until the author selects Apply. Confirmation SHALL show identity, privacy, asset summaries containing each asset's name and truncated description, and every file/declaration operation with its exact path. Apply SHALL commit all queued changes atomically. Exiting before Apply SHALL leave every file and the manifest unchanged. + +#### Scenario: Author confirms changes + +- **WHEN** the author selects Apply from confirmation +- **THEN** all queued disk and manifest changes SHALL commit atomically + +#### Scenario: Author exits before confirmation + +- **WHEN** the author exits before Apply +- **THEN** no file SHALL be created, modified, or deleted +- **AND** the manifest SHALL remain unchanged + +#### Scenario: Confirmation shows privacy intent + +- **WHEN** confirmation is displayed +- **THEN** it SHALL show privacy alongside identity fields + +#### Scenario: Confirmation shows all deltas + +- **WHEN** a session includes identity, privacy, asset, README, companion, and front-matter changes +- **THEN** confirmation SHALL list each change and exact affected path + +#### Scenario: README and companion changes wait for Apply + +- **WHEN** the author queues `README.md` creation and companion adoption +- **THEN** confirmation SHALL list both exact paths +- **AND** neither disk nor manifest SHALL change before Apply + +### Requirement: Content files contain no front matter + +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. + +#### 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 + +- **WHEN** a primary asset file contains YAML front matter +- **THEN** the build SHALL fail and identify the file + +#### Scenario: Archive contains clean primary files + +- **WHEN** a facet is built +- **THEN** every primary asset entry SHALL contain markdown without YAML front matter + +#### Scenario: Supplementary front matter is allowed + +- **WHEN** a supplementary file contains front-matter-like text +- **THEN** the build SHALL preserve it byte-for-byte and SHALL NOT reject it for that content diff --git a/openspec/changes/support-non-asset-files/specs/installation/spec.md b/openspec/changes/support-non-asset-files/specs/installation/spec.md new file mode 100644 index 00000000..0a5c506b --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/installation/spec.md @@ -0,0 +1,342 @@ +## ADDED Requirements + +### Requirement: Archive-only supplementary files are never materialized + +Supplementary files outside skill directories SHALL remain available inside the verified facet archive but SHALL NOT be written into any selected adapter. They SHALL NOT receive an asset type, scope, independent lockfile asset entry, or per-file lockfile record. Their bytes SHALL remain protected by facet-level archive integrity. + +#### Scenario: Root README is not installed + +- **WHEN** a verified facet contains declared root `README.md` +- **THEN** installation SHALL NOT write that file into any adapter tree +- **AND** the lockfile SHALL NOT record `README.md` under an asset + +#### Scenario: Extra agent-adjacent file is not installed + +- **WHEN** a verified facet contains declared archive-only `agents/notes.md` +- **THEN** installation SHALL materialize no file for that supplementary entry + +#### Scenario: Archive-only tampering still blocks installation + +- **WHEN** an archive-only supplementary entry has been altered after build +- **THEN** facet-level integrity verification SHALL fail before any materialized file is written + +### Requirement: Drift is detected and reported per locked file + +The system SHALL compare every materialized file with its canonical per-file integrity record. Verbatim companion files SHALL be hashed from their installed bytes. When an adapter stores a primary asset in a transformed representation, its read result SHALL provide canonical logical content for comparison. Drift reports SHALL identify the exact locked path rather than only the owning facet or asset. + +#### Scenario: Companion drift identifies exact path + +- **WHEN** installed `skills/review/references/api.md` differs from its locked canonical bytes +- **THEN** the system SHALL report that exact path as drifted + +#### Scenario: Transformed primary compares canonically + +- **WHEN** an adapter stores metadata around a primary asset but returns its canonical logical content +- **THEN** drift comparison SHALL use the canonical content integrity rather than adapter-specific storage bytes + +#### Scenario: Reinstall repairs one drifted file + +- **WHEN** one companion in an otherwise unchanged skill bundle has drifted +- **THEN** reinstall SHALL restore that file from verified content +- **AND** the user-visible result SHALL identify its exact path + +### Requirement: Unsupported archive versions fail with actionable guidance + +Installation SHALL accept current `0.2` and valid legacy `0.1` archives during the compatibility window. Any other archive version SHALL return structured failure data containing the observed and supported versions before materialization. A malformed current archive SHALL NOT be reinterpreted as legacy. For a known newer archive format, the user-facing failure SHALL name the minimum release that supports it. For an unknown future format, the failure SHALL advise updating to the latest release without inventing a minimum version. + +#### Scenario: Known newer format names its minimum supporting release + +- **WHEN** an archive uses a format unsupported by this release but mapped to a known newer release +- **THEN** installation SHALL fail before materialization +- **AND** the message SHALL name the minimum supporting release +- **AND** project, lockfile, receipt, and adapter state SHALL remain unchanged + +#### Scenario: Unknown future format advises updating to latest + +- **WHEN** an archive uses a format for which no supporting release is known +- **THEN** installation SHALL fail before materialization +- **AND** the message SHALL advise updating to the latest release without naming a minimum +- **AND** project, lockfile, receipt, and adapter state SHALL remain unchanged + +#### Scenario: Valid legacy archive remains installable + +- **WHEN** a valid `0.1` archive is installed during the compatibility window +- **THEN** the system SHALL apply the legacy verification and materialization behavior + +#### Scenario: Malformed current archive is not retried as legacy + +- **WHEN** a `0.2` archive violates the current schema +- **THEN** installation SHALL fail under the current rules +- **AND** the system SHALL NOT retry it as `0.1` + +## MODIFIED Requirements + +### Requirement: Facet operations require compatible selected adapters before mutation + +Before adding, removing, or installing facets, the system SHALL verify that every selected installed adapter declares an API supported by the current CLI. If a selected adapter is missing its declaration, has a malformed or unsupported declaration, conflicts with its recorded package declaration, or cannot be loaded as a valid adapter, the operation SHALL fail before invoking any adapter contract method or writing project or materialized state. The failure SHALL identify every incompatible selected adapter and provide the best available compatible-install command. The system SHALL NOT automatically upgrade or replace an incompatible adapter during a facet operation. + +This adapter-compatibility preflight SHALL run before archive-version dispatch and before any per-file integrity reconciliation, so an adapter declaring the superseded positional API `0.0` SHALL cause a `0.1`-only CLI to fail on the adapter — with reinstall guidance — before the archive's `facetVersion` is examined. The adapter API axis and the archive-format axis SHALL be classified independently. + +Facet removal SHALL remain independent of cached facet content and network access, but it SHALL still require compatible selected adapters because deleting materialized assets invokes each selected adapter's contract. + +#### Scenario: Adding a facet with an incompatible selected adapter changes nothing + +- **WHEN** a user adds a facet +- **AND** a selected installed adapter does not declare an API supported by the CLI +- **THEN** the operation SHALL fail before any facet is materialized +- **AND** no adapter contract method SHALL be invoked +- **AND** the project manifest, lockfile, install receipt, and materialized assets SHALL remain unchanged +- **AND** the error SHALL direct the user to install a compatible adapter + +#### Scenario: Positional 0.0 adapter blocks a facet operation before archive dispatch + +- **WHEN** a user adds or installs a facet whose archive uses `facetVersion: 0.2` +- **AND** a selected installed adapter declares the positional API `0.0` +- **AND** the CLI supports only the tagged-contract API `0.1` +- **THEN** the operation SHALL fail on the incompatible adapter before the archive version is dispatched +- **AND** no adapter contract method SHALL be invoked +- **AND** the project manifest, lockfile, install receipt, and materialized assets SHALL remain unchanged +- **AND** the error SHALL direct the user to reinstall a compatible adapter + +#### Scenario: Installing with several incompatible adapters reports all of them + +- **WHEN** a user installs the project's declared facets +- **AND** more than one selected installed adapter is incompatible or cannot be loaded as a valid adapter +- **THEN** the operation SHALL fail before any materialization write +- **AND** the failure SHALL identify every incompatible selected adapter and every selected adapter that cannot be loaded +- **AND** each compatibility failure SHALL include its best available repair command + +#### Scenario: Removing a facet does not bypass adapter compatibility + +- **WHEN** a user removes a facet +- **AND** a selected installed adapter is incompatible or cannot be loaded as a valid adapter +- **THEN** the operation SHALL fail before deleting any materialized asset +- **AND** the project manifest, lockfile, install receipt, and materialized assets SHALL remain unchanged + +#### Scenario: Compatible selected adapters allow facet operations to proceed + +- **WHEN** a user adds or installs a facet +- **AND** every selected installed adapter loads as a valid adapter and declares an API supported by the CLI +- **THEN** the operation SHALL proceed through the applicable fetch, integrity verification, materialization, and project-state update requirements + +#### Scenario: Facet operation does not auto-upgrade an incompatible adapter + +- **WHEN** a facet operation detects an incompatible selected adapter +- **THEN** the system SHALL NOT download or activate a replacement adapter automatically +- **AND** the failure SHALL direct the user to an explicit adapter install command + +### Requirement: Lockfile declares a version + +The lockfile SHALL declare `lockfileVersion`. Current lockfiles SHALL use numeric `0.2`. Version selection SHALL use exact equality rather than numeric ordering: numeric `1` SHALL identify only the preceding closed-alpha schema, and numeric `0.2` SHALL identify only the current schema. Missing or unsupported versions SHALL produce structured rejection data. + +A normal install MAY migrate a verified legacy numeric-`1` lockfile to `0.2` only after the resolved artifacts satisfy every current integrity check. Frozen installation SHALL retain legacy behavior without rewriting a numeric-`1` lockfile. A current `0.2` archive SHALL require a `0.2` lockfile in frozen mode. Before a future stable lockfile v1 reuses numeric `1`, legacy-alpha support SHALL be removed and old-shape files SHALL receive actionable delete-and-regenerate guidance rather than shape-based reinterpretation. + +#### Scenario: Missing lockfile version + +- **WHEN** a lockfile omits `lockfileVersion` +- **THEN** the system SHALL reject the lockfile + +#### Scenario: Current lockfile version is accepted + +- **WHEN** a lockfile declares numeric `lockfileVersion: 0.2` and satisfies the current schema +- **THEN** the system SHALL accept it as current + +#### Scenario: Legacy alpha version is selected exactly + +- **WHEN** a lockfile declares numeric `lockfileVersion: 1` +- **THEN** the system SHALL interpret it under the previous alpha schema only +- **AND** it SHALL NOT infer a schema from the remaining shape + +#### Scenario: Unsupported lockfile version is rejected + +- **WHEN** a lockfile declares an unsupported version +- **THEN** the system SHALL reject it with structured data identifying the observed and supported versions + +#### Scenario: Normal install migrates verified legacy state + +- **WHEN** a non-frozen install loads a valid legacy-alpha lockfile and verifies the resolved artifact +- **THEN** it MAY write an equivalent `0.2` lockfile after successful installation + +#### Scenario: Frozen install does not migrate legacy state + +- **WHEN** frozen installation uses a valid legacy-alpha lockfile and legacy artifact +- **THEN** it SHALL retain legacy behavior and SHALL NOT rewrite the lockfile + +#### Scenario: Frozen current archive requires current lockfile + +- **WHEN** frozen installation encounters a `0.2` archive with a legacy-alpha lockfile +- **THEN** it SHALL fail without rewriting any state + +### Requirement: Each facet entry lists its assets, adapter-agnostically + +Every current facet entry SHALL include an `assets` array. Each member SHALL record `scope`, `type`, `name`, and a required `files` array sorted deterministically by canonical path. `scope` SHALL be `system`, `user`, or `project`; `type` SHALL be `skill`, `agent`, or `command`. Each file record SHALL contain canonical inner-archive `path` and `sha256:` `integrity` over canonical archive bytes. The lockfile SHALL contain no adapter-specific fields or adapter-encoded hashes. + +A skill's file records SHALL include `skills//SKILL.md` and every declared companion. An agent or command SHALL contain exactly its conventional primary file record. Companions SHALL remain subordinate to their owning skill and SHALL NOT become independent assets or receive their own scopes. Archive-only supplementary files SHALL NOT appear in an asset's files. + +#### Scenario: Valid multi-file skill entry + +- **WHEN** a skill named `planning` owns `SKILL.md` and two companions +- **THEN** its lockfile asset entry SHALL contain three sorted canonical file records with integrity values + +#### Scenario: Valid single-file asset entry + +- **WHEN** an agent entry has `scope: "user"`, `type: "agent"`, and `name: "reviewer"` +- **THEN** its `files` array SHALL contain exactly `agents/reviewer.md` and its integrity + +#### Scenario: Missing files array is rejected + +- **WHEN** a `0.2` asset entry omits `files` +- **THEN** the system SHALL reject the lockfile + +#### Scenario: Companion is not an independent asset + +- **WHEN** skill `review` owns companion `references/api.md` +- **THEN** the companion SHALL appear only in the skill's `files` array +- **AND** it SHALL NOT appear as another asset entry + +#### Scenario: Archive-only path is excluded + +- **WHEN** a facet archive contains declared root `README.md` +- **THEN** no lockfile asset's `files` array SHALL contain `README.md` + +#### Scenario: Unknown asset scope + +- **WHEN** an asset entry has `scope: "global"` +- **THEN** the system SHALL reject the lockfile + +#### Scenario: Unknown asset type + +- **WHEN** an asset entry has `type: "hook"` +- **THEN** the system SHALL reject the lockfile + +### Requirement: A machine-local record tracks what each project has materialized + +The system SHALL maintain a machine-local receipt describing the asset and file ownership successfully materialized for each project. The receipt SHALL be separate from version-controlled state, identified by canonical project location, and sufficient for offline rollback and removal without cache or network access. Each canonical project location SHALL have its own receipt; two projects SHALL never share one, and concurrent operations in different projects SHALL NOT contend on the same receipt. The receipt SHALL survive lockfile changes made outside the system. Current receipts SHALL use schema version `0.2` and mirror each committed lockfile asset/file ownership set without storing adapter-encoded hashes. A project without a receipt SHALL bootstrap one from its current lockfile. A legacy receipt MAY be refined to primary-only ownership because legacy installation could not materialize companions. + +The receipt, lockfile, and materialized state SHALL commit together: within one operation, handled failures SHALL roll back all three, and an interruption that prevents rollback SHALL be recoverable by re-running installation, whose per-file integrity reconciliation converges disk, lockfile, and receipt without deleting unowned files. Receipt-driven deletion SHALL pass each skill's validated owned companion path set into the adapter deletion request, so removal after a pulled lockfile drops an entry deletes exactly the recorded owned files without cache or network access. The receipt SHALL determine what is currently materialized when pulled version-control changes remove lockfile entries. In frozen-lockfile mode, receipt-driven cleanup SHALL begin only after the frozen consistency check passes. If that check rejects an orphaned lockfile entry, installation SHALL fail before cleanup changes any materialized state. Receipt data SHALL be treated as untrusted: project identity, record shape, and path containment within the selected adapter's storage SHALL be validated before deletion. Invalid or escaping records SHALL be reported and SHALL NOT cause deletion; files not recorded as owned SHALL never be deleted. + +#### Scenario: Pulled change still cleans up a multi-file skill + +- **WHEN** version-control changes remove a facet from the manifest and lockfile but the receipt records its skill primary and companions +- **THEN** install SHALL supply the validated recorded companion paths in the adapter deletion request +- **AND** delete every recorded owned file from each selected adapter +- **AND** no network or cache content SHALL be required + +#### Scenario: Interrupted install converges on re-run + +- **WHEN** an installation is interrupted after some skill-bundle writes but before lockfile and receipt commit +- **THEN** re-running installation SHALL compare locked per-file integrity against disk and complete or repair the bundle +- **AND** the re-run SHALL NOT delete any file not recorded as owned + +#### Scenario: Removal needs neither cache nor network + +- **WHEN** a facet is no longer wanted, its content is not cached, and its registry is unavailable +- **THEN** the system SHALL remove its recorded files using the receipt + +#### Scenario: Project without a receipt bootstraps one + +- **WHEN** a project has a lockfile but no receipt +- **THEN** the next operation SHALL create a project-specific receipt from current locked ownership + +#### Scenario: Escaping receipt path is not deleted + +- **WHEN** a receipt companion path resolves outside its selected adapter's storage through traversal, an absolute path, or a link +- **THEN** the system SHALL NOT delete that path +- **AND** it SHALL report the invalid record while continuing to process valid owned paths safely + +#### Scenario: Mismatched project receipt is ignored + +- **WHEN** a receipt's project identity differs from the active project +- **THEN** the system SHALL NOT delete anything based on that receipt +- **AND** it SHALL recreate ownership from the current lockfile + +#### Scenario: Unowned file in a skill directory survives cleanup + +- **WHEN** a skill directory contains a user file absent from the receipt's ownership records +- **THEN** skill removal SHALL leave that file unchanged + +### Requirement: Integrity is verified before any asset is written + +The system SHALL verify fetched or locally built facet content before writing any asset. For a current install, it SHALL require exact agreement between the lockfile's facet integrity and recomputed archive integrity; lockfile asset identities and verified materialization identities; each asset's complete locked path set and its verified owned-file set; and each per-file locked integrity, recomputed entry hash, and verified build-manifest hash. Any disagreement SHALL abort before materialization and return structured data identifying the facet, asset, canonical path, expected integrity, and actual integrity when available. Normal resolution SHALL write a replacement lock entry only after all checks succeed; frozen mode SHALL fail without rewriting. + +#### Scenario: Registry content fails declared integrity + +- **WHEN** fetched registry content differs from registry-declared integrity +- **THEN** installation SHALL abort before writing any asset or project state +- **AND** the failure SHALL identify the facet and expected and observed hashes + +#### Scenario: Registry content fails self-declared integrity + +- **WHEN** registry integrity matches but the archive does not reproduce its self-declared integrity +- **THEN** installation SHALL abort with structured archive-corruption data + +#### Scenario: Cached content fails locked integrity + +- **WHEN** cached content does not reproduce the lockfile's facet integrity +- **THEN** installation SHALL abort and identify the facet + +#### Scenario: Git or local content fails computed integrity + +- **WHEN** a built git or local artifact differs from its locked facet integrity +- **THEN** installation SHALL abort and identify the facet + +#### Scenario: Companion integrity mismatch identifies exact path + +- **WHEN** locked `skills/review/references/api.md` differs from the recomputed archive-entry hash +- **THEN** installation SHALL abort before any write +- **AND** the failure SHALL contain that path and expected and actual integrity values + +#### Scenario: Locked file set mismatch is rejected + +- **WHEN** a skill's locked `files` set has a missing or extra path relative to verified owned files +- **THEN** installation SHALL abort with structured data identifying the differing path + +#### Scenario: Frozen mismatch does not rewrite + +- **WHEN** any current per-file integrity check fails in frozen mode +- **THEN** manifest, lockfile, receipt, and adapter state SHALL remain unchanged + +### Requirement: Removing a facet uninstalls it + +When a user removes a facet from a project, the system SHALL drop the facet from the project manifest, delete the facet's materialized assets from every selected adapter, and update the lockfile and the receipt so neither records the facet — all in a single operation. A user SHALL NOT need to run a separate install step after removing. The asset set to delete SHALL be taken from the receipt, so removal SHALL require neither the cache nor the network. Skill deletion SHALL supply the validated owned companion path set in the adapter deletion request and SHALL remove the primary and every owned companion atomically while leaving unowned files untouched. Before deleting any materialized asset, the system SHALL verify that every selected installed adapter loads as a valid adapter and declares an API supported by the CLI. When a selected adapter has a missing, malformed, unsupported, or metadata-inconsistent API declaration, or cannot be loaded as a valid adapter, removal SHALL fail before deleting any materialized asset and SHALL leave the project manifest, lockfile, receipt, and materialized assets unchanged. An adapter declaring the superseded positional API `0.0` SHALL be unsupported by a CLI whose supported set is the tagged-contract API `0.1` and SHALL trigger this failure. This compatibility precondition SHALL require neither cache access nor network access; once the adapter incompatibility is repaired, removal SHALL remain able to use the receipt without either resource. + +#### Scenario: Removing a declared facet uninstalls it + +- **WHEN** a user removes a facet that is declared in the project manifest +- **AND** every selected installed adapter loads as a valid adapter and declares an API supported by the CLI +- **THEN** the system SHALL remove the facet's manifest entry, locked entry, receipt entry, and every recorded materialized file in one command + +#### Scenario: Removing multi-file skill preserves unowned content + +- **WHEN** a removed facet owns `skills/review/SKILL.md` and `skills/review/references/api.md` but not `skills/review/notes.txt` +- **AND** every selected installed adapter loads as a valid adapter and declares an API supported by the CLI +- **THEN** deletion SHALL remove the primary and owned companion +- **AND** it SHALL preserve `notes.txt` + +#### Scenario: Other facets are left intact + +- **WHEN** one facet is removed from a project with several facets +- **AND** every selected installed adapter loads as a valid adapter and declares an API supported by the CLI +- **THEN** every other facet's manifest, lockfile, receipt, and materialized files SHALL remain unchanged + +#### Scenario: Removing the last facet leaves an empty project + +- **WHEN** the user removes the only declared facet +- **AND** every selected installed adapter loads as a valid adapter and declares an API supported by the CLI +- **THEN** the manifest and lockfile SHALL remain valid and contain no facets + +#### Scenario: Removal deletes recorded assets without cache or network + +- **WHEN** a user removes a facet whose content is absent from the cache and whose registry is unreachable +- **AND** every selected installed adapter loads as a valid adapter and declares an API supported by the CLI +- **THEN** the system SHALL still delete that facet's recorded owned files using the receipt +- **AND** removal SHALL succeed without any cache read or network access + +#### Scenario: An incompatible adapter blocks removal without weakening offline recovery + +- **WHEN** a user removes a facet +- **AND** a selected installed adapter is incompatible or cannot be loaded as a valid adapter +- **THEN** removal SHALL fail before deleting any materialized asset +- **AND** the project manifest, lockfile, receipt, and materialized assets SHALL remain unchanged +- **AND** the failure SHALL NOT require or result from cache access or network access +- **AND** after the adapter incompatibility is repaired, removal SHALL remain able to use the receipt without cache or network access diff --git a/openspec/changes/support-non-asset-files/specs/protocol/spec.md b/openspec/changes/support-non-asset-files/specs/protocol/spec.md new file mode 100644 index 00000000..294ea255 --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/protocol/spec.md @@ -0,0 +1,35 @@ +## MODIFIED Requirements + +### Requirement: Protocol requirements evolve under semantic-versioning discipline + +The protocol's published surface SHALL evolve under semantic versioning. While the published protocol remains pre-1.0, backward-incompatible changes SHALL be released in the next minor version, and patch releases SHALL NOT remove, tighten, or incompatibly change requirements from their minor release. At and after 1.0, backward-incompatible changes SHALL be released only in a new major version. Within any compatible release line, requirements MAY be added only when previously conforming systems remain conforming. Release notes for every breaking release SHALL describe the behavior that previously conformed and is no longer accepted. + +#### Scenario: Adding a new optional field is backward-compatible + +- **WHEN** a new optional field is added to an artifact schema without changing the meaning of existing fields +- **THEN** systems built against the previous compatible release SHALL continue to be conforming +- **AND** the new field SHALL NOT be required for conformance until a breaking release + +#### Scenario: A pre-1.0 constraint tightening uses a minor release + +- **WHEN** a pre-1.0 release rejects a value accepted by the preceding minor release +- **THEN** the change SHALL be released in the next minor version rather than a patch version +- **AND** the release notes SHALL describe the value that is no longer accepted + +#### Scenario: A post-1.0 constraint tightening uses a major release + +- **WHEN** a release at or after 1.0 rejects a value accepted by the preceding major release +- **THEN** the change SHALL be released only in a new major version +- **AND** the major-version release notes SHALL describe the value that is no longer accepted + +#### Scenario: Removing a requirement is a breaking change + +- **WHEN** an existing protocol requirement is removed +- **THEN** the removal SHALL use the applicable pre-1.0 minor or post-1.0 major breaking-release rule +- **AND** the previous compatibility level SHALL remain available for existing consumers + +#### Scenario: Removing legacy artifact support is breaking + +- **WHEN** a release stops accepting an artifact format accepted by the preceding release line +- **THEN** the removal SHALL be released under the applicable pre-1.0 minor or post-1.0 major breaking-change rule +- **AND** the preceding release line SHALL remain available for consumers that still require the legacy format diff --git a/openspec/changes/support-non-asset-files/specs/protocol__content-hashing/spec.md b/openspec/changes/support-non-asset-files/specs/protocol__content-hashing/spec.md new file mode 100644 index 00000000..dab5fce6 --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/protocol__content-hashing/spec.md @@ -0,0 +1,165 @@ +## MODIFIED Requirements + +### Requirement: Content hashes are computed for individual text assets + +The system SHALL compute a SHA-256 content hash for every inner-archive entry: the facet manifest, each resolved primary asset, and each declared supplementary file. The facet manifest's hash SHALL use the exact bytes of the embedded `facet.json` archive entry — the source manifest file verbatim, with no re-serialization or canonicalization. A primary text asset's hash SHALL use its resolved string content encoded as UTF-8. A supplementary file's hash SHALL use its exact bytes without front-matter processing, line-ending normalization, or text decoding; empty and binary supplementary files SHALL be hashable. Every hash SHALL use the format `sha256:`. + +#### Scenario: Per-entry hashes are computed during build + +- **WHEN** a facet is built with two skills, one agent, a root `README.md`, and a skill companion +- **THEN** the build output SHALL include a content hash for the facet manifest and every primary and supplementary entry +- **AND** each hash SHALL be in `sha256:` format + +#### Scenario: Identical content produces identical hashes + +- **WHEN** two entries contain identical bytes +- **THEN** their content hashes SHALL be identical + +#### Scenario: Any content change produces a different hash + +- **WHEN** an entry's content changes by one byte +- **THEN** its content hash SHALL differ from the previous hash + +#### Scenario: Empty supplementary file is hashed + +- **WHEN** a declared supplementary file contains zero bytes +- **THEN** the build SHALL record the SHA-256 hash of the empty byte sequence + +### Requirement: Build output is assembled into a compressed archive + +The system SHALL assemble all resolved build output into a two-layer archive file with the extension `.facet`. The outer layer SHALL be an uncompressed tar containing exactly `build-manifest.json` and `archive.tar.gz`. The inner `archive.tar.gz` SHALL be a gzip-compressed tar containing the facet manifest, all resolved primary asset files, and every declared supplementary file at its canonical path. Every inner entry SHALL be derivable from the embedded facet manifest; undeclared source-tree files SHALL NOT be packaged. The archive filename SHALL follow `-.facet`, using the facet manifest's name and version. + +Before replacing previous build output, the system SHALL validate every declared source path. A missing declaration target, unsafe or non-canonical path, non-regular file, or collision SHALL produce structured failure data and SHALL leave previous build output unchanged. + +#### Scenario: Successful build produces a self-contained facet archive + +- **WHEN** a facet named `example-facet` at version `1.0.0` is built successfully +- **THEN** the system SHALL write `dist/example-facet-1.0.0.facet` +- **AND** its outer tar SHALL contain exactly `build-manifest.json` and `archive.tar.gz` + +#### Scenario: Inner archive contains all declared entries + +- **WHEN** a facet declares skill `review` with companion `references/api.md` and top-level `README.md` +- **THEN** the inner archive SHALL contain `facet.json`, `skills/review/SKILL.md`, `skills/review/references/api.md`, and `README.md` + +#### Scenario: Undeclared source files are not packaged + +- **WHEN** a source tree contains `notes.txt` that is neither a primary asset nor declared in `files` +- **THEN** the inner archive SHALL NOT contain `notes.txt` + +#### Scenario: Missing declaration target preserves previous output + +- **WHEN** a declared supplementary file is missing and `dist/` contains a previous successful build +- **THEN** the build SHALL fail with structured data identifying the missing path +- **AND** the previous `dist/` contents SHALL remain unchanged + +#### Scenario: Non-regular supplementary source is rejected + +- **WHEN** a supplementary declaration resolves through a symbolic link or hard link +- **THEN** the build SHALL fail before writing output + +#### Scenario: Inner archive name is fixed + +- **WHEN** any facet is built, regardless of name or version +- **THEN** the inner archive entry SHALL be named `archive.tar.gz` + +### Requirement: An integrity hash is computed for the uncompressed tar archive + +The system SHALL compute a SHA-256 content hash of the complete uncompressed inner-tar bytes before compression. The hash SHALL therefore cover the embedded facet manifest, every primary asset, every supplementary file, and canonical entry metadata. The inner tar SHALL use the canonical serialization: entries sorted lexicographically by path and every entry carrying the fixed deterministic metadata values (zeroed timestamps and ownership, fixed mode, empty user and group names), so independent producers reproduce identical tar bytes and identical integrity values for identical logical content. The hash format SHALL be `sha256:`. Compression SHALL remain a delivery concern so verification is independent of the gzip implementation. + +#### Scenario: Integrity hash is computed from uncompressed tar bytes + +- **WHEN** a facet is built successfully +- **THEN** the system SHALL compute the SHA-256 hash of the uncompressed inner-tar bytes before gzip compression +- **AND** the hash SHALL be in `sha256:` format + +#### Scenario: Integrity hash changes when any entry changes + +- **WHEN** any primary or supplementary entry changes +- **THEN** the integrity hash SHALL differ from the previous build's integrity hash + +#### Scenario: Integrity verification is independent of the gzip implementation + +- **WHEN** a consumer decompresses an inner archive using any compatible gzip implementation and hashes the resulting tar bytes +- **THEN** the computed hash SHALL match the integrity declared in the build manifest + +#### Scenario: Independent builders reproduce identical integrity + +- **WHEN** two facet-compatible systems build the same facet from identical sources, including supplementary files +- **THEN** the canonical serialization SHALL yield byte-identical uncompressed inner-tar bytes +- **AND** both systems SHALL compute the same integrity hash + +### Requirement: A build manifest records content hashes + +The system SHALL produce `build-manifest.json` in the outer tar of every `.facet` archive. Every newly produced build manifest SHALL declare numeric `facetVersion: 0.2`, `archive: "archive.tar.gz"`, the inner-archive `integrity`, and a `files` object mapping every canonical inner-archive path to its content hash. This current shape SHALL be emitted for asset-only facets and facets with supplementary files alike. New producers SHALL NOT emit `0.1` or an `assets` map; `0.1` remains a legacy consumer input during the compatibility window. The build manifest SHALL be a flat JSON object. + +#### Scenario: Current build manifest is embedded in the facet archive + +- **WHEN** a facet is built successfully +- **THEN** the outer tar SHALL contain `build-manifest.json` +- **AND** the manifest SHALL declare `facetVersion: 0.2`, `archive: "archive.tar.gz"`, `integrity`, and `files` + +#### Scenario: Asset-only facet still emits current format + +- **WHEN** a facet with no supplementary declarations is built +- **THEN** its build manifest SHALL declare `facetVersion: 0.2` +- **AND** its `files` map SHALL include `facet.json` and every primary asset + +#### Scenario: Build manifest integrity matches inner archive + +- **WHEN** a consumer decompresses `archive.tar.gz` and hashes the uncompressed tar bytes +- **THEN** the computed hash SHALL match the build manifest's `integrity` + +#### Scenario: Build manifest file hashes match every entry + +- **WHEN** a consumer hashes each individual inner-archive entry +- **THEN** every computed hash SHALL match the corresponding value in `files` + +#### Scenario: Build manifest is readable without inner decompression + +- **WHEN** a consumer parses the outer tar +- **THEN** the consumer SHALL be able to read `build-manifest.json` without decompressing `archive.tar.gz` + +### Requirement: Build output contains the self-contained archive + +After successful input validation, the `dist/` directory SHALL contain the `.facet` archive. By default, `dist/` SHALL contain only that archive. With `--emit-manifest`, the system SHALL also write a loose `build-manifest.json` identical to the embedded copy. The system SHALL NOT write loose manifest, asset, or supplementary source files to `dist/`. Previous build output SHALL be removed only after all source input validation succeeds. + +#### Scenario: dist contains one file by default + +- **WHEN** a facet builds successfully without `--emit-manifest` +- **THEN** `dist/` SHALL contain exactly the `.facet` archive + +#### Scenario: dist contains two files with emit manifest + +- **WHEN** a facet builds successfully with `--emit-manifest` +- **THEN** `dist/` SHALL contain the `.facet` archive and `build-manifest.json` +- **AND** the loose manifest SHALL be identical to the embedded manifest + +#### Scenario: Valid rebuild cleans previous output + +- **WHEN** all source inputs validate and `dist/` contains output from an older build +- **THEN** the system SHALL remove the previous output before writing the new output + +#### Scenario: Invalid rebuild preserves previous output + +- **WHEN** source input validation fails and `dist/` contains output from an older build +- **THEN** the previous output SHALL remain unchanged + +### Requirement: Integrity hash information is displayed in build output + +After a successful build, the system SHALL display the emitted `facetVersion`, the integrity hash, and the complete inner-archive entry listing, including supplementary files. The build progress display SHALL show archive assembly as a visible stage. The persistent plain-text summary SHALL include the integrity hash. + +#### Scenario: Build displays format, integrity, and entries + +- **WHEN** a facet with a declared `README.md` builds successfully +- **THEN** the system SHALL display `facetVersion: 0.2`, the `sha256:` integrity, and an entry listing containing `README.md` + +#### Scenario: Stdout summary includes integrity hash + +- **WHEN** a facet named `my-facet` at version `1.0.0` builds successfully +- **THEN** the stdout summary SHALL include its name, version, entry count, and a truncated integrity hash + +#### Scenario: Build progress shows archive assembly stage + +- **WHEN** a facet build is in progress +- **THEN** the progress display SHALL show archive assembly alongside validation and output stages diff --git a/openspec/changes/support-non-asset-files/specs/protocol__integrity/spec.md b/openspec/changes/support-non-asset-files/specs/protocol__integrity/spec.md new file mode 100644 index 00000000..2b560271 --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/protocol__integrity/spec.md @@ -0,0 +1,137 @@ +## MODIFIED Requirements + +### Requirement: Integrity failures are structured data + +An integrity failure SHALL be returned as structured data identifying the failing check, the expected and observed values when available, the facet name, and the exact canonical entry path when the failure concerns one file. Unsupported archive-format failures SHALL contain the observed version and supported versions. Integrity failures SHALL NOT be returned as opaque messages or raw exceptions. + +#### Scenario: A facet-level failure is structured + +- **WHEN** a facet-level integrity check fails +- **THEN** the failure SHALL identify the check, facet name, expected integrity, and observed integrity + +#### Scenario: A per-entry failure is structured + +- **WHEN** a recorded file hash differs from the recomputed hash of a primary or supplementary entry +- **THEN** the failure SHALL identify the facet and exact canonical entry path +- **AND** the failure SHALL include the expected and observed hashes + +#### Scenario: Unsupported format failure is structured + +- **WHEN** an archive declares an unsupported `facetVersion` +- **THEN** the failure SHALL contain the observed version and all supported versions + +### Requirement: A single archive-verification operation produces a structured result for any consumer of a built `.facet` + +A facet-compatible system SHALL expose one archive-verification operation that accepts built `.facet` bytes and returns structured success or failure data without throwing for expected validation failures. The operation SHALL parse the canonical two-entry outer container, select the archive schema by exact `facetVersion`, decompress the inner archive through a caller-supplied decompressor, verify the uncompressed archive integrity, validate the embedded facet manifest, verify manifest-derived archive membership, and verify every per-entry content hash. + +Valid legacy `0.1` archives SHALL be verified under their exact legacy schema and content rules during the compatibility window. Current `0.2` archives SHALL be verified under current schema and content rules. Any other version SHALL return a structured unsupported-version failure, and a malformed archive SHALL NOT be retried under another version's rules. + +Raw entry validation SHALL apply to both archive layers before any path-keyed selection. The outer container's entries SHALL be validated before either required entry is chosen: duplicate outer paths, portable-alias outer paths, and non-regular outer entries SHALL each be structured rejections rather than being collapsed by parser behavior. Before trusting inner entry contents, verification SHALL reject exact duplicate paths, portable-alias paths that collide by Unicode normalization or case folding, non-regular entries including links and directories, and unsafe, non-canonical, or non-portable paths (including Windows-reserved device-name segments, forbidden portable characters, and segments ending in a dot or space). The embedded facet manifest and the build manifest SHALL be rejected when their JSON documents contain duplicate object member names, before schema validation. The expected path set SHALL be derived from the embedded facet manifest's conventional primary paths and exact supplementary declarations rather than from the build manifest. The observed set, expected set, and `0.2` build manifest `files` key set SHALL be exactly equal. Verification SHALL reject both undeclared extra entries and declared-but-missing entries, then recompute and compare the hash of every expected entry. + +A successful `0.2` result SHALL distinguish primary assets, companion files grouped with their owning skill, and archive-only supplementary files. Primary assets SHALL be exposed as text eligible for asset processing; supplementary content SHALL remain opaque bytes. Empty-content and front-matter rules SHALL apply to primary assets only. Current asset names and skill/command namespace collisions SHALL be validated under the current manifest rules; legacy archives SHALL retain their legacy naming rules. + +The operation SHALL be the shared verification mechanism for consumers before an archive is trusted. Decompression SHALL remain caller-supplied and MAY enforce a caller-selected maximum decompressed size. A decompression refusal SHALL be represented as structured failure data. The operation SHALL NOT perform compression, decompression, or ambient input/output itself; it SHALL use the supplied decompressor for inner-archive decompression. + +#### Scenario: A self-consistent current archive verifies as valid + +- **WHEN** a caller verifies a `0.2` archive whose manifest declares `README.md` and `skills/review/references/api.md`, whose observed entries and `files` hashes exactly match those declarations, and whose integrity is valid +- **THEN** the operation SHALL return success +- **AND** the result SHALL distinguish the primary skill, its companion bytes, and the archive-only README bytes + +#### Scenario: A valid legacy archive remains accepted + +- **WHEN** a caller verifies a valid `0.1` archive during the compatibility window +- **THEN** the operation SHALL apply the legacy schema and rules and return success + +#### Scenario: An unsupported version is rejected without fallback + +- **WHEN** a caller verifies an archive declaring `facetVersion: 0.3` +- **THEN** the operation SHALL return structured failure data containing `0.3` and the supported versions +- **AND** the operation SHALL NOT reinterpret the archive as `0.1` or `0.2` + +#### Scenario: A tampered inner archive is rejected + +- **WHEN** inner-archive bytes no longer reproduce the build manifest's integrity +- **THEN** the operation SHALL return an integrity-mismatch failure +- **AND** the operation SHALL NOT throw + +#### Scenario: A supplementary-file hash mismatch is rejected + +- **WHEN** `skills/review/references/api.md` does not hash to its `files` value +- **THEN** the failure SHALL identify that exact path and the expected and observed hashes + +#### Scenario: Undeclared inner entry is rejected + +- **WHEN** a `0.2` inner archive contains `secret.txt` that is not derivable from the embedded manifest +- **THEN** the operation SHALL return structured failure data identifying `secret.txt` as undeclared + +#### Scenario: Build-manifest-only entry cannot expand archive membership + +- **WHEN** a `0.2` build manifest records `secret.txt` and the inner archive contains it, but the embedded facet manifest does not derive that path +- **THEN** verification SHALL reject `secret.txt` as undeclared +- **AND** the build-manifest record SHALL NOT legitimize the entry + +#### Scenario: Declared but missing entry is rejected + +- **WHEN** the embedded manifest declares `README.md` but the inner archive omits it +- **THEN** the operation SHALL return structured failure data identifying `README.md` as missing + +#### Scenario: Duplicate inner paths are rejected + +- **WHEN** the inner tar contains two entries with the same path +- **THEN** the operation SHALL reject the archive rather than silently selecting one entry + +#### Scenario: Portable alias inner paths are rejected + +- **WHEN** an inner tar contains paths that differ in spelling but collide by Unicode normalization or portable case folding +- **THEN** verification SHALL return structured failure data identifying both aliased paths +- **AND** it SHALL reject the archive before selecting either entry + +#### Scenario: Non-regular inner entry is rejected + +- **WHEN** the inner tar contains a symbolic link, hard link, directory, or device entry +- **THEN** the operation SHALL return a structured content failure + +#### Scenario: Invalid embedded facet manifest is rejected + +- **WHEN** an archive's embedded `facet.json` does not satisfy the schema for its version +- **THEN** the operation SHALL return a failure identifying the embedded manifest + +#### Scenario: Current content rules distinguish primary and supplementary files + +- **WHEN** a `0.2` archive contains an empty primary asset and an empty declared supplementary file +- **THEN** the operation SHALL reject the empty primary asset +- **AND** it SHALL NOT reject the supplementary file merely because it is empty + +#### Scenario: Current skill and command collision is rejected + +- **WHEN** a `0.2` embedded manifest declares both skill `review` and command `review` +- **THEN** the operation SHALL return a structured naming-collision failure identifying both declarations + +#### Scenario: Caller-supplied decompressor refuses to decompress + +- **WHEN** the supplied decompressor refuses because the inner archive exceeds the caller's allowance +- **THEN** the operation SHALL return structured decompression-refusal data +- **AND** the operation SHALL NOT throw + +#### Scenario: Malformed outer container is rejected + +- **WHEN** input bytes cannot be parsed as the canonical two-entry outer container +- **THEN** the operation SHALL return structured malformed-container data +- **AND** the operation SHALL NOT throw + +#### Scenario: Duplicate outer entry is rejected + +- **WHEN** the outer tar contains two entries named `build-manifest.json`, or a non-regular entry in place of a required outer entry +- **THEN** the operation SHALL return structured failure data before selecting either entry +- **AND** the operation SHALL NOT let parser collapse decide which entry is authoritative + +#### Scenario: Duplicate JSON members are rejected + +- **WHEN** the embedded `facet.json` or `build-manifest.json` contains the same object member name twice +- **THEN** the operation SHALL return a structured rejection before schema validation + +#### Scenario: Non-portable inner path is rejected + +- **WHEN** a `0.2` inner archive contains an entry whose path includes a Windows-reserved device-name segment such as `references/con`, a forbidden character such as `:`, or a segment ending in a dot or space +- **THEN** the operation SHALL return structured failure data identifying the non-portable path diff --git a/openspec/changes/support-non-asset-files/specs/protocol__schemas/spec.md b/openspec/changes/support-non-asset-files/specs/protocol__schemas/spec.md new file mode 100644 index 00000000..aae653e2 --- /dev/null +++ b/openspec/changes/support-non-asset-files/specs/protocol__schemas/spec.md @@ -0,0 +1,252 @@ +## MODIFIED Requirements + +### Requirement: A facet manifest schema is published as part of the protocol + +The shape of a facet manifest (`facet.json`) SHALL be published as a normative schema. Any system that produces a facet manifest SHALL produce one conforming to the published schema. Any system that consumes a facet manifest SHALL validate it against the published schema before treating any value as trusted. The schema SHALL define the required fields, the permitted shapes for skills, agents, and commands, exact supplementary-file declarations, the accepted facet and asset identity grammars, the optional facet privacy declaration, and the rules for unrecognized fields. + +A facet identity name SHALL be either an unscoped name (``) or a scoped name (`@/`). Each `slug` and `scope` component SHALL satisfy the same component grammar: it MUST be at least 2 characters and at most 64 characters, MUST start with a lowercase ASCII letter, MUST end with a lowercase ASCII letter or ASCII digit, MUST contain only lowercase ASCII letters, ASCII digits, and hyphens, and MUST NOT contain consecutive hyphens. Uppercase letters, non-ASCII characters, underscores, dots, spaces, plus signs, tildes, emoji, and any other character outside the component grammar SHALL be rejected rather than normalized. A facet manifest whose `name` is malformed SHALL be rejected as invalid. + +Current-format skill, agent, and command names SHALL implement the [Agent Skills `name` field convention](https://agentskills.io/specification#name-field) with the following normative ASCII interpretation: each name MUST contain 1–64 lowercase ASCII letters, digits, or hyphens; MUST NOT begin or end with a hyphen; MUST NOT contain consecutive hyphens; and MUST be a single segment without `/`. Skills and commands SHALL share one logical namespace and MUST NOT use the same name. Agents SHALL occupy a separate namespace and MAY share a name with a skill or command. Legacy `0.1` archives SHALL retain their legacy asset-name and namespace validation; an invalid current-format manifest SHALL NOT be reinterpreted under legacy rules. + +The manifest schema SHALL define optional exact-path supplementary declarations at two sites. A top-level `files` array SHALL contain repository-relative paths for archive-only files and MUST NOT resolve under `skills/`. A skill descriptor's `files` array SHALL contain paths relative to that skill's directory, MUST NOT name `SKILL.md`, and MUST resolve below the skill directory. Both arrays SHALL contain exact paths only; glob or pattern declarations SHALL NOT be accepted. + +Every declared supplementary path MUST be non-empty, relative, and canonical. It MUST NOT contain empty, `.` or `..` segments, backslashes, NUL bytes, or absolute, drive, or URL-like prefixes. Every path segment MUST be portable across supported filesystems: it MUST NOT contain control bytes or the characters `<`, `>`, `:`, `"`, `|`, `?`, `*`; MUST NOT equal a Windows-reserved device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`, `LPT1`–`LPT9`) case-insensitively, with or without an extension; and MUST NOT end with a dot or a space. The exact root path `facet.json` MUST NOT be declared, while that basename MAY be used below another directory. Supplementary paths MUST NOT collide with conventional primary-asset paths. The complete declared set MUST be free of exact duplicates, Unicode-normalization aliases, portable case-fold aliases, and file/directory prefix conflicts. + +A consumer validating a facet manifest SHALL reject a JSON document containing duplicate object member names before schema validation, rather than allowing parser-dependent last-member-wins collapse to select which declaration is validated. + +The facet manifest schema SHALL define an optional top-level `private` field. When present, `private` SHALL be a boolean. A manifest with `private: true` SHALL express the author's intent that the facet is private. A manifest with `private: false`, or with no `private` field, SHALL express public-by-default behavior. Validation SHALL NOT inject `private: false` into a manifest that omits the field; omission remains omission in validated data. Values of any non-boolean type SHALL be rejected rather than treated as unknown extension data or coerced to booleans. + +The facet manifest schema SHALL NOT document unsupported composition or server-reference fields as part of the current user-facing manifest contract. Current user-facing manifest documentation SHALL describe only supported manifest behavior and SHALL use the manifest specification page as the canonical place for facet-name grammar. + +#### Scenario: A producer emits a manifest conforming to the published schema + +- **WHEN** a system produces a `facet.json` for distribution +- **THEN** the produced manifest SHALL satisfy every requirement of the published schema +- **AND** another facet-compatible system SHALL accept the manifest after validating it + +#### Scenario: A consumer accepts valid unscoped facet identities + +- **WHEN** a system receives a `facet.json` whose `name` is `ab`, `cowsay`, `julian`, `admin-tester`, `apple-b34r`, or `f-o-s-s-o` +- **THEN** the system SHALL accept the facet identity as valid +- **AND** the accepted identity SHALL remain the facet's canonical name without normalization + +#### Scenario: A consumer accepts a valid scoped facet identity + +- **WHEN** a system receives a `facet.json` whose `name` is `@julian/cowsay` +- **THEN** the system SHALL accept the facet identity as valid +- **AND** both scoped identity components SHALL satisfy the same component grammar as unscoped facet names +- **AND** the scoped identity SHALL remain the facet's canonical name + +#### Scenario: A consumer accepts omitted public privacy declaration + +- **WHEN** a system receives a `facet.json` with no `private` field +- **THEN** the system SHALL accept the manifest as public by default +- **AND** validation SHALL NOT add a `private` field to the accepted manifest data + +#### Scenario: A consumer accepts explicit public privacy declaration + +- **WHEN** a system receives a `facet.json` with `private: false` +- **THEN** the system SHALL accept the manifest as explicitly public +- **AND** the accepted manifest SHALL preserve `private: false` + +#### Scenario: A consumer accepts private privacy declaration + +- **WHEN** a system receives a `facet.json` with `private: true` +- **THEN** the system SHALL accept the manifest as declaring private publish intent +- **AND** the accepted manifest SHALL preserve `private: true` + +#### Scenario: A consumer rejects non-boolean privacy declaration + +- **WHEN** a system receives a `facet.json` whose `private` field is a string, number, object, array, or null +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error indicating that `private` must be boolean + +#### Scenario: A consumer rejects invalid slug components + +- **WHEN** a system receives a `facet.json` whose `name` is empty, `a`, `z`, `A`, `Cowsay`, `1abc`, `-abc`, `abc-`, `abc--def`, `abc_def`, `abc.def`, `abc def`, `éclair`, `gооgle` with Cyrillic homoglyphs, or any component longer than 64 characters +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error indicating that the facet identity is malformed + +#### Scenario: A consumer rejects malformed scoped facet identities + +- **WHEN** a system receives a `facet.json` whose `name` is `@scope`, `@/name`, `@scope/`, `@scope/name/extra`, or `scope/name` +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error indicating that the facet identity is malformed + +#### Scenario: Current-format asset names use one ASCII segment + +- **WHEN** a current-format manifest declares assets named `review`, `review-2`, and `a` +- **THEN** the system SHALL accept those asset names + +#### Scenario: Invalid current-format asset name is rejected + +- **WHEN** a current-format manifest declares an asset named `review/code`, `-review`, `review-`, `review--code`, `Review`, or a name longer than 64 characters +- **THEN** the system SHALL reject the manifest +- **AND** the system SHALL identify the invalid asset declaration and naming constraint + +#### Scenario: Skill and command names collide + +- **WHEN** a current-format manifest declares both a skill named `review` and a command named `review` +- **THEN** the system SHALL reject the manifest +- **AND** the structured failure SHALL identify both declarations + +#### Scenario: Agent may share a name with a skill + +- **WHEN** a current-format manifest declares a skill named `review` and an agent named `review` +- **THEN** the system SHALL accept the shared name + +#### Scenario: Supplementary declaration sites are disjoint + +- **WHEN** a manifest declares `skills/review/references/api.md` in top-level `files` +- **THEN** the system SHALL reject the manifest +- **AND** the failure SHALL direct the declaration to the owning skill's `files` array + +#### Scenario: Unsafe supplementary path is rejected + +- **WHEN** a manifest declares `../secret`, `/absolute`, `C:\secret`, or `docs//guide.md` +- **THEN** the system SHALL reject the manifest +- **AND** the structured failure SHALL identify the unsafe path + +#### Scenario: Portable declaration collision is rejected + +- **WHEN** a manifest declares both `Docs/guide.md` and `docs/guide.md`, or both `docs` as a file and `docs/guide.md` +- **THEN** the system SHALL reject the manifest as colliding on supported filesystems + +#### Scenario: Windows-reserved path component is rejected + +- **WHEN** a manifest declares `references/con`, `aux.txt`, `notes:draft.md`, `report.` , or `draft ` +- **THEN** the system SHALL reject the manifest +- **AND** the structured failure SHALL identify the non-portable path segment + +#### Scenario: Duplicate manifest members are rejected + +- **WHEN** a `facet.json` document contains the same object member name twice +- **THEN** the system SHALL reject the document before schema validation +- **AND** the system SHALL NOT silently validate whichever member a parser retains + +#### Scenario: A consumer rejects a manifest that violates the published schema + +- **WHEN** a system receives a `facet.json` that omits a required field or contains a field with the wrong type +- **THEN** the system SHALL reject the manifest as invalid +- **AND** the system SHALL surface a structured error indicating which constraint was violated + +#### Scenario: A consumer tolerates unrecognized fields + +- **WHEN** a system receives a `facet.json` containing a field not defined in the schema +- **THEN** the system SHALL accept the manifest as valid +- **AND** the system SHALL preserve the unknown field if it later re-emits the manifest + +### Requirement: A lockfile schema is published as part of the protocol + +The shape of a lockfile (`facets.lock`) SHALL be published as a normative schema. Any system that reads, writes, or interprets a lockfile SHALL conform to the published schema. The schema SHALL define the lockfile version, source-provenance fields, identity-and-integrity fields, the asset list and its materialized-file integrity records, and the rules for unrecognized fields. + +Version dispatch SHALL use exact equality rather than numeric ordering. Legacy numeric `1` SHALL identify only the previous alpha schema, and numeric `0.2` SHALL identify the current schema. Archive-format and lockfile-format versions SHALL be interpreted independently even when they have the same numeric value. A lockfile JSON document containing duplicate object member names SHALL be rejected before schema validation. + +The published schema's source-provenance fields SHALL take a tagged form keyed on the source kind, so that the provenance fields meaningful for each kind are explicit. The published schema SHALL define a registry source that records the registry origin, a git source that records the repository URL and a required resolved commit, and a local source that records the resolved path. A lockfile whose entry source does not declare a recognized kind, or omits a field required for its declared kind, SHALL NOT satisfy the published schema. A source MAY carry additional unrecognized keys and still satisfy the published schema. + +Every `0.2` asset entry SHALL record `scope`, `type`, and `name` plus a required `files` array sorted deterministically by canonical path. Each file record SHALL contain exactly the canonical inner-archive `path` and its `sha256:` `integrity` over canonical archive bytes. A skill entry SHALL list `skills//SKILL.md` and every declared companion below that skill. An agent entry SHALL list exactly `agents/.md`; a command entry SHALL list exactly `commands/.md`. Archive-only supplementary files SHALL NOT appear in an asset's `files` array, and companion files SHALL NOT appear as independent assets. + +#### Scenario: A consumer interprets a lockfile written by a different system + +- **WHEN** a system reads a `facets.lock` written by a different facet-compatible system +- **THEN** the system SHALL interpret every field per the published schema +- **AND** the system SHALL accept the lockfile as valid input for installation + +#### Scenario: A producer writes a lockfile that any consumer can read + +- **WHEN** a system writes a `facets.lock` after resolving facet sources +- **THEN** the resulting file SHALL satisfy the published schema +- **AND** another facet-compatible system SHALL be able to read the file and reproduce the same install state + +#### Scenario: Source provenance is tagged by kind + +- **WHEN** a facet-compatible system reads an entry's source provenance from a `facets.lock` +- **THEN** the source SHALL declare its kind as registry, git, or local +- **AND** a registry source SHALL record the registry origin and SHALL NOT carry a version specifier +- **AND** a git source SHALL record the repository URL and a required resolved commit, and SHALL NOT record a symbolic ref +- **AND** a local source SHALL record the resolved path + +#### Scenario: A git source missing its required commit is rejected + +- **WHEN** a facet-compatible system reads a lockfile whose entry declares a git source with no commit +- **THEN** the lockfile SHALL NOT satisfy the published schema +- **AND** the system SHALL reject the lockfile + +#### Scenario: A source with extra unrecognized keys is accepted + +- **WHEN** a facet-compatible system reads a lockfile whose entry source declares a recognized kind with all its required fields plus unrecognized keys +- **THEN** the lockfile SHALL satisfy the published schema +- **AND** the system SHALL accept the lockfile + +#### Scenario: Current skill entry lists every materialized file + +- **WHEN** a `0.2` lockfile records skill `review` with companions `references/api.md` and `scripts/run.ts` +- **THEN** the skill's sorted `files` array SHALL contain `skills/review/SKILL.md`, `skills/review/references/api.md`, and `skills/review/scripts/run.ts` +- **AND** each record SHALL contain a canonical path and `sha256:` integrity + +#### Scenario: Single-file assets list one file + +- **WHEN** a `0.2` lockfile records agent `reviewer` and command `review` +- **THEN** each asset's `files` array SHALL contain exactly its conventional primary path + +#### Scenario: Archive-only file is excluded from asset records + +- **WHEN** a verified facet contains a declared root `README.md` +- **THEN** no `0.2` asset entry SHALL list `README.md` + +#### Scenario: Legacy alpha version is selected exactly + +- **WHEN** a lockfile declares numeric `lockfileVersion: 1` +- **THEN** the system SHALL interpret it only with the previous alpha schema +- **AND** the system SHALL NOT reinterpret its shape as `0.2` + +### Requirement: A build manifest schema is published as part of the protocol + +The shape of a build manifest (`build-manifest.json`) embedded inside a `.facet` archive SHALL be published as a normative schema. Any system that produces a `.facet` archive SHALL include a build manifest conforming to the schema. Any system that consumes a `.facet` archive SHALL validate the embedded build manifest against the schema before trusting its values. + +The build-manifest schema SHALL be selected by exact `facetVersion` equality. Legacy `0.1` manifests SHALL retain their legacy `assets` hash map. Current `0.2` manifests SHALL declare numeric `facetVersion: 0.2` exactly, SHALL declare `archive` with the exact literal value `"archive.tar.gz"`, and SHALL contain `integrity` and a `files` map from every canonical inner-archive path to its `sha256:` content hash; they SHALL NOT contain an `assets` map. Any other `archive` value SHALL fail schema validation, so producers and consumers cannot disagree about which outer-tar entry is authoritative. A build-manifest JSON document containing duplicate object member names SHALL be rejected before schema validation. The `files` key set SHALL exactly equal the observed inner-archive entry set and SHALL include the embedded `facet.json`, every primary asset, and every supplementary file. Entry classification SHALL be derived from the embedded facet manifest rather than the hash map. + +A `files` key in a `0.1` build manifest or an `assets` key in a `0.2` build manifest SHALL fail schema validation. A malformed current manifest SHALL NOT be reinterpreted under a legacy schema. An unsupported version SHALL produce structured failure data containing the observed version and the supported versions. + +#### Scenario: A producer embeds a conforming current build manifest + +- **WHEN** a system produces a current `.facet` archive +- **THEN** the embedded `build-manifest.json` SHALL declare `facetVersion: 0.2`, `archive: "archive.tar.gz"`, the archive integrity, and one `files` hash for every inner-archive entry + +#### Scenario: A consumer accepts a legacy build manifest + +- **WHEN** a system receives a valid `0.1` build manifest with its legacy `assets` map +- **THEN** the system SHALL validate it under the `0.1` schema + +#### Scenario: Current manifest rejects legacy hash-map shape + +- **WHEN** a `0.2` build manifest contains an `assets` map instead of `files` +- **THEN** the system SHALL reject the manifest +- **AND** the system SHALL NOT retry validation as `0.1` + +#### Scenario: Current file-hash set must be complete + +- **WHEN** a `0.2` build manifest omits an observed inner-archive path or records a path absent from the inner archive +- **THEN** the system SHALL reject the archive as invalid + +#### Scenario: Unsupported build-manifest version is structured + +- **WHEN** a system receives a build manifest with `facetVersion: 0.3` +- **THEN** the system SHALL return structured failure data containing `0.3` and the supported versions + +#### Scenario: Non-canonical archive entry name is rejected + +- **WHEN** a `0.2` build manifest declares `archive: "payload.tar.gz"` +- **THEN** the system SHALL reject the manifest as violating the schema + +#### Scenario: Duplicate build-manifest members are rejected + +- **WHEN** a `build-manifest.json` document contains two `files` members +- **THEN** the system SHALL reject the document before schema validation +- **AND** the system SHALL NOT select either member's hash map + +#### Scenario: A consumer rejects an archive whose build manifest violates the schema + +- **WHEN** a system receives a `.facet` archive whose `build-manifest.json` is missing a required field or has malformed integrity +- **THEN** the system SHALL reject the archive as invalid +- **AND** the system SHALL surface a structured error identifying the violation diff --git a/openspec/changes/support-non-asset-files/tasks.md b/openspec/changes/support-non-asset-files/tasks.md new file mode 100644 index 00000000..e1507ddb --- /dev/null +++ b/openspec/changes/support-non-asset-files/tasks.md @@ -0,0 +1,138 @@ +> **Before executing any tasks below**, load the `viper-execution-rules` skill for the full VIPER step protocol (step types, execution rules, gating, and hard constraints). + +## Step Types + +- **Verify** → CHECK. Run automated checks (tests, lint, type checks). + If all checks pass, proceed. If anything fails, STOP and notify the user. +- **Implement** → WRITE. Make code changes — create, edit, or delete files. +- **Propose** → READ-ONLY + USER GATE. Present intended changes in your message text first, + then ask for approval using the `question` tool with a short prompt (Approve / Reject / Request changes). + Never put details in the question — the question is just the gate. Do not write anything. +- **Explore** → READ-ONLY. Read files, search the codebase, investigate broadly. + No writes allowed. Use this to understand the problem space before acting. +- **Review** → READ-ONLY + USER GATE. Present findings and analysis in your message text first, + then ask for feedback using the `question` tool with a short prompt. + Never put details in the question — the question is just the gate. + +## 1. Protocol Models and Archive Plan — Research + +- [ ] 1.1 Explore: Inspect the current facet, build-manifest, lockfile, and asset-name schemas and identify every current-versus-legacy validation call site +- [ ] 1.2 Explore: Trace archive membership, path validation, collision detection, and per-entry hashing across protocol and engine build code +- [ ] 1.3 Explore: Inspect protocol public exports, version constants, fixtures, and schema tests that constrain compatibility +- [ ] 1.4 Propose: Define the protocol model for exact supplementary declarations, tagged archive-plan entries, version dispatch, and structured validation failures + +## 2. Protocol Models and Archive Plan — Implementation + +- [ ] 2.1 Implement: Add top-level and per-skill exact `files` declarations, current single-segment asset-name validation, and the shared skill/command namespace while isolating legacy `0.1` naming behavior +- [ ] 2.2 Implement: Add one pure archive-plan operation that classifies manifest, primary-asset, skill-companion, and archive-only entries and enforces the complete path-safety and collision grammar, including Windows-portable component rules (reserved device names, forbidden characters, control bytes, trailing dot/space) +- [ ] 2.3 Implement: Add separate archive-format and lockfile-format constants plus exact versioned build-manifest schemas for legacy `0.1` `assets` and current `0.2` `files`, pinning numeric `facetVersion: 0.2` and the exact `archive: "archive.tar.gz"` literal, and rejecting duplicate JSON object members in facet manifests, build manifests, and lockfiles before schema validation +- [ ] 2.4 Implement: Add the lockfile `0.2` schema with deterministic per-asset file-integrity records and exact legacy-alpha-`1` versus current-`0.2` dispatch +- [ ] 2.5 Implement: Curate protocol exports and add focused schema, name, archive-plan, collision, version-dispatch, and lockfile tests for all legal and illegal states +- [ ] 2.6 Verify: Run the focused protocol typecheck and test suites for schemas and archive planning + +## 3. Archive Verification and Consumer Bridge — Research + +- [ ] 3.1 Explore: Trace outer/inner tar parsing and identify where duplicate, aliased, unsafe, and non-regular headers can be rejected before path-keyed maps are built +- [ ] 3.2 Explore: Trace archive verification, cache extraction/auditing, registry download, and engine loading from verified bytes through resolved facet data +- [ ] 3.3 Explore: Inspect integrity result types and CLI failure rendering for path-specific mismatches, decompression refusal, and unsupported versions +- [ ] 3.4 Propose: Define the consumer-first bridge approach for strict `0.1`/`0.2` dispatch, tagged verified content, immutable fixtures, and actionable failures without enabling `0.2` production + +## 4. Archive Verification and Consumer Bridge — Implementation + +- [ ] 4.1 Implement: Validate raw tar headers for both the outer container and the inner archive before lossy mapping and return structured failures for duplicate paths, portable aliases, unsafe or non-portable paths, and every non-regular entry type +- [ ] 4.2 Implement: Make archive verification derive exact expected membership from the embedded manifest's shared archive plan and require equality with observed entries and the version-selected hash map +- [ ] 4.3 Implement: Keep supplementary content as opaque bytes and return a tagged verified result that groups companions with their owning skill while decoding and validating only primary assets as text +- [ ] 4.4 Implement: Add structured unsupported-version and per-entry integrity failures, preserve caller-supplied decompression, and prevent malformed current archives from falling back to legacy rules +- [ ] 4.5 Implement: Update registry download, cache audit/extraction, and engine loaders to consume tagged verified results without exposing archive-only files to materialization +- [ ] 4.6 Implement: Add immutable valid `0.1` and `0.2` fixtures plus tampering, missing/extra entry, raw-header (both layers), duplicate-JSON-member, portable-alias, non-portable path, binary, empty-supplementary, and legacy-compatibility tests +- [ ] 4.7 Verify: Run focused protocol and engine consumer tests and confirm the bridge accepts both formats while no producer yet emits `0.2` + +## 5. Adapter Skill Bundles — Research + +- [ ] 5.1 Explore: Trace adapter install/read/delete calls and identify every positional-contract implementation and consumer +- [ ] 5.2 Explore: Inspect filesystem helper containment, metadata transformation, pruning, and failure behavior for each first-party adapter +- [ ] 5.3 Explore: Inspect engine materialization's type-only adapter dependency and determine how atomic helpers can remain owned by the adapter SDK +- [ ] 5.4 Propose: Define tagged request/result unions and an all-or-nothing owned skill-bundle lifecycle that cannot represent companions on agents or commands +- [ ] 5.5 Explore: Audit the merged adapter-API-version machinery — `ADAPTER_API_VERSION`, `SUPPORTED_ADAPTER_APIS`, verifier/loader/inspection classification, npm package/runtime declaration selection, and first-party prepack `facetAdapterApiVersion` injection — and confirm every consumer derives from the single SDK constant + +## 6. Adapter Skill Bundles — Implementation + +- [ ] 6.1 Implement: Replace positional adapter asset methods with tagged skill, agent, and command requests/results carrying explicit scope, type, and name, with skill variants carrying engine-supplied owned-companion path sets for install, read, and delete +- [ ] 6.2 Implement: Add SDK filesystem helpers that validate every supplied companion path (new or owned) as contained below the skill root before any filesystem access, plus staged bundle replacement, rollback, ownership-set-based deletion, and empty-directory pruning with primary-only metadata transformation +- [ ] 6.3 Implement: Migrate claude-code, opencode, and codex adapters to the tagged contract and canonical reads, including consistent skill-root pruning and preservation of unowned files +- [ ] 6.4 Implement: Add SDK and first-party adapter tests for companion-less and multi-file skills, escaping paths in bundles and ownership sets, malformed ownership rejection before any filesystem access, idempotence, canonical reads limited to requested owned paths, unowned content preservation, and injected failures at every write/delete/commit boundary +- [ ] 6.5 Implement: Update adapter public exports and add the required pre-1.0 minor-release metadata describing the breaking contract +- [ ] 6.6 Implement: Bump the single-source-of-truth `ADAPTER_API_VERSION` from `0.0` to `0.1` for the tagged contract, keep `SUPPORTED_ADAPTER_APIS` derived from it (support set `{0.1}`), and update first-party adapter package/runtime declarations and prepack `facetAdapterApiVersion` injection so every consumer derives `0.1` without hardcoding the token +- [ ] 6.7 Implement: Add fail-closed coverage proving `defineAdapter()` stamps `0.1` while author definitions cannot supply it, a positional `0.0` declaration is well-formed but unsupported by a `{0.1}` CLI, an installed/runtime `0.0` adapter is rejected before any contract method or project mutation, package/runtime metadata agree at `0.1`, and tagged `0.1` first-party adapters proceed through normal materialization +- [ ] 6.8 Verify: Run focused adapter SDK, compatibility/verifier, and all first-party adapter typechecks and tests + +## 7. Lockfile, Receipt, and Materialization — Research + +- [ ] 7.1 Explore: Trace lockfile loading/writing and every place resolved entries are inherited, minted, compared, or carried forward +- [ ] 7.2 Explore: Trace receipt loading, bootstrapping, project isolation, drift removal, tri-write commit, and rollback ordering +- [ ] 7.3 Explore: Trace materialization, skip-if-identical behavior, journaling, deletion, drift reporting, and archive-to-adapter data flow +- [ ] 7.4 Propose: Define the migration and transaction approach for per-file integrity, untrusted receipt ownership, atomic skill bundles, normal legacy migration, and frozen legacy behavior + +## 8. Lockfile, Receipt, and Materialization — Implementation + +- [ ] 8.1 Implement: Replace numeric-order lockfile handling with exact legacy-alpha-`1` and current-`0.2` loading, normal-mode migration, and frozen-mode no-rewrite behavior +- [ ] 8.2 Implement: Derive sorted lockfile asset file records from the verified materialization subset and recomputed entry hashes rather than copying self-declared hash values +- [ ] 8.3 Implement: Enforce pre-materialization agreement among facet integrity, asset identities, complete owned path sets, recomputed entry hashes, and verified build-manifest hashes with path-specific result variants, running the adapter-compatibility preflight (positional `0.0` rejected by a `{0.1}` CLI) ahead of archive-version dispatch and per-file reconciliation +- [ ] 8.4 Implement: Introduce receipt `0.2` asset/file ownership, safe legacy refinement, project-isolated bootstrap, and containment validation that treats receipt data as untrusted +- [ ] 8.5 Implement: Commit lockfile, receipt, and adapter state transactionally and ensure frozen consistency gates complete before receipt-driven cleanup begins +- [ ] 8.6 Implement: Materialize only primary assets and owned skill companions through tagged adapter requests carrying validated ownership sets from the lockfile and receipt, with per-file skip/repair behavior and rollback journal preimages +- [ ] 8.7 Implement: Make drift and removal path-specific, preserve unowned files, and support offline multi-file cleanup from receipts without cache or network access +- [ ] 8.8 Implement: Render lockfile, archive-version, per-file mismatch, adapter-bundle, and receipt failures exhaustively in CLI install output using one compatibility table for known format transitions +- [ ] 8.9 Implement: Add engine and CLI tests for migration, frozen failures, receipt corruption/isolation, pulled-lockfile cleanup, per-file drift, integrity mismatch, rollback, archive-only withholding, and exact diagnostics +- [ ] 8.10 Implement: Add a full-cycle end-to-end test that builds and verifies a facet with skill companions and archive-only files, installs it, detects and repairs single-file drift, exercises interrupted-install convergence on re-run without deleting unowned files, and removes it offline from the receipt, then exercises the same install path with an immutable legacy `0.1` archive +- [ ] 8.11 Verify: Run focused install, materialization, receipt, lockfile, cache, registry, and CLI install tests + +## 9. Current Producer and Build Pipeline — Research + +- [ ] 9.1 Explore: Trace source-file loading, build validation stages, archive assembly, output cleanup, and build-result rendering +- [ ] 9.2 Explore: Inspect source filesystem APIs needed to reject missing files, links, resolved aliases, and non-regular declarations before output mutation +- [ ] 9.3 Explore: Confirm consumer support and the external registry verification gate required before any producer can emit archive format `0.2` +- [ ] 9.4 Propose: Define the producer switch that reuses the archive plan, preserves deterministic bytes, validates before cleanup, and emits only current-format output + +## 10. Current Producer and Build Pipeline — Implementation + +- [ ] 10.1 Implement: Record a passing consumer-and-registry readiness gate and do not enable `0.2` producer output if either consumer class is not ready +- [ ] 10.2 Implement: Load declared supplementary files as exact bytes, validate their resolved regular-file identities, and preserve previous `dist/` output on every input failure +- [ ] 10.3 Implement: Drive archive collection and all-entry hashing from the shared archive plan, preserving deterministic ordering and opaque binary or empty supplementary content +- [ ] 10.4 Implement: Switch every new build, including asset-only facets, to flat build-manifest `0.2` output with a complete `files` map while retaining legacy consumer support +- [ ] 10.5 Implement: Update build results and CLI output to show the emitted format, complete entry listing, integrity, and archive-assembly stage +- [ ] 10.6 Implement: Add the build failure-class matrix for traversal, absolute/drive/URL prefixes, backslashes, NUL, empty/`.`/`..` segments, Unicode-normalization and portable-case aliases, Windows-reserved device names, forbidden portable characters, trailing dot/space segments, file/directory prefix collisions, symlinks, hard links, duplicate paths, reserved root `facet.json`, conventional-primary-path collisions, missing declarations, undeclared entries, and tampered bytes, plus success tests for top-level files, nested companions, binary/empty bytes, exact manifest-byte hashing, canonical-tar determinism, scoped output paths, and validation-before-cleanup +- [ ] 10.7 Verify: Run focused build pipeline and CLI build tests and inspect representative `0.2` archives for exact deterministic membership + +## 11. Create and Edit Authoring — Research + +- [ ] 11.1 Explore: Trace scaffold options, manifest generation, templates, previews, and create wizard state/editor round-trips +- [ ] 11.2 Explore: Trace edit scanner, reconciliation, context, operation, manifest-rewrite, confirmation, and transactional apply types +- [ ] 11.3 Explore: Inspect create/edit focus management and exhaustive UI switches that must represent two independent README paths and path-bearing reconciliation items +- [ ] 11.4 Propose: Define tagged README and supplementary-file states, stable reconciliation identities, headless-create behavior, and an exact-path operation preview for the full authoring block + +## 12. Create and Edit Authoring — Implementation + +- [ ] 12.1 Implement: Add an editable default `README.md` scaffold option and template that writes the file and top-level declaration atomically without regenerating authored content after identity edits +- [ ] 12.2 Implement: Add the dedicated create README card/editor flow, optional disable behavior, state snapshotting, and explicit confirmation preview, and align headless create with the documented policy +- [ ] 12.3 Implement: Extend edit scanning and reconciliation for undeclared skill companions, common root files, and missing declared supplementary files while routing only exact `README.md` and `README` paths to a dedicated panel +- [ ] 12.4 Implement: Add independent tagged states and actions for both conventional README paths, preserving bytes on adoption and retaining exact paths for scaffold, edit, removal, and declaration changes +- [ ] 12.5 Implement: Replace string-parsed reconciliation keys with stable structured identities and represent file/declaration operations as tagged variants with no invalid combinations +- [ ] 12.6 Implement: Apply README, companion, and generic supplementary changes transactionally with manifest edits and show every queued exact-path operation before Apply +- [ ] 12.7 Implement: Add engine, CLI, TUI, integration, and create-build end-to-end tests covering README defaults/disable/edit preservation in interactive and headless creates, both README paths, adoption, missing-file choices, companion discovery, skill deletion preserving undeclared files, confirmation, cancellation, and buildability +- [ ] 12.8 Verify: Run focused scaffold, edit, create, TUI, integration, and end-to-end tests + +## 13. Documentation and Release Readiness — Research + +- [ ] 13.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 +- [ ] 13.2 Explore: Inspect documentation generation and shared snippets so field descriptions and compatibility values are referenced from authoritative schemas or constants rather than duplicated +- [ ] 13.3 Explore: Inspect linked-package changeset policy, current package versions, and release-note requirements for pre-1.0 breaking minor releases +- [ ] 13.4 Propose: Define the documentation, generated-reference, compatibility-warning, and release-note update set, including the custom-adapter contract page omitted from the original migration list + +## 14. Documentation and Release Readiness — Implementation + +- [ ] 14.1 Implement: Update archive, build, manifest, integrity, lockfile, commit, install, publish, and terminology documentation for supplementary membership, strict versions, path safety, per-file hashes, and atomic skill bundles +- [ ] 14.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, and non-asset files, and update the adapter API version-negotiation docs and adapter install/list surfaces from `0.0` to `0.1` (tagged contract, CLI-supported-API values, and reinstall guidance for old positional `0.0` adapters) +- [ ] 14.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 +- [ ] 14.4 Implement: Add linked minor-release changeset metadata and release notes identifying the previously accepted archive, lockfile, naming, and adapter behaviors that become incompatible — including the adapter API `0.0`→`0.1` cutover for the SDK and three first-party adapters — while intentionally omitting an `agent-facets` CLI changeset from this stack so the CLI release requiring `0.1` follows in a second cycle gated on all three first-party adapters publishing `facetAdapterApiVersion: 0.1`, 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 +- [ ] 14.5 Verify: Run documentation checks, 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 and confirm the consumer-first bridge and external registry gate precede `0.2` producer enablement