T01 had already delivered half of composition without naming it: a derived default binds an input to a prompt dependency, which is transclusion — run package B, use its output. What was missing was the deterministic half. `include` inlines another package's rendered template as text. No model is involved, so the reference CLI can actually perform it, and a shared preamble, rubric or style block becomes a versioned package instead of copied text. This is the concrete way to honor INTENT principle 9 without any runtime. `derive` stays as it was. Both are input defaults, so composition reuses the resolution machinery rather than adding a second one. No template inheritance. Four of this repo's own documents argue against it: INTENT principle 3 (hidden context defeats reuse), section 19's "make package contents visible before execution", section 17's requirement that behavior changes produce a new version, and the non-goal on range resolution. Version selectors: an exact pin is the expected form, with `any`, `newest` and `>= X.Y.Z` as explicit opt-ins so looseness is written rather than implied by absence. Selectors are evaluated per dependency against what is available — no solver, no cross-dependency constraint satisfaction — which is what keeps them outside the range-resolution non-goal, and the spec says so. Also defines `type` (template | fragment), which appeared once in the section 4 manifest surface and was specified nowhere. Spec: 3.2 (type), 5.1 (inclusion resolution rule, renumbered), 6.1 (included default), 10.1 and 10.2 (new), 18 (rules 14-16), 21. Reference CLI: validate_version_selector, select_version, prompt_dependencies replacing prompt_dependency_ids, check_composition_reference, CatalogComposer with cycle detection, and resolve_inputs gaining composer= and inherited=. Tests 21 -> 42. Examples: house-style is a real fragment package; pqrst-estimate composes it and is bumped 0.1.0 -> 0.2.0 per section 17. Fixes an ordering bug found while testing: inputs resolved before parameters, so an included package could not see the including package's parameters and silently fell back to its own defaults — the fragment rendered tone=neutral where the including package said blunt. Parameters now resolve first; the report still lists inputs first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502 |
||
|---|---|---|
| .. | ||
| tests | ||
| canned_prompts.py | ||
| pyproject.toml | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
canned-prompts reference CLI
This is intentionally a small reference implementation, not the intended final architecture.
It demonstrates seven verbs:
add PATH
search QUERY
show ID
resolve ID --set key=value
render ID --set key=value
install ID [--version VERSION]
publish PATH
The implementation uses a local catalog plus a filesystem registry and performs no model calls.
resolve and render are separate because the specification separates them
(§ 5.1): resolution decides each value and may be non-deterministic, rendering
substitutes and always is. resolve prints where every value came from —
supplied, default, or fallback — before any prompt is produced.
Stores
Default locations:
~/.canned-prompts/catalog
~/.canned-prompts/registry
A registry stores packages flat, because an id is unambiguous within one registry:
<registry>/<id path>/<version>/...
A catalog is namespaced by registry, because identity is registry-scoped (§ 3.2) and the same id may be installed from more than one place:
<catalog>/<registry name>/<id path>/<version>/...
For example:
~/.canned-prompts/catalog/house/practice/pqrst-estimate/0.1.0/
~/.canned-prompts/catalog/local/practice/pqrst-estimate/0.1.0/
A registry's name comes from its optional registry.yaml, and otherwise from
its directory basename. add takes a package from a path rather than a
registry, so it files it under local (override with --as).
Commands that take an ID accept a bare id or a qualified <registry>:<id>.
A bare id installed from more than one registry is reported as ambiguous
rather than resolved by guessing.
Design choices
- YAML manifest via PyYAML.
{{ name }}template substitution only.- No arbitrary expression/code execution.
- Published versions are immutable by default.
installcopies from registry to catalog.addcopies a package directly to catalog.search,show,resolve, andrenderoperate on catalog packages, and print qualified<registry>:<id>references.includedefaults are satisfied (inclusion is deterministic);derivedefaults are reported, not run. Inclusion cycles are detected and named.- An optional
registry.yamlnames a registry and records namespace claims.publishwarns when a namespace is declaredclosed— it cannot authenticate a publisher, and says so rather than implying it checked. - Static input defaults are applied; derived defaults (§ 6.1) are not. This
tool never calls a model, so a derived default is satisfied only by its
static fallback
value. Without one,resolvereports the input as unresolved andrenderrefuses rather than substituting empty text.
Use this implementation to challenge the format. Replace it once real usage reveals the right architecture.