diff --git a/CannedPromptFormat-v0.1.md b/CannedPromptFormat.md similarity index 87% rename from CannedPromptFormat-v0.1.md rename to CannedPromptFormat.md index 44e5f29..802e6b9 100644 --- a/CannedPromptFormat-v0.1.md +++ b/CannedPromptFormat.md @@ -1,8 +1,10 @@ -# Canned Prompt Format v0.1 +# Canned Prompt Format -**Status:** Seed specification / experimental +**Revision:** v0.2 — packages declare `format: canned-prompt/v0.2` +**Status:** Experimental **Project:** `canned-prompts` -**Purpose:** Portable packaging of reusable prompts and prompt templates. +**Purpose:** Portable packaging of reusable prompts and prompt templates. +**Compatibility:** packages declaring `canned-prompt/v0.1` remain valid (§ 3.2). ## 1. Goals @@ -18,7 +20,7 @@ A conforming package should be: - versionable; - extensible with examples, evals, dependencies, and provenance. -CPF v0.1 specifies the **artifact format**. It intentionally does not specify model execution, agent orchestration, dependency resolution, registry transport, or evaluation engines. +CPF specifies the **artifact format**. It intentionally does not specify model execution, agent orchestration, dependency resolution, registry transport, or evaluation engines. ## 2. Package layout @@ -78,7 +80,7 @@ The canonical manifest is UTF-8 YAML named `prompt.yaml`. ### 3.1 Minimal manifest ```yaml -format: canned-prompt/v0.1 +format: canned-prompt/v0.2 id: review/code-review name: Code Review version: 1.0.0 @@ -99,13 +101,20 @@ Required fields are: #### `format` -MUST be exactly: +The current revision is: ```yaml -format: canned-prompt/v0.1 +format: canned-prompt/v0.2 ``` -for this specification. +A package declaring `canned-prompt/v0.1` remains valid and MUST still be +accepted. Everything v0.2 adds is additive — input defaults, composition, the +eval schema, registry manifests — so a v0.1 package means exactly what it +always meant. By § 17's own guidance this is a MINOR revision: new capability, +unchanged contract. + +A consumer MUST reject a `format` it does not recognize rather than guessing at +its meaning. #### `id` @@ -125,7 +134,7 @@ engineering/architecture-review practice/pqrst-estimate ``` -Rules for v0.1: +Rules: - lowercase ASCII is RECOMMENDED; - `/`, `-`, `_`, and `.` MAY be used; @@ -138,7 +147,7 @@ Rules for v0.1: ##### Identity is registry-scoped An `id` names a package **within a registry**, the way a path names a file -within a repository. CPF v0.1 does **not** claim that an id is globally unique: +within a repository. CPF does **not** claim that an id is globally unique: `practice/pqrst-estimate` obtained from two different registries may be two different packages, and a consumer that draws on more than one registry MUST keep track of which registry each package came from. @@ -200,10 +209,10 @@ rendered on its own; the field records the author's intent so that a consumer can warn when a package is used against it — rendering a fragment as a standalone prompt, or including a whole template where a fragment was meant. -## 4. Complete v0.1 manifest surface +## 4. Complete manifest surface ```yaml -format: canned-prompt/v0.1 +format: canned-prompt/v0.2 id: review/code-review name: Code Review version: 1.2.0 @@ -285,7 +294,7 @@ All fields other than the required fields in section 3.1 are optional. ## 5. Prompt template syntax -CPF v0.1 uses deliberately small placeholder semantics: +CPF uses deliberately small placeholder semantics: ```text {{ variable_name }} @@ -337,7 +346,7 @@ always produce the same output. A consumer that satisfies a derived default **Rendering rules:** -8. Values are substituted as text in v0.1. +8. Values are substituted as text. 9. A placeholder with no resolved value is an error. 10. Template evaluation MUST NOT execute arbitrary code. 11. Rendering MUST NOT derive values. A tool offering derivation MUST perform @@ -350,7 +359,7 @@ static defaults only. Such a tool is conforming: it reports an input with an unsatisfied derived default and no static fallback as unresolved, which rule 9 makes an error. -CPF v0.1 does not define conditionals, loops, filters, or functions. Implementations MAY offer richer rendering modes only when explicitly declared by an extension; they MUST NOT silently reinterpret a v0.1 template as executable code. +CPF does not define conditionals, loops, filters, or functions. Implementations MAY offer richer rendering modes only when explicitly declared by an extension; they MUST NOT silently reinterpret a template as executable code. ## 6. Inputs @@ -374,7 +383,7 @@ Fields: | `description` | no | Human-readable explanation | | `default` | no | Value used when the caller supplies none; see § 6.1 | -Recommended v0.1 input types are: +Recommended input types are: - `content` - `text` @@ -382,7 +391,7 @@ Recommended v0.1 input types are: - `path` - `json` -These are descriptive hints in v0.1. A runtime MAY use them for validation or adapters. +These are descriptive hints. A runtime MAY use them for validation or adapters. ### 6.1 Input defaults @@ -674,7 +683,7 @@ Recommended requirement values: - `optional` - `generate` -`generate` means that a resolver MAY satisfy a missing dependency by invoking an appropriate generation process. **CPF v0.1 does not define how generation or dependency resolution works.** +`generate` means that a resolver MAY satisfy a missing dependency by invoking an appropriate generation process. **CPF does not define how generation or dependency resolution works.** ### 10.3 Naming and pinning a dependency @@ -851,7 +860,7 @@ provenance: version: 1.1.0 ``` -The field is descriptive in v0.1. Registries SHOULD preserve provenance when publishing or mirroring packages. +The field is descriptive. Registries SHOULD preserve provenance when publishing or mirroring packages. ## 14. Licensing @@ -923,11 +932,11 @@ This guidance is intentionally advisory because prompt behavior is probabilistic ## 18. Package validation -A v0.1 validator SHOULD verify at least: +A validator SHOULD verify at least: 1. `prompt.yaml` exists and parses as YAML; 2. required manifest fields exist; -3. `format == canned-prompt/v0.1`; +3. `format` names a recognized revision (§ 3.2); 4. `id` is non-empty and contains no path traversal; 5. `version` is non-empty; 6. `template` resolves to a regular file inside the package; @@ -991,7 +1000,7 @@ Implementations SHOULD: ## 20. Registry model -CPF v0.1 does not mandate registry transport. +CPF does not mandate registry transport. A valid registry may be: @@ -1098,7 +1107,7 @@ registry, an id is unambiguous by definition. ## 21. Reference CLI semantics -The v0.1 reference tool uses two stores: +The reference tool uses two stores: - **catalog** — packages locally available for search/show/render; - **registry** — packages available for publish/install. @@ -1143,17 +1152,32 @@ These semantics are illustrative, not mandatory for other implementations. ## 22. Worked example +This is `examples/pqrst-estimate` in the `canned-prompts` repository, abridged. +It composes a shared style fragment, so it also demonstrates § 10.4. + `prompt.yaml`: ```yaml -format: canned-prompt/v0.1 +format: canned-prompt/v0.2 id: practice/pqrst-estimate name: PQRST Estimate -version: 0.1.0 +version: 0.2.1 summary: Estimate how session effort was distributed across PQRST categories. template: prompt.md +dependencies: + prompts: + - id: practice/house-style + version: ">= 0.1.0" + requirement: required + inputs: + - name: house_style + type: content + required: false + default: + include: practice/house-style + - name: session_summary type: content required: true @@ -1166,12 +1190,22 @@ parameters: output: format: markdown +compatibility: + capabilities: [long-context] + +license: MIT + +examples: [examples/basic.yaml] +evals: [evals/quality.yaml] + tags: [retrospective, agentic-coding, pqrst] ``` `prompt.md`: ```markdown +{{ house_style }} + Review the following coding-session summary and estimate the distribution of session effort across PQRST. Percentages must sum to 100%. @@ -1188,21 +1222,60 @@ Session: Include rationale: {{ include_rationale }} ``` -## 23. Open questions for v0.2+ +## 23. Open questions -Experience should determine whether later revisions standardize: +### Settled since v0.1 + +`CANP-WP-0002` promoted five items from "experience will decide" to decisions. +Each is now specified. This table records where the rule lives and what was +chosen, so a later reader can find the reasoning and not just the rule. + +| Question | Decision | Where | +|---|---|---| +| Optional inputs were unusable | `default` on inputs, static or derived; resolution separated from rendering | § 5.1, § 6.1 | +| Registry namespaces and ownership | Identity is registry-scoped; ownership is registry policy, never a package's own claim | § 3.2, § 20.1, § 20.2 | +| Prompt composition and inheritance | Composition by reference in two kinds, `include` and `derive`; no inheritance | § 10.3, § 10.4 | +| Canonical evaluation schemas | One schema, `canned-prompts/eval-rubric/v0.1`; render checks run, output criteria are declared | § 12.1 | +| Typed context and dependency contracts | `dependencies` means required, `compatibility` means observed; `context` names what CPF cannot package | § 9.1, § 10.1, § 10.2 | + +Two habits recur across those decisions, and later revisions should follow them +rather than rediscover them: + +**Separate the deterministic half from the rest.** `include` and `derive`, +render checks and output criteria, resolve and render. Each pair splits at the +same seam: what a tool can do with the package alone, and what needs a model. A +consumer that cannot call a model is never second-class — it does the +deterministic half completely and reports the rest honestly, rather than +guessing or silently producing something incomplete. + +**A package never asserts what it cannot back.** Ownership belongs to the +registry that admits a package, not to the package that would like to claim a +namespace. `compatibility` records observation, never permission. A derived +default is a request to a consumer, not an instruction the package executes. +This is why § 19 can treat a package as content rather than as an actor. + +### Still deferred + +Experience should still determine whether later revisions standardize: -- typed context/dependency contracts; - content macros; -- prompt composition and inheritance; -- registry namespaces and ownership; -- cryptographic integrity/signing; -- canonical evaluation schemas; -- model capability vocabularies; +- cryptographic integrity and signing; +- model capability vocabularies — § 10.2 keeps capability names free-form, + validated for shape and not for membership; - run manifests and evidence formats; - deterministic compilation manifests; -- trust/reputation signals; +- trust and reputation signals; - federated discovery; -- richer template syntax. +- richer template syntax; +- pattern-matching render checks — § 12.1 stops at `contains`, `not_contains` + and `resolves_all` rather than adding a matching language to a format whose + § 5 rests on not having one. -Until practical usage forces these decisions, v0.1 should remain intentionally small. +### Decided against + +Template inheritance (§ 10.4) is not deferred; it was considered and refused. +Reopening it means overturning a decision, not filling a gap, and would have to +answer the four objections recorded there. + +Until practical usage forces these questions, the format should remain +intentionally small. diff --git a/README.md b/README.md index bc9cc1b..71bce90 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This bundle contains a first project seed for `canned-prompts`: - [`INTENT.md`](INTENT.md) — project mission, boundaries, principles, and success criteria. -- [`CannedPromptFormat-v0.1.md`](CannedPromptFormat-v0.1.md) — experimental package-format specification. +- [`CannedPromptFormat.md`](CannedPromptFormat.md) — the package-format specification, currently revision v0.2. - [`reference/`](reference/) — deliberately small Python CLI implementing the basic lifecycle. - [`examples/pqrst-estimate/`](examples/pqrst-estimate/) — a real package that can be used to exercise the implementation. - [`examples/house-style/`](examples/house-style/) — a `type: fragment` package that `pqrst-estimate` composes. @@ -67,7 +67,7 @@ or command-level `--catalog` / `--registry` options. ## Resolution vs rendering -The format separates the two steps (`CannedPromptFormat-v0.1.md` § 5.1). +The format separates the two steps (`CannedPromptFormat.md` § 5.1). Resolution decides a value for every input and parameter and may be non-deterministic; rendering substitutes those values and always is. An input may declare a default that is either a static value or a *derived* one — a @@ -165,7 +165,7 @@ requires (this tool cannot verify these): ## Registries and identity -An id names a package *within a registry* (`CannedPromptFormat-v0.1.md` +An id names a package *within a registry* (`CannedPromptFormat.md` § 3.2). The same id obtained from two registries may be two different packages, so the catalog keeps them apart and a bare id that matches more than one is reported as ambiguous rather than guessed. Qualify it when you need to: diff --git a/SCOPE.md b/SCOPE.md index 7e9503f..4443956 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -14,7 +14,7 @@ evidence, and is inspectable before use. See `INTENT.md`. ## In Scope -- `CannedPromptFormat-v0.1.md` — the package-format specification. +- `CannedPromptFormat.md` — the package-format specification (revision v0.2). - `reference/` — a deliberately small Python CLI (`add`, `search`, `show`, `render`, `install`, `publish`) over a filesystem catalog and registry. - `examples/` — real packages that exercise the format and the implementation. @@ -36,12 +36,13 @@ Thin execution adapters are permitted; a general runtime is not. ## Current State -- **Seed / experimental.** Spec v0.1 is written; the reference CLI implements the - full local lifecycle and its 3 tests pass. One example package - (`examples/pqrst-estimate`) round-trips add → search → show → render → publish - → install. -- The reference implementation is spec-conformant on the points it covers; the - known open design questions are tracked in `workplans/CANP-WP-0002-*.md`. +- **Experimental, revision v0.2.** The five open questions promoted from v0.1 + are resolved and specified (`CANP-WP-0002`); what remains deferred is listed + in § 23. Packages declaring `canned-prompt/v0.1` remain valid. +- The reference CLI implements the full local lifecycle across eight verbs and + its 81 tests pass. Two example packages (`practice/pqrst-estimate` and the + `practice/house-style` fragment it composes) round-trip add → search → show → + resolve → render → eval → publish → install. ## Developer Workflow @@ -50,7 +51,7 @@ cd reference python3 -m venv .venv && . .venv/bin/activate pip install -r requirements-dev.txt -python3 -m pytest -q # tests +python3 -m pytest -q # 81 tests CANNED_PROMPTS_HOME=/tmp/cp \ python3 canned_prompts.py add ../examples/pqrst-estimate # smoke ``` @@ -61,6 +62,6 @@ There is no lint or build step yet; `pyproject.toml` declares the ## Getting Oriented - Intent and principles: `INTENT.md` -- Format specification: `CannedPromptFormat-v0.1.md` +- Format specification: `CannedPromptFormat.md` - Agent instructions: `AGENTS.md` - Workplans: `workplans/` diff --git a/examples/house-style/README.md b/examples/house-style/README.md index 3af72b2..ebf3360 100644 --- a/examples/house-style/README.md +++ b/examples/house-style/README.md @@ -21,7 +21,7 @@ inputs: Inclusion is deterministic — the fragment's template is rendered and inlined as text, with no model involved — so the reference CLI performs it during -`resolve`. See `CannedPromptFormat-v0.1.md` § 10.4. +`resolve`. See `CannedPromptFormat.md` § 10.4. Its `tone` parameter is inherited from the including package when that package declares one, and otherwise falls back to the default here. diff --git a/examples/house-style/prompt.yaml b/examples/house-style/prompt.yaml index a55d391..533e6e8 100644 --- a/examples/house-style/prompt.yaml +++ b/examples/house-style/prompt.yaml @@ -1,7 +1,7 @@ -format: canned-prompt/v0.1 +format: canned-prompt/v0.2 id: practice/house-style name: House Style -version: 0.1.0 +version: 0.1.1 summary: > Shared style block for review and estimate prompts: evidence first, no filler, state uncertainty plainly. diff --git a/examples/pqrst-estimate/prompt.yaml b/examples/pqrst-estimate/prompt.yaml index 5e06966..9f08670 100644 --- a/examples/pqrst-estimate/prompt.yaml +++ b/examples/pqrst-estimate/prompt.yaml @@ -1,7 +1,7 @@ -format: canned-prompt/v0.1 +format: canned-prompt/v0.2 id: practice/pqrst-estimate name: PQRST Estimate -version: 0.2.0 +version: 0.2.1 summary: 'Produce a post-session estimate of effort distributed across the PQRST categories for an agentic coding session. diff --git a/reference/canned_prompts.py b/reference/canned_prompts.py index 8bb0eb7..410e75b 100755 --- a/reference/canned_prompts.py +++ b/reference/canned_prompts.py @@ -19,7 +19,9 @@ from typing import Any, Iterable import yaml -FORMAT = "canned-prompt/v0.1" +FORMAT = "canned-prompt/v0.2" +# v0.2 is additive, so a v0.1 package means exactly what it always meant (§ 3.2). +ACCEPTED_FORMATS = ("canned-prompt/v0.2", "canned-prompt/v0.1") REGISTRY_FORMAT = "canned-prompt-registry/v0.1" LOCAL_REGISTRY = "local" REGISTRY_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") @@ -324,8 +326,11 @@ def validate_package(package_dir: Path) -> dict[str, Any]: if missing: raise CannedPromptError("missing required fields: " + ", ".join(missing)) - if manifest["format"] != FORMAT: - raise CannedPromptError(f"unsupported format: {manifest['format']!r}") + if manifest["format"] not in ACCEPTED_FORMATS: + raise CannedPromptError( + f"unsupported format: {manifest['format']!r}; expected one of " + + ", ".join(ACCEPTED_FORMATS) + ) package_id = manifest["id"] if not isinstance(package_id, str) or not package_id.strip(): diff --git a/reference/tests/test_canned_prompts.py b/reference/tests/test_canned_prompts.py index ab0c67a..6a4a26d 100644 --- a/reference/tests/test_canned_prompts.py +++ b/reference/tests/test_canned_prompts.py @@ -762,3 +762,26 @@ def test_copy_refuses_to_overwrite(tmp_path: Path) -> None: cp.copy_package(src, dst, manifest, "package") with pytest.raises(cp.CannedPromptError, match="already exists"): cp.copy_package(src, dst, manifest, "package") + + +# --- format revisions (§ 3.2) --- + +@pytest.mark.parametrize("declared", ["canned-prompt/v0.2", "canned-prompt/v0.1"]) +def test_both_format_revisions_are_accepted(tmp_path: Path, declared) -> None: + """v0.2 is additive, so a v0.1 package means exactly what it always meant.""" + pkg = write_pkg( + tmp_path / "p", + BASE.replace("canned-prompt/v0.1", declared) + + "inputs:\n - name: greeting\n required: false\n default: hi\n", + ) + assert cp.validate_package(pkg)["format"] == declared + + +def test_unknown_format_is_rejected(tmp_path: Path) -> None: + pkg = write_pkg( + tmp_path / "p", + BASE.replace("canned-prompt/v0.1", "canned-prompt/v9.9") + + "inputs:\n - name: greeting\n required: false\n default: hi\n", + ) + with pytest.raises(cp.CannedPromptError, match="unsupported format"): + cp.validate_package(pkg) diff --git a/workplans/CANP-WP-0002-format-open-questions.md b/workplans/CANP-WP-0002-format-open-questions.md index 85b6943..66208ad 100644 --- a/workplans/CANP-WP-0002-format-open-questions.md +++ b/workplans/CANP-WP-0002-format-open-questions.md @@ -4,7 +4,7 @@ type: workplan title: "Resolve CPF v0.1 open questions promoted for v0.2" domain: agents repo: canned-prompts -status: proposed +status: finished owner: codex topic_slug: practice created: "2026-09-06" @@ -13,7 +13,7 @@ reviewed_at: "2026-09-06" reviewed_by: "claude" context_paths: - "INTENT.md" - - "CannedPromptFormat-v0.1.md" + - "CannedPromptFormat.md" - "reference/canned_prompts.py" - "examples/pqrst-estimate/" state_hub_workstream_id: "c87c8e27-8b11-5306-b687-86779ef3f1be" @@ -21,7 +21,7 @@ state_hub_workstream_id: "c87c8e27-8b11-5306-b687-86779ef3f1be" # Resolve CPF v0.1 open questions promoted for v0.2 -`CannedPromptFormat-v0.1.md` § 23 currently lists twelve items as "experience +`CannedPromptFormat.md` § 23 currently lists twelve items as "experience should determine". A review of the seed on 2026-09-06 (spec + reference CLI + `examples/pqrst-estimate`; 3/3 tests pass, full local lifecycle smokes clean) plus an operator interview promoted five of them from "wait and see" to @@ -36,6 +36,11 @@ federated discovery, richer template syntax. format may *declare* a requirement; it must not specify the resolver, runtime, or execution engine that satisfies it. +**Outcome.** All seven tasks are done. The format is now revision v0.2 +(`CannedPromptFormat.md`), the reference CLI covers eight verbs with 81 passing +tests, and § 23 records what was settled, what stays deferred, and what was +decided against. One residual is carried forward as `CANP-WP-0003`. + ## Optional inputs need defaults ```task @@ -373,16 +378,48 @@ what it genuinely has. Illustrations live in the spec; examples stay honest. ```task id: CANP-WP-0002-T06 -status: todo +status: done priority: medium state_hub_task_id: "06032689-fc19-5fcd-a76e-5dbc87d02cd4" ``` -After T01–T05 land, replace § 23's flat twelve-item list with two sections: -questions **being decided for v0.2** (each with its scoped question and stated -leaning, referencing this workplan) and questions **still deferred** (the eight -unpromoted items listed at the top of this file). Bump the spec status line if -the format revision warrants it. +**Decisions (operator, 2026-09-06):** + +- *Bump to `canned-prompt/v0.2`, accept both.* Everything added is additive, so + a v0.1 package means exactly what it always meant — the MINOR case § 17 + itself describes. Tools accept both strings; new packages declare v0.2. +- *Drop the version from the spec filename.* `CannedPromptFormat-v0.1.md` → + `CannedPromptFormat.md`, with the revision stated in the document. One stable + path that never breaks a link, and no rename per revision; the version lives + in the `format` string, where tools actually read it. + +Delivered: + +1. § 23 rewritten into three parts rather than the planned two. **Settled since + v0.1** tables the five resolved questions against where each rule now lives. + **Still deferred** carries the eight unpromoted items plus pattern-matching + render checks, deferred during T04. **Decided against** holds template + inheritance on its own, because "deferred" would misdescribe it: reopening + it means overturning a decision and answering four recorded objections, not + filling a gap. +2. § 23 also names the two habits the five decisions share, so later revisions + follow them rather than rediscover them: *separate the deterministic half + from the rest*, and *a package never asserts what it cannot back*. +3. Header now carries revision, status and a compatibility line; § 3.2 states + the acceptance rule and why v0.2 is MINOR; § 18 rule 3 checks for a + recognized revision rather than one literal string. +4. Stale "CPF v0.1" phrasings throughout replaced with unversioned wording. The + `canned-prompts/eval-rubric/v0.1` and `canned-prompt-registry/v0.1` schemas + deliberately keep their own v0.1 — they are new in this revision and on + their own version lines. +5. `reference/canned_prompts.py`: `ACCEPTED_FORMATS`; unknown revisions are + rejected with the accepted list. Tests 78 → 81. +6. Example packages declare v0.2, bumped 0.1.0 → 0.1.1 and 0.2.0 → 0.2.1 as + § 17 PATCH — a metadata correction with intended behavior unchanged. + +**Also fixed.** § 22's worked example had drifted: it showed `pqrst-estimate` +at 0.1.0 with no composition, contradicting the package actually in the repo. +It now mirrors the real package and doubles as a § 10.4 illustration. ## Reference implementation conformance fixes diff --git a/workplans/CANP-WP-0003-registry-naming-residual.md b/workplans/CANP-WP-0003-registry-naming-residual.md new file mode 100644 index 0000000..4f99b17 --- /dev/null +++ b/workplans/CANP-WP-0003-registry-naming-residual.md @@ -0,0 +1,54 @@ +--- +id: CANP-WP-0003 +type: workplan +title: "Default registry name reads as 'registry:'" +domain: agents +repo: canned-prompts +status: proposed +owner: codex +topic_slug: practice +created: "2026-09-06" +updated: "2026-09-06" +--- + +# Default registry name reads as `registry:` + +Residual from `CANP-WP-0002` (`origin: residual`, `origin_ref: CANP-WP-0002`). + +## Give the default registry a better name + +```task +id: CANP-WP-0003-T01 +status: todo +priority: low +``` + +`CANP-WP-0002-T02` made a registry's name come from its `registry.yaml`, and +otherwise from its directory basename (§ 20.1). The reference tool's default +registry lives at `~/.canned-prompts/registry`, so its basename-derived name is +`registry`, and every install from it reads: + +```text +installed registry:practice/pqrst-estimate@0.1.0 +``` + +That is honest — it is genuinely what the directory is called — but it reads +poorly, and `registry:` is a confusing qualifier for the one registry most +users will ever see. + +Deliberately not fixed in `CANP-WP-0002`: special-casing the default path to +produce a prettier name would be exactly the kind of magic the § 20.1 rule was +written to avoid, and it was not worth an exception mid-task. + +Options worth weighing: + +1. Write a starter `registry.yaml` when the default registry is first created, + naming it something like `default` or `local-registry`. Uses the documented + mechanism rather than bypassing it; the cost is a tool writing a file into + a store the user may consider theirs. +2. Rename the default directory itself, so the basename is already good. + Changes an existing on-disk layout. +3. Accept it and document it. Zero code, and the escape hatch — add a + `registry.yaml` — already exists and is documented. + +Option 3 is the current state. Pick deliberately rather than by default.