Register with Custodian State Hub and seed format open-questions workplan

Register canned-prompts under agents / practice (topic
c1d199b6-55ee-4db6-b49e-257a9f0f15ac, workplan prefix CANP-WP) via
`statehub register`, then replace the generated placeholders with
repo-specific facts.

- SCOPE.md: real boundaries drawn from INTENT.md's deliberate boundary,
  current state (spec v0.1 + reference CLI, 3/3 tests pass, example
  round-trips), and the developer workflow.
- AGENTS.md: drop the unresolved {CREDENTIAL_ROUTING} template token left
  by the generator.
- CANP-WP-0001: bootstrap tasks closed.
- CANP-WP-0002: new workplan carrying the five § 23 open questions promoted
  from "experience will decide" to "decide for v0.2" — optional-input
  defaults (static or derived), registry namespaces/ownership, prompt
  composition, canonical eval schemas, typed context/dependency contracts —
  plus two reference-implementation conformance defects found in review
  (prerelease versions sort as newest; copy_immutable packages the whole
  source directory).

Also lands the previously untracked seed: INTENT.md, the CPF v0.1 spec,
the reference CLI, and examples/pqrst-estimate.

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
This commit is contained in:
tegwick 2026-09-06 00:45:23 +02:00
parent 985b41dc87
commit dc615ef530
19 changed files with 2130 additions and 2 deletions

27
.custodian-brief.md Normal file
View file

@ -0,0 +1,27 @@
<!-- custodian-brief: generated by statehub register; fix-consistency may replace this file -->
# Custodian Brief - canned-prompts
**Project:** canned-prompts
**Domain:** agents
**State Hub:** http://127.0.0.1:8000
**Topic ID:** `c1d199b6-55ee-4db6-b49e-257a9f0f15ac`
## Open Workplans
### Bootstrap State Hub integration
Workplan file: `workplans/CANP-WP-0001-statehub-bootstrap.md`
Open tasks:
- T01 - Review generated integration files
- T02 - Verify local developer workflow
- T03 - Seed first real workplan
## Session Start
1. Read `INTENT.md`, `SCOPE.md`, and `AGENTS.md`.
2. Check inbox: `GET /messages/?to_agent=canned-prompts&unread_only=true`.
3. Scan `workplans/`.
4. Update task statuses in workplan files as work progresses.
Last generated: 2026-09-06

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# state-hub: track .claude/rules
# Claude Code local state (track shared rules; ignore machine-specific files)
.claude/*
!.claude/rules/
!.claude/rules/*.md
# Python
__pycache__/
*.py[cod]
.venv/

203
AGENTS.md Normal file
View file

@ -0,0 +1,203 @@
# canned-prompts — Agent Instructions
## Repo Identity
**Purpose:** Portable package format, spec and reference CLI for reusable prompt artifacts (Canned Prompt Format v0.1).
**Domain:** agents
**Repo slug:** canned-prompts
**Topic ID:** `c1d199b6-55ee-4db6-b49e-257a9f0f15ac`
**Workplan prefix:** `CANP-WP-`
---
## State Hub Integration
The Custodian State Hub tracks work across all domains. Codex uses HTTP REST and
the `statehub` CLI by default. MCP is opt-in because the current Codex MCP bridge
adds severe call latency; the full administrative MCP surface remains available
to clients that need it.
| Context | URL |
|---------|-----|
| Local workstation | `http://127.0.0.1:8000` |
| Remote via tunnel | `http://127.0.0.1:18000` |
| Optional local edge relay | http://127.0.0.1:18080 |
When an operator has enabled the edge relay, set API_BASE to the relay URL.
Queueable writes return an explicit queued receipt if the central hub is
unreachable. Treat that as pending local evidence, then ask the operator to run
statehub outbox status/replay after connectivity returns.
Codex workspace-write sandboxes need network access enabled to reach the host's
loopback listener. Bootstrap this once with `make -C ~/state-hub configure-codex`
and restart Codex. The canonical REST health endpoint is `/state/health`, not
`/health`. If a sandboxed loopback probe fails, retry it with escalated execution
before declaring State Hub unavailable; a managed Codex permission profile may
still enforce isolated networking. Experimental MCP can be enabled explicitly
with `make -C ~/state-hub configure-codex WITH_MCP=1`.
### Orient at session start
```bash
# Offline brief — works without hub connection
cat .custodian-brief.md
# Active workplans for this domain
curl -s "http://127.0.0.1:8000/workplans/?topic_id=c1d199b6-55ee-4db6-b49e-257a9f0f15ac&status=active" \
| python3 -m json.tool
# Check inbox
curl -s "http://127.0.0.1:8000/messages/?to_agent=canned-prompts&unread_only=true" \
| python3 -m json.tool
```
Mark a message read:
```bash
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
-H "Content-Type: application/json" -d '{}'
```
### Log progress (required at session close)
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{
"summary": "what was done",
"event_type": "note",
"author": "codex",
"workplan_id": "<uuid>",
"task_id": "<uuid>"
}'
```
Omit `workplan_id` / `task_id` when not applicable.
### Update task status
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"status": "progress"}'
# values: wait | todo | progress | done | cancel
```
### Flag a task for human review
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-H "Content-Type: application/json" \
-d '{"needs_human": true, "intervention_note": "reason"}'
```
---
## Session Protocol
**Start:**
1. `cat .custodian-brief.md` — domain goal and open workplans (offline-safe)
2. Check inbox: `GET /messages/?to_agent=canned-prompts&unread_only=true`; mark read
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
**During work:**
- Update task statuses in workplan files as tasks progress
- Record significant decisions via `POST /decisions/`
**Close:**
1. Update workplan file task statuses to reflect progress
2. If finishing a workplan: hand off **residuals** as live work records first
(intake with `origin: residual` + `origin_ref: <WP-id>`, or a next workplan /
decision / engagement). Do not park leftovers only in prose or `SCOPE.md`.
Canon: `the-custodian/canon/standards/work-record-types_v0.1.md` § Residuals.
3. Log: `POST /progress/` with a summary of what changed (name handoff ids)
4. After workplan file changes, run:
```bash
uv run --project ~/repo-manager rmgr sync --path . --push
```
This assigns only missing deterministic identifiers, verifies the pushed
Forgejo commit and `primary/railliance01`, then requests one central
reconciliation. A queued receipt is pending evidence; rerun after
connectivity returns. Use `statehub fix-consistency` for a separate deep audit.
---
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
---
## Workplan Convention (ADR-001)
Work items originate as files in this repo — not in the hub. The hub is a
read/cache/index layer that rebuilds from files.
**File location:** `workplans/CANP-WP-NNNN-<slug>.md`
**Archived location:** finished workplans may move to
`workplans/archived/YYMMDD-CANP-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
the completion/archive date; the frontmatter `id` does not change.
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
`workplans/ADHOC-YYYY-MM-DD.md`, workplan id
`CANP-WP-ADHOC-YYYY-MM-DD`, and task ids
`CANP-WP-ADHOC-YYYY-MM-DD-T01`, etc. `CANP-WP` includes its final `-WP`
token. Unqualified historic `ADHOC-*` ids are grandfathered and must not be
copied into new records. Use this only for low-risk work completed directly;
create a normal workplan for anything needing analysis, design, approval,
dependencies, or multiple phases.
**Frontmatter:**
```yaml
---
id: CANP-WP-NNNN
type: workplan
title: "..."
domain: agents
repo: canned-prompts
status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex
topic_slug: ...
created: "YYYY-MM-DD"
updated: "YYYY-MM-DD"
state_hub_workstream_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
---
```
Use `proposed` for a new draft, `ready` after review against current repo
state, and `finished` after implementation. `stalled` and `needs_review` are
derived health labels, not frontmatter statuses.
**Terminology:** workplan is the fleet term; `workstream` appears only in legacy
API/MCP/frontmatter bridges until `STATE-WP-0069` retires them — see
`the-custodian/canon/standards/workplan-terminology-fleet_v0.1.md`.
**Task block format** (one per `##` section):
```
## Task Title
` ` `task
id: CANP-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
` ` `
Task description text.
```
Status progression: `todo``progress``done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
**Residuals when finishing:** actionable leftovers become live work records
before `status: finished` — usually an intake (`origin: residual`,
`origin_ref: CANP-WP-NNNN`) or a spawned workplan. Residual is a *role*,
not a kind. Fleet list lives on State Hub, not in `SCOPE.md`.
To create a new workplan:
1. Write the file following the format above
2. Run `uv run --project ~/repo-manager rmgr sync --path . --push`.
3. Run `statehub fix-consistency` only when a separate deep audit is needed.

640
CannedPromptFormat-v0.1.md Normal file
View file

@ -0,0 +1,640 @@
# Canned Prompt Format v0.1
**Status:** Seed specification / experimental
**Project:** `canned-prompts`
**Purpose:** Portable packaging of reusable prompts and prompt templates.
## 1. Goals
The Canned Prompt Format (CPF) defines the smallest practical contract for a reusable prompt artifact.
A conforming package should be:
- human-readable;
- filesystem-portable;
- provider-neutral;
- inspectable before use;
- parameterizable where useful;
- 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.
## 2. Package layout
The minimum valid package is:
```text
my-prompt/
├── prompt.yaml
└── prompt.md
```
A richer package may contain:
```text
my-prompt/
├── prompt.yaml
├── prompt.md
├── README.md
├── examples/
│ ├── basic.yaml
│ └── edge-case.yaml
├── evals/
│ └── quality.yaml
└── assets/
└── rubric.md
```
### Reserved paths
| Path | Meaning |
|---|---|
| `prompt.yaml` | Required package manifest |
| `prompt.md` | Default prompt template unless overridden by `template` |
| `README.md` | Optional human documentation |
| `examples/` | Optional examples/fixtures |
| `evals/` | Optional evaluation specifications |
| `assets/` | Optional supporting text/data artifacts |
Tools MUST ignore unknown non-reserved files unless a manifest field explicitly references them.
## 3. Manifest
The canonical manifest is UTF-8 YAML named `prompt.yaml`.
### 3.1 Minimal manifest
```yaml
format: canned-prompt/v0.1
id: review/code-review
name: Code Review
version: 1.0.0
summary: Review a change for correctness and maintainability.
template: prompt.md
```
Required fields are:
- `format`
- `id`
- `name`
- `version`
- `summary`
- `template`
### 3.2 Package identity
#### `format`
MUST be exactly:
```yaml
format: canned-prompt/v0.1
```
for this specification.
#### `id`
A stable, registry-independent logical identifier.
Recommended syntax:
```text
<namespace>/<name>
```
Examples:
```text
review/code-review
engineering/architecture-review
practice/pqrst-estimate
```
Rules for v0.1:
- lowercase ASCII is RECOMMENDED;
- `/`, `-`, `_`, and `.` MAY be used;
- whitespace MUST NOT be used;
- the ID MUST NOT contain `..` path traversal segments;
- registry implementations MUST treat the ID as logical metadata rather than an unchecked filesystem path.
#### `name`
Human-readable display name.
#### `version`
A package version. Semantic Versioning (`MAJOR.MINOR.PATCH`) is RECOMMENDED and used by the reference implementation.
Behavior-changing edits SHOULD create a new version rather than overwrite a published package.
#### `summary`
A short description of the intended purpose. A consumer SHOULD be able to decide whether a package is potentially relevant from `name` + `summary` alone.
#### `template`
Relative path to the primary prompt template inside the package. The path MUST remain within the package directory.
## 4. Complete v0.1 manifest surface
```yaml
format: canned-prompt/v0.1
id: review/code-review
name: Code Review
version: 1.2.0
summary: >
Review a change for correctness, maintainability,
security and test coverage.
type: template
template: prompt.md
inputs:
- name: change
type: content
required: true
description: The code, diff, or change to review.
- name: repository_context
type: content
required: false
description: Optional surrounding repository context.
parameters:
depth:
type: enum
values: [quick, normal, thorough]
default: normal
description: Desired review depth.
include_security:
type: boolean
default: true
output:
format: markdown
description: A structured review with findings and recommendations.
compatibility:
capabilities:
- code-analysis
models: []
providers: []
dependencies:
prompts: []
context: []
capabilities: []
examples:
- examples/basic.yaml
evals:
- evals/review-quality.yaml
license: CC-BY-4.0
tags:
- code-review
- engineering
provenance:
author: Example Author
source: https://example.invalid/original
derived_from: []
extensions: {}
```
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:
```text
{{ variable_name }}
```
A placeholder name MUST correspond to either:
- a declared `input`, or
- a declared `parameter`.
Whitespace immediately inside `{{` and `}}` is insignificant.
Examples:
```markdown
Review the following change at {{ depth }} depth.
{{ change }}
```
### 5.1 Rendering rules
1. Call-supplied values override defaults.
2. A declared parameter default is used when no call value is supplied.
3. A required input without a value is an error.
4. A placeholder with no resolved value is an error.
5. Values are substituted as text in v0.1.
6. Template evaluation MUST NOT execute arbitrary code.
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.
## 6. Inputs
`inputs` is an optional ordered list.
```yaml
inputs:
- name: document
type: content
required: true
description: Document to summarize.
```
Fields:
| Field | Required | Meaning |
|---|---:|---|
| `name` | yes | Placeholder/input identifier |
| `type` | no | Suggested semantic type; defaults to `content` |
| `required` | no | Whether a caller must supply it; defaults to `false` |
| `description` | no | Human-readable explanation |
Recommended v0.1 input types are:
- `content`
- `text`
- `url`
- `path`
- `json`
These are descriptive hints in v0.1. A runtime MAY use them for validation or adapters.
## 7. Parameters
`parameters` is an optional mapping keyed by parameter name.
Supported descriptive parameter types:
- `string`
- `integer`
- `number`
- `boolean`
- `enum`
Example:
```yaml
parameters:
tone:
type: enum
values: [neutral, friendly, formal]
default: neutral
max_items:
type: integer
default: 10
```
A tool SHOULD validate enum values. Other type validation is RECOMMENDED but not mandatory for a minimal implementation.
## 8. Output contract
`output` describes the intended result, not an execution protocol.
```yaml
output:
format: markdown
description: Concise structured findings.
```
Suggested `format` values include:
- `text`
- `markdown`
- `json`
- `yaml`
- `xml`
- `code`
Registries MAY index output format for discovery.
## 9. Compatibility
`compatibility` records known requirements or observations without binding the package to one runtime.
```yaml
compatibility:
capabilities:
- code-analysis
- long-context
models:
- example/model-family
providers: []
```
Semantics:
- `capabilities`: abstract capabilities expected from the execution environment;
- `models`: model identifiers known to be compatible or evaluated;
- `providers`: provider identifiers when provider-specific behavior matters.
An empty list means "not constrained/unspecified", not "compatible with nothing".
## 10. Dependencies
Dependencies describe external artifacts or capabilities expected by the prompt.
```yaml
dependencies:
prompts:
- id: context/repository-summary
version: 1.0.0
requirement: optional
context:
- id: policy/security
requirement: required
capabilities:
- web-search
```
Recommended requirement values:
- `required`
- `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.**
This allows richer systems to integrate prompt resolution without forcing simple tools to implement an agent runtime.
## 11. Examples
`examples` is a list of relative paths.
An example file is not normative but SHOULD make intended usage obvious.
Suggested YAML shape:
```yaml
name: basic review
values:
change: |
def add(a, b):
return a + b
depth: quick
```
Tools MAY render examples directly.
## 12. Evals
`evals` is a list of relative paths to evaluation specifications.
CPF v0.1 deliberately does not standardize a universal evaluation language. Eval files SHOULD therefore declare their own evaluator or schema.
Example:
```yaml
schema: canned-prompts/eval-rubric/v0.1
name: code-review-quality
criteria:
- identifies correctness risks
- distinguishes blocking from advisory findings
- avoids inventing repository facts
```
A registry may associate externally collected run/eval evidence with `<id>@<version>` without mutating the package.
## 13. Provenance and lineage
```yaml
provenance:
author: Ada Example
source: https://example.invalid/source
derived_from:
- id: review/code-review
version: 1.1.0
```
The field is descriptive in v0.1. Registries SHOULD preserve provenance when publishing or mirroring packages.
## 14. Licensing
A package MAY declare an SPDX license identifier or other clear license expression:
```yaml
license: CC-BY-4.0
```
Absence of a license MUST NOT be interpreted as permission to redistribute or modify the package.
Tools SHOULD surface licensing metadata during publishing and installation.
## 15. Tags
```yaml
tags:
- architecture
- review
- agentic-coding
```
Tags are free-form discovery hints. Registries MAY normalize or enrich tags while preserving package metadata.
## 16. Extensions
`extensions` is the designated escape hatch for experimental or implementation-specific metadata.
```yaml
extensions:
org.example.canned-prompts:
maturity: experimental
```
Extension keys SHOULD be namespaced to avoid collisions.
A consumer MUST ignore unknown extension entries unless it explicitly claims support for them.
## 17. Immutability and versioning
Published `<id>@<version>` pairs SHOULD be immutable.
A registry SHOULD reject publication of a package when the same ID/version already exists with different contents unless an explicit administrative override mechanism exists.
Suggested versioning guidance:
- PATCH: wording/metadata correction with intended behavior unchanged;
- MINOR: backward-compatible behavior or parameter additions;
- MAJOR: changed contract, renamed/removed inputs, or materially different intended behavior.
This guidance is intentionally advisory because prompt behavior is probabilistic and cannot be versioned as mechanically as an API.
## 18. Package validation
A v0.1 validator SHOULD verify at least:
1. `prompt.yaml` exists and parses as YAML;
2. required manifest fields exist;
3. `format == canned-prompt/v0.1`;
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;
7. referenced example/eval paths do not escape the package;
8. required inputs and parameter names are unique;
9. every template placeholder resolves to a declared input or parameter;
10. no required value is silently omitted during rendering.
## 19. Security requirements
Prompt packages are content, not trusted code.
Implementations MUST NOT:
- execute code merely because it appears in a package;
- treat template expressions as arbitrary code;
- interpolate environment variables or credentials implicitly;
- follow paths outside the package without explicit user action;
- embed or require secrets in published package metadata.
Implementations SHOULD:
- inspect all referenced paths for traversal;
- make package contents visible before execution;
- surface provenance and license metadata;
- treat remote content referenced by a package as untrusted input;
- separate package installation from model/tool authorization.
## 20. Registry model
CPF v0.1 does not mandate registry transport.
A valid registry may be:
- a filesystem directory;
- a Git repository;
- an object store;
- an HTTP service;
- a federated catalog.
Conceptually, a registry stores immutable package versions keyed by:
```text
<id>@<version>
```
The reference implementation uses the filesystem layout:
```text
registry/
└── <id path>/
└── <version>/
├── prompt.yaml
└── ...
```
For example:
```text
registry/
└── practice/
└── pqrst-estimate/
└── 0.1.0/
├── prompt.yaml
└── prompt.md
```
## 21. Reference CLI semantics
The v0.1 reference tool uses two stores:
- **catalog** — packages locally available for search/show/render;
- **registry** — packages available for publish/install.
Commands:
```text
add PATH validate and copy a package into the local catalog
search QUERY search locally installed package metadata
show ID display one installed package manifest
render ID render an installed prompt with supplied values
publish PATH validate and copy a package into a filesystem registry
install ID copy a package version from the registry into the catalog
```
These semantics are illustrative, not mandatory for other implementations.
## 22. Worked example
`prompt.yaml`:
```yaml
format: canned-prompt/v0.1
id: practice/pqrst-estimate
name: PQRST Estimate
version: 0.1.0
summary: Estimate how session effort was distributed across PQRST categories.
template: prompt.md
inputs:
- name: session_summary
type: content
required: true
parameters:
include_rationale:
type: boolean
default: true
output:
format: markdown
tags: [retrospective, agentic-coding, pqrst]
```
`prompt.md`:
```markdown
Review the following coding-session summary and estimate the distribution of
session effort across PQRST. Percentages must sum to 100%.
P = main problem
Q = quality and tests
R = research and context clarification
S = security and credentials
T = task organization
Session:
{{ session_summary }}
Include rationale: {{ include_rationale }}
```
## 23. Open questions for v0.2+
Experience should 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;
- run manifests and evidence formats;
- deterministic compilation manifests;
- trust/reputation signals;
- federated discovery;
- richer template syntax.
Until practical usage forces these decisions, v0.1 should remain intentionally small.

247
INTENT.md Normal file
View file

@ -0,0 +1,247 @@
# canned-prompts — INTENT
> **Collect, reuse and share prompts and prompt templates.**
## Mission
`canned-prompts` exists to make prompts that are worth using more than once easy to package, understand, find, reuse, version, evaluate, and share.
The project treats a reusable prompt as a small software-like artifact with an explicit contract rather than as an anonymous block of copied text.
A canned prompt can therefore carry not only prompt text, but also its purpose, inputs, parameters, expected output, examples, compatibility information, provenance, and quality evidence.
## Core proposition
> **The valuable unit is not prompt text. It is a reusable prompt contract.**
A copied prompt is easy to produce but hard to govern. A reusable prompt should be independently understandable and usable by another human, tool, or agent without requiring hidden knowledge from its author.
## Primary user loop
```text
write
package
publish
discover
configure
render / invoke
observe
improve
publish a new revision
```
The first implementation should make this loop cheap enough that packaging a useful prompt feels easier than leaving it in an arbitrary notes file.
## Scope
`canned-prompts` owns the **artifact and ecosystem around reusable prompts**.
It should establish:
1. a portable package format for prompts and prompt templates;
2. a catalog/registry model for discovery and distribution;
3. explicit parameters and context requirements;
4. versioning and provenance;
5. examples and evaluation hooks;
6. compatibility metadata for models, tools, agents, or required capabilities;
7. composition metadata for prompts that depend on other reusable prompt/context artifacts;
8. lightweight tooling to add, inspect, search, install, render, and publish packages.
## Deliberate boundary
> **canned-prompts standardizes reusable prompt artifacts, not agent execution.**
The project may provide thin execution adapters for convenience, but it should not become a general agent runtime, workflow engine, model router, memory system, or orchestration framework.
Execution systems should be able to consume canned prompt packages without being forced to adopt a particular agent architecture.
A useful conceptual separation is:
```text
canned-prompts
|
| discover / retrieve
v
PromptPackage / PromptTemplate
|
v
resolver / compiler / agent runtime
|
v
CompiledPrompt
|
v
PromptRun
|
v
result + run evidence
```
## Design principles
### 1. Text first
A package should remain readable and editable with ordinary text tools. Markdown plus a small machine-readable manifest is the preferred baseline.
### 2. Portable by default
The package format must not depend on one model provider, IDE, agent framework, or hosted registry.
### 3. Explicit over magical
Inputs, parameters, dependencies, and expectations should be declared where practical. Hidden context is the enemy of reuse.
### 4. Useful before sophisticated
A prompt consisting only of `prompt.yaml` and `prompt.md` should already be a valid package. Examples, evals, dependencies, and richer metadata are progressive enhancements.
### 5. Version prompt behavior
Changes that materially alter intended behavior should create a new package version. Consumers should be able to pin versions when reproducibility matters.
### 6. Preserve provenance
Authorship, source, derivation, and lineage should be representable. Forking and adaptation are expected rather than treated as exceptional.
### 7. Quality should become evidence-backed
Popularity is not the same as quality. The ecosystem should make room for evals, run evidence, model/context compatibility, and adoption signals without requiring them in the minimum package.
### 8. Safe to inspect
Prompt packages must never require embedded secrets or credentials. Consumers should be able to inspect the complete artifact before execution.
### 9. Composition without capture
Packages may declare dependencies on prompts, context generators, policies, or information spaces, but the format should describe those requirements without dictating one resolver implementation.
### 10. Local-first, registry-ready
The first useful implementation should work entirely on a filesystem. Hosted or federated registries can be layered on later without changing the core package semantics.
## Canonical concepts
### PromptPackage
The distributable directory containing a manifest, template text, and optional supporting artifacts.
### PromptTemplate
The reusable prompt text plus its declared substitution/context contract.
### Input
Content supplied by a caller for a specific use, such as source code, a document, or a question.
### Parameter
A named configuration choice that modifies how the prompt behaves, preferably with type, allowed values, and defaults.
### Dependency
Another named artifact or capability that a package expects to be available. Dependency resolution is outside the core package format.
### Example
A small reproducible usage fixture showing representative inputs and/or rendered output.
### Eval
A machine- or human-readable specification for assessing whether the prompt behaves as intended.
### Provenance
Metadata describing origin, authorship, source, derivation, and lineage.
### RunEvidence
Optional external evidence produced by use of a prompt version. Run evidence is not embedded into the immutable package by default, but may be linked to it by registries or evaluation systems.
## Initial capability surface
A minimal reference tool should support:
```text
canned-prompts add <package-path>
canned-prompts search <query>
canned-prompts show <id>
canned-prompts render <id> --set name=value
canned-prompts install <id>
canned-prompts publish <package-path>
```
The reference implementation may use a filesystem-backed local catalog and filesystem-backed registry. Network services are explicitly unnecessary for v0.1.
## Non-goals for v0.1
The first version does **not** need to solve:
- hosted social features;
- model execution or billing;
- autonomous dependency generation;
- semantic version range resolution;
- trust/reputation scoring;
- cryptographic package signing;
- cross-registry federation;
- secrets management;
- universal prompt evaluation;
- a universal agent or workflow specification.
These may become separate modules or later layers if usage demonstrates the need.
## Success criteria for the first practical release
The project is useful when a user can:
1. take a prompt worth keeping and package it in a few minutes;
2. understand an unfamiliar package without reading external documentation;
3. search a local catalog and discover a suitable prompt;
4. render a parameterized prompt deterministically;
5. publish a version to a registry and install it elsewhere;
6. keep multiple versions without destructive overwrite;
7. attach examples or eval specifications without changing the core format;
8. use the package from another tool without importing the reference runtime.
## Evolution hypothesis
If the simple package loop proves useful, `canned-prompts` can evolve from a prompt library into a package ecosystem for reusable cognitive procedures.
A mature feedback loop could be:
```text
PromptPackage
Use
Run evidence
Evaluation
Improved package
New version
Adoption / comparison
```
That later ecosystem should be able to answer questions such as:
- Which prompt versions perform better for a defined task?
- Under which models, contexts, or capabilities?
- Which forks outperform their ancestors?
- Which dependencies reliably improve outcomes?
- When has a prompt's behavior drifted enough to require a new version?
These are long-term opportunities, not reasons to complicate the initial format.
## Working motto
> **Package what is worth prompting twice.**

View file

@ -1,3 +1,55 @@
# canned-prompts
# canned-prompts seed
Collect, reuse and share prompts and prompt templates
> **Collect, reuse and share prompts and prompt templates.**
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.
- [`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.
## Try the reference implementation
```bash
cd reference
python -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
# add the included example to your local catalog
python canned_prompts.py add ../examples/pqrst-estimate
# find and inspect it
python canned_prompts.py search pqrst
python canned_prompts.py show practice/pqrst-estimate
# render it
python canned_prompts.py render practice/pqrst-estimate \
--set session_summary="Implemented feature X, read unfamiliar code, added tests."
# publish it to the local filesystem registry
python canned_prompts.py publish ../examples/pqrst-estimate
# remove the local catalog if you want to simulate another machine, then install
python canned_prompts.py install practice/pqrst-estimate --version 0.1.0
```
By default the reference tool uses:
```text
~/.canned-prompts/catalog
~/.canned-prompts/registry
```
Override them with:
```text
CANNED_PROMPTS_HOME=/some/path
```
or command-level `--catalog` / `--registry` options.
## Deliberate limitations
This seed has no hosted registry, model execution, authentication, network access, dependency resolver, or social features. `publish` and `install` operate on a filesystem registry so that the package semantics can be tested before infrastructure is built around them.

66
SCOPE.md Normal file
View file

@ -0,0 +1,66 @@
# SCOPE
## One-liner
Portable package format, spec and reference CLI for reusable prompt artifacts
(Canned Prompt Format v0.1).
## Core Idea
The valuable unit is not prompt text but a **reusable prompt contract**: a
filesystem-portable, provider-neutral package (`prompt.yaml` + `prompt.md`) that
declares its purpose, inputs, parameters, output, compatibility, provenance and
evidence, and is inspectable before use. See `INTENT.md`.
## In Scope
- `CannedPromptFormat-v0.1.md` — the package-format specification.
- `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.
- Format evolution: manifest surface, rendering rules, validation rules,
registry semantics, provenance and eval hooks.
## Out of Scope
Per `INTENT.md` § Deliberate boundary — canned-prompts standardizes reusable
prompt artifacts, **not agent execution**:
- model execution, routing, billing, or authentication;
- agent runtimes, workflow/orchestration engines, memory systems;
- hosted or federated registries and network transport (v0.1 is filesystem-only);
- dependency resolution, semver range resolution, package signing;
- trust/reputation scoring and social features.
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`.
## Developer Workflow
```bash
cd reference
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt
python3 -m pytest -q # tests
CANNED_PROMPTS_HOME=/tmp/cp \
python3 canned_prompts.py add ../examples/pqrst-estimate # smoke
```
There is no lint or build step yet; `pyproject.toml` declares the
`canned-prompts` console script but the module is normally run directly.
## Getting Oriented
- Intent and principles: `INTENT.md`
- Format specification: `CannedPromptFormat-v0.1.md`
- Agent instructions: `AGENTS.md`
- Workplans: `workplans/`

View file

@ -0,0 +1,11 @@
# PQRST Estimate example
This package is included as a first non-trivial example of Canned Prompt Format v0.1.
It demonstrates:
- one required content input;
- one boolean parameter with a default;
- a structured output expectation;
- discovery tags;
- a prompt that carries terminology and interpretation rules, not merely prose.

View file

@ -0,0 +1,8 @@
name: feature implementation with unfamiliar codebase
values:
session_summary: |
The session traced an unfamiliar request path through the repository,
implemented a new validation rule, added unit and integration tests, fixed
two edge cases discovered during testing, and updated the implementation
after finding a conflicting assumption in an internal helper.
include_rationale: true

View file

@ -0,0 +1,47 @@
Review the coding session described below and produce a **PQRST Estimate** of
where effort was spent.
Use these categories:
- **P — Main problem:** implementing or directly solving the requested deliverable.
- **Q — Quality and tests:** tests, verification, edge cases, maintainability,
error handling, cleanup, and production-quality hardening.
- **R — Research and context clarification:** reading the codebase or docs,
tracing behavior, investigating unknowns, reconciling requirements, and
establishing missing context.
- **S — Security and credentials:** authentication, authorization, secrets,
credentials, trust boundaries, security validation, and security-specific
handling.
- **T — Task organization:** planning, decomposition, todo management,
sequencing, coordination, and overhead required to keep the work organized.
Treat this as a **post-session audit, not a planning estimate**. Estimate
relative cognitive/work effort rather than tokens or wall-clock time. The five
percentages **must sum to exactly 100%**.
Where activities overlap, assign effort according to the primary purpose of the
activity. Do not inflate a category merely because it was important; estimate
how much effort it actually consumed.
Session material:
{{ session_summary }}
Return:
```text
P: NN%
Q: NN%
R: NN%
S: NN%
T: NN%
Total: 100%
```
Then provide:
1. **Primary effort driver** — one sentence naming what dominated the session.
2. **Interpretation** — what the distribution says about the session's shape.
3. **Signal** — one notable imbalance, if any, that may be worth learning from.
Include rationale: {{ include_rationale }}

View file

@ -0,0 +1,38 @@
format: canned-prompt/v0.1
id: practice/pqrst-estimate
name: PQRST Estimate
version: 0.1.0
summary: 'Produce a post-session estimate of effort distributed across the PQRST categories
for an agentic coding session.
'
type: template
template: prompt.md
inputs:
- name: session_summary
type: content
required: true
description: 'Session transcript, summary, or sufficiently detailed account of the
work performed during the coding session.
'
parameters:
include_rationale:
type: boolean
default: true
description: Explain the evidence behind the estimate.
output:
format: markdown
description: A 100% PQRST effort allocation with concise interpretation.
compatibility:
capabilities:
- session-review
tags:
- pqrst
- retrospective
- agentic-coding
- effort-estimation
provenance:
author: canned-prompts seed
examples:
- examples/basic.yaml

49
reference/README.md Normal file
View file

@ -0,0 +1,49 @@
# canned-prompts reference CLI
This is intentionally a **small reference implementation**, not the intended final architecture.
It demonstrates six verbs:
```text
add PATH
search QUERY
show ID
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.
## Stores
Default locations:
```text
~/.canned-prompts/catalog
~/.canned-prompts/registry
```
Catalog and registry both store packages as:
```text
<store>/<id path>/<version>/...
```
For example:
```text
~/.canned-prompts/catalog/practice/pqrst-estimate/0.1.0/
```
## Design choices
- YAML manifest via PyYAML.
- `{{ name }}` template substitution only.
- No arbitrary expression/code execution.
- Published versions are immutable by default.
- `install` copies from registry to catalog.
- `add` copies a package directly to catalog.
- `search`, `show`, and `render` operate on catalog packages.
Use this implementation to challenge the format. Replace it once real usage reveals the right architecture.

418
reference/canned_prompts.py Executable file
View file

@ -0,0 +1,418 @@
#!/usr/bin/env python3
"""Tiny reference CLI for Canned Prompt Format v0.1.
This implementation intentionally favors readability over features. It uses a
filesystem-backed local catalog and registry and never calls a model.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import sys
from pathlib import Path
from typing import Any, Iterable
import yaml
FORMAT = "canned-prompt/v0.1"
PLACEHOLDER_RE = re.compile(r"{{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*}}")
SEMVER_RE = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+].*)?$")
REQUIRED_FIELDS = ("format", "id", "name", "version", "summary", "template")
class CannedPromptError(Exception):
pass
def home_dir() -> Path:
return Path(os.environ.get("CANNED_PROMPTS_HOME", Path.home() / ".canned-prompts"))
def default_catalog() -> Path:
return home_dir() / "catalog"
def default_registry() -> Path:
return home_dir() / "registry"
def read_manifest(package_dir: Path) -> dict[str, Any]:
manifest_path = package_dir / "prompt.yaml"
if not manifest_path.is_file():
raise CannedPromptError(f"missing manifest: {manifest_path}")
try:
data = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
except yaml.YAMLError as exc:
raise CannedPromptError(f"invalid YAML in {manifest_path}: {exc}") from exc
if not isinstance(data, dict):
raise CannedPromptError("prompt.yaml must contain a mapping")
return data
def safe_relative_file(package_dir: Path, relative: str, field: str) -> Path:
if not isinstance(relative, str) or not relative.strip():
raise CannedPromptError(f"{field} must be a non-empty relative path")
candidate = (package_dir / relative).resolve()
root = package_dir.resolve()
try:
candidate.relative_to(root)
except ValueError as exc:
raise CannedPromptError(f"{field} escapes the package: {relative}") from exc
if not candidate.is_file():
raise CannedPromptError(f"{field} does not reference a file: {relative}")
return candidate
def declared_names(manifest: dict[str, Any]) -> set[str]:
names: set[str] = set()
inputs = manifest.get("inputs") or []
if not isinstance(inputs, list):
raise CannedPromptError("inputs must be a list")
for item in inputs:
if not isinstance(item, dict) or not isinstance(item.get("name"), str):
raise CannedPromptError("each input must be a mapping with a string name")
name = item["name"]
if name in names:
raise CannedPromptError(f"duplicate input/parameter name: {name}")
names.add(name)
parameters = manifest.get("parameters") or {}
if not isinstance(parameters, dict):
raise CannedPromptError("parameters must be a mapping")
for name, spec in parameters.items():
if not isinstance(name, str) or not isinstance(spec, dict):
raise CannedPromptError("parameters must map names to mappings")
if name in names:
raise CannedPromptError(f"duplicate input/parameter name: {name}")
names.add(name)
return names
def validate_package(package_dir: Path) -> dict[str, Any]:
package_dir = package_dir.resolve()
if not package_dir.is_dir():
raise CannedPromptError(f"package directory not found: {package_dir}")
manifest = read_manifest(package_dir)
missing = [field for field in REQUIRED_FIELDS if field not in manifest]
if missing:
raise CannedPromptError("missing required fields: " + ", ".join(missing))
if manifest["format"] != FORMAT:
raise CannedPromptError(f"unsupported format: {manifest['format']!r}")
package_id = manifest["id"]
if not isinstance(package_id, str) or not package_id.strip():
raise CannedPromptError("id must be a non-empty string")
if any(part in ("", ".", "..") for part in package_id.split("/")):
raise CannedPromptError("id contains an invalid path segment")
if re.search(r"[^A-Za-z0-9._/-]", package_id):
raise CannedPromptError("id contains unsupported characters")
version = manifest["version"]
if not isinstance(version, str) or not SEMVER_RE.match(version):
raise CannedPromptError("version must be semantic-version-like, e.g. 1.2.0")
template_path = safe_relative_file(package_dir, manifest["template"], "template")
for field in ("examples", "evals"):
refs = manifest.get(field) or []
if not isinstance(refs, list):
raise CannedPromptError(f"{field} must be a list")
for relative in refs:
safe_relative_file(package_dir, relative, field)
names = declared_names(manifest)
template = template_path.read_text(encoding="utf-8")
placeholders = set(PLACEHOLDER_RE.findall(template))
undeclared = sorted(placeholders - names)
if undeclared:
raise CannedPromptError(
"template contains undeclared placeholders: " + ", ".join(undeclared)
)
return manifest
def id_path(store: Path, package_id: str) -> Path:
parts = package_id.split("/")
if any(part in ("", ".", "..") for part in parts):
raise CannedPromptError("unsafe package id")
return store.joinpath(*parts)
def package_path(store: Path, package_id: str, version: str) -> Path:
return id_path(store, package_id) / version
def parse_semver(value: str) -> tuple[int, int, int, str]:
match = SEMVER_RE.match(value)
if not match:
return (-1, -1, -1, value)
return (int(match.group(1)), int(match.group(2)), int(match.group(3)), value)
def versions_for(store: Path, package_id: str) -> list[str]:
base = id_path(store, package_id)
if not base.is_dir():
return []
versions = [p.name for p in base.iterdir() if p.is_dir() and (p / "prompt.yaml").is_file()]
return sorted(versions, key=parse_semver, reverse=True)
def resolve_installed(store: Path, package_id: str, version: str | None) -> Path:
if version:
path = package_path(store, package_id, version)
if not path.is_dir():
raise CannedPromptError(f"package not found: {package_id}@{version}")
return path
versions = versions_for(store, package_id)
if not versions:
raise CannedPromptError(f"package not found: {package_id}")
return package_path(store, package_id, versions[0])
def copy_immutable(src: Path, dst: Path, what: str) -> None:
if dst.exists():
raise CannedPromptError(f"{what} already exists: {dst}")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(src, dst)
def iter_packages(store: Path) -> Iterable[tuple[Path, dict[str, Any]]]:
if not store.exists():
return
for manifest_path in store.rglob("prompt.yaml"):
package_dir = manifest_path.parent
try:
manifest = validate_package(package_dir)
except CannedPromptError:
continue
yield package_dir, manifest
def cmd_add(args: argparse.Namespace) -> None:
src = Path(args.path)
manifest = validate_package(src)
catalog = Path(args.catalog).expanduser()
dst = package_path(catalog, manifest["id"], manifest["version"])
copy_immutable(src.resolve(), dst, "catalog package")
print(f"added {manifest['id']}@{manifest['version']} -> {dst}")
def cmd_publish(args: argparse.Namespace) -> None:
src = Path(args.path)
manifest = validate_package(src)
registry = Path(args.registry).expanduser()
dst = package_path(registry, manifest["id"], manifest["version"])
copy_immutable(src.resolve(), dst, "published package")
print(f"published {manifest['id']}@{manifest['version']} -> {dst}")
def cmd_install(args: argparse.Namespace) -> None:
registry = Path(args.registry).expanduser()
catalog = Path(args.catalog).expanduser()
src = resolve_installed(registry, args.id, args.version)
manifest = validate_package(src)
dst = package_path(catalog, manifest["id"], manifest["version"])
copy_immutable(src, dst, "catalog package")
print(f"installed {manifest['id']}@{manifest['version']} -> {dst}")
def cmd_search(args: argparse.Namespace) -> None:
catalog = Path(args.catalog).expanduser()
query = args.query.lower()
matches: list[dict[str, str]] = []
for _, manifest in iter_packages(catalog):
haystack = " ".join(
[
str(manifest.get("id", "")),
str(manifest.get("name", "")),
str(manifest.get("summary", "")),
" ".join(str(tag) for tag in (manifest.get("tags") or [])),
]
).lower()
if query in haystack:
matches.append(
{
"id": manifest["id"],
"version": manifest["version"],
"name": manifest["name"],
"summary": manifest["summary"],
}
)
matches.sort(key=lambda m: (m["id"], parse_semver(m["version"])), reverse=False)
if args.json:
print(json.dumps(matches, indent=2, ensure_ascii=False))
return
if not matches:
print("no matches")
return
for item in matches:
print(f"{item['id']}@{item['version']} {item['name']}")
print(f" {item['summary']}")
def cmd_show(args: argparse.Namespace) -> None:
catalog = Path(args.catalog).expanduser()
package_dir = resolve_installed(catalog, args.id, args.version)
manifest = validate_package(package_dir)
if args.json:
print(json.dumps(manifest, indent=2, ensure_ascii=False))
else:
print(yaml.safe_dump(manifest, sort_keys=False, allow_unicode=True).rstrip())
def coerce_value(raw: str, spec: dict[str, Any]) -> Any:
kind = spec.get("type", "string")
if kind == "boolean":
lowered = raw.lower()
if lowered in {"true", "1", "yes", "on"}:
return True
if lowered in {"false", "0", "no", "off"}:
return False
raise CannedPromptError(f"cannot parse boolean value: {raw}")
if kind == "integer":
try:
return int(raw)
except ValueError as exc:
raise CannedPromptError(f"cannot parse integer value: {raw}") from exc
if kind == "number":
try:
return float(raw)
except ValueError as exc:
raise CannedPromptError(f"cannot parse numeric value: {raw}") from exc
if kind == "enum":
values = spec.get("values") or []
if raw not in values:
raise CannedPromptError(f"invalid enum value {raw!r}; expected one of {values}")
return raw
def supplied_values(pairs: list[str]) -> dict[str, str]:
values: dict[str, str] = {}
for pair in pairs:
if "=" not in pair:
raise CannedPromptError(f"--set expects name=value, got: {pair}")
name, value = pair.split("=", 1)
if not name:
raise CannedPromptError("--set name cannot be empty")
values[name] = value
return values
def resolve_values(manifest: dict[str, Any], raw_values: dict[str, str]) -> dict[str, Any]:
resolved: dict[str, Any] = {}
known: set[str] = set()
inputs = manifest.get("inputs") or []
for item in inputs:
name = item["name"]
known.add(name)
if name in raw_values:
resolved[name] = raw_values[name]
elif item.get("required", False):
raise CannedPromptError(f"missing required input: {name}")
parameters = manifest.get("parameters") or {}
for name, spec in parameters.items():
known.add(name)
if name in raw_values:
resolved[name] = coerce_value(raw_values[name], spec)
elif "default" in spec:
resolved[name] = spec["default"]
unknown = sorted(set(raw_values) - known)
if unknown:
raise CannedPromptError("unknown values: " + ", ".join(unknown))
return resolved
def render_template(template: str, values: dict[str, Any]) -> str:
def replace(match: re.Match[str]) -> str:
name = match.group(1)
if name not in values:
raise CannedPromptError(f"unresolved placeholder: {name}")
value = values[name]
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (dict, list)):
return json.dumps(value, ensure_ascii=False)
return str(value)
return PLACEHOLDER_RE.sub(replace, template)
def cmd_render(args: argparse.Namespace) -> None:
catalog = Path(args.catalog).expanduser()
package_dir = resolve_installed(catalog, args.id, args.version)
manifest = validate_package(package_dir)
template_path = safe_relative_file(package_dir, manifest["template"], "template")
raw = supplied_values(args.set_values)
values = resolve_values(manifest, raw)
rendered = render_template(template_path.read_text(encoding="utf-8"), values)
print(rendered, end="" if rendered.endswith("\n") else "\n")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="canned-prompts", description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
add = sub.add_parser("add", help="add a package directory to the local catalog")
add.add_argument("path")
add.add_argument("--catalog", default=str(default_catalog()))
add.set_defaults(func=cmd_add)
search = sub.add_parser("search", help="search the local catalog")
search.add_argument("query")
search.add_argument("--catalog", default=str(default_catalog()))
search.add_argument("--json", action="store_true")
search.set_defaults(func=cmd_search)
show = sub.add_parser("show", help="show an installed package manifest")
show.add_argument("id")
show.add_argument("--version")
show.add_argument("--catalog", default=str(default_catalog()))
show.add_argument("--json", action="store_true")
show.set_defaults(func=cmd_show)
render = sub.add_parser("render", help="render an installed prompt template")
render.add_argument("id")
render.add_argument("--version")
render.add_argument("--catalog", default=str(default_catalog()))
render.add_argument("--set", dest="set_values", action="append", default=[], metavar="NAME=VALUE")
render.set_defaults(func=cmd_render)
install = sub.add_parser("install", help="install a package from a filesystem registry")
install.add_argument("id")
install.add_argument("--version")
install.add_argument("--catalog", default=str(default_catalog()))
install.add_argument("--registry", default=str(default_registry()))
install.set_defaults(func=cmd_install)
publish = sub.add_parser("publish", help="publish a package to a filesystem registry")
publish.add_argument("path")
publish.add_argument("--registry", default=str(default_registry()))
publish.set_defaults(func=cmd_publish)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
args.func(args)
return 0
except CannedPromptError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())

16
reference/pyproject.toml Normal file
View file

@ -0,0 +1,16 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "canned-prompts-reference"
version = "0.1.0"
description = "Tiny filesystem reference CLI for Canned Prompt Format v0.1"
requires-python = ">=3.10"
dependencies = ["PyYAML>=6.0,<7"]
[project.scripts]
canned-prompts = "canned_prompts:main"
[tool.setuptools]
py-modules = ["canned_prompts"]

View file

@ -0,0 +1,2 @@
-r requirements.txt
pytest>=8,<9

View file

@ -0,0 +1 @@
PyYAML>=6.0,<7

View file

@ -0,0 +1,53 @@
from pathlib import Path
import pytest
import canned_prompts as cp
@pytest.fixture()
def package(tmp_path: Path) -> Path:
pkg = tmp_path / "pkg"
pkg.mkdir()
(pkg / "prompt.yaml").write_text(
"""\
format: canned-prompt/v0.1
id: demo/hello
name: Hello
version: 1.0.0
summary: Say hello.
template: prompt.md
inputs:
- name: person
required: true
parameters:
tone:
type: enum
values: [warm, formal]
default: warm
""",
encoding="utf-8",
)
(pkg / "prompt.md").write_text(
"Say hello to {{ person }} in a {{ tone }} tone.\n", encoding="utf-8"
)
return pkg
def test_validate_and_render(package: Path) -> None:
manifest = cp.validate_package(package)
values = cp.resolve_values(manifest, {"person": "Ada"})
rendered = cp.render_template((package / "prompt.md").read_text(), values)
assert rendered == "Say hello to Ada in a warm tone.\n"
def test_missing_required_input_fails(package: Path) -> None:
manifest = cp.validate_package(package)
with pytest.raises(cp.CannedPromptError, match="missing required input"):
cp.resolve_values(manifest, {})
def test_undeclared_placeholder_fails(package: Path) -> None:
(package / "prompt.md").write_text("{{ missing }}\n", encoding="utf-8")
with pytest.raises(cp.CannedPromptError, match="undeclared placeholders"):
cp.validate_package(package)

View file

@ -0,0 +1,57 @@
---
id: CANP-WP-0001
type: workplan
title: "Bootstrap State Hub integration"
domain: agents
repo: canned-prompts
status: finished
owner: codex
topic_slug: practice
created: "2026-09-06"
updated: "2026-09-06"
---
# Bootstrap State Hub integration
Portable package format, spec and reference CLI for reusable prompt artifacts (Canned Prompt Format v0.1).
## Review Generated Integration Files
```task
id: CANP-WP-0001-T01
status: done
priority: high
```
Review `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `.custodian-brief.md`.
Replace generated placeholders with repo-specific facts where needed.
## Verify Local Developer Workflow
```task
id: CANP-WP-0001-T02
status: done
priority: high
```
Identify the repo's install, test, lint, build, and run commands. Add or refine
those commands in the agent instructions so future coding sessions can verify
changes confidently.
## Seed First Real Workplan
```task
id: CANP-WP-0001-T03
status: done
priority: medium
```
Done: `workplans/CANP-WP-0002-format-open-questions.md`.
Create the first implementation workplan for the repository's most important
next change. After workplan file updates, run the sync locally from this repo
checkout:
```bash
statehub fix-consistency
```

View file

@ -0,0 +1,183 @@
---
id: CANP-WP-0002
type: workplan
title: "Resolve CPF v0.1 open questions promoted for v0.2"
domain: agents
repo: canned-prompts
status: ready
owner: codex
topic_slug: practice
created: "2026-09-06"
updated: "2026-09-06"
---
# Resolve CPF v0.1 open questions promoted for v0.2
`CannedPromptFormat-v0.1.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
"decide, with a stated leaning". This workplan carries those decisions.
Unpromoted § 23 items stay deferred and unchanged: content macros, cryptographic
integrity/signing, model capability vocabularies, run manifests and evidence
formats, deterministic compilation manifests, trust/reputation signals,
federated discovery, richer template syntax.
**Guardrail for every task below:** `INTENT.md` § Deliberate boundary. The
format may *declare* a requirement; it must not specify the resolver, runtime,
or execution engine that satisfies it.
## Optional inputs need defaults
```task
id: CANP-WP-0002-T01
status: todo
priority: high
```
**Gap found in review.** Rendering rule § 5.1(4) makes any unresolved
placeholder an error, and § 6 gives `inputs` no `default` field. An input
declared `required: false` and referenced from the template therefore makes
`render` fail unless a value is supplied — optional inputs are effectively
unusable. The spec's own § 4 manifest example hits this with
`repository_context`. `reference/canned_prompts.py` is conformant here; the
defect is in the spec.
**Decision (operator, 2026-09-06):** add a `default` field to `inputs`, and
allow that default to be either
- a **static** value, or
- a **derived** default — a prompt that produces the value from available
context.
Unresolved-with-no-default remains an error.
Work:
1. Extend § 6 with `default`, and state the two default kinds.
2. Specify the derived-default declaration form so it stays inert data: the
package declares *what* to derive and the prompt to derive it with; it never
names or requires a specific resolver, model, or runtime. A consumer that
cannot derive must report the input as unresolved rather than guess.
3. Reconcile § 5.1: rules 12 gain input defaults; rule 4 keeps unresolved as
an error; § 5.1's "no conditionals, loops, filters, or functions" and
§ 19's "MUST NOT execute code merely because it appears in a package" must
survive the change — a derived default is a *request to a consumer*, not
template-embedded execution. Say so explicitly.
4. Extend § 18 validation accordingly.
5. Implement static defaults in `reference/canned_prompts.py` `resolve_values`
and add tests. Derived defaults: validate and surface them; the reference
CLI never calls a model, so it reports them as unresolved-by-design.
## Registry namespaces and ownership
```task
id: CANP-WP-0002-T02
status: todo
priority: high
```
An id like `practice/pqrst-estimate` has a namespace prefix with no owner. Two
authors publishing `practice/…` into one registry collide today; `add` and
`publish` only refuse an exact `id@version` that already exists.
Decide, for § 3.2 and § 20: what a namespace means, who may claim one, how a
filesystem registry records the claim, and what a consumer does on conflict.
Leaning: keep v0.1's local-first stance — namespace ownership is a *registry
policy*, declared by the registry rather than by the package — so package
semantics do not change when a hosted registry appears later.
Signing and trust scoring stay out of scope (§ 23, INTENT non-goals).
## Prompt composition and inheritance
```task
id: CANP-WP-0002-T03
status: todo
priority: high
```
Largest gap between `INTENT.md` and the spec. Principle 9 ("composition without
capture") and the `dependencies.prompts` field both promise composition, but
v0.1 defines no mechanism — `dependencies` is a declared field with no
semantics.
Decide what composition means at the *artifact* level: how one package
references another, whether references are includes, extends, or plain
declared prerequisites, and how versions are pinned. Leaning: declaration only
— a package states what it needs, and resolution stays with the consumer, per
the INTENT boundary. Do not introduce range resolution (INTENT non-goal).
## Canonical eval schemas
```task
id: CANP-WP-0002-T04
status: todo
priority: medium
```
Cheapest item to pin down. `evals/` is a reserved path and `evals:` is a
manifest list, but § 12 defines no schema, so an eval file is an unvalidated
blob that no tool can act on.
Define a minimal eval-spec schema: identity, what is being asserted, the
fixture it runs against, and how a result is reported. Keep it declarative and
engine-neutral — "universal prompt evaluation" is an explicit INTENT non-goal,
so this specifies the *file*, not an evaluation engine. Extend § 18 to validate
eval files that declare the schema, and add one eval to
`examples/pqrst-estimate` as a worked case.
## Typed context and dependency contracts
```task
id: CANP-WP-0002-T05
status: todo
priority: medium
```
`dependencies.context` and `dependencies.capabilities` appear in the § 4
manifest surface with no semantics whatsoever in v0.1, and § 9
`compatibility.capabilities` overlaps them without a stated relationship.
Decide: what a context dependency declares, how it differs from an input, how
it relates to `compatibility.capabilities`, and whether capability names are
free strings in v0.2 (model capability vocabularies stay deferred). Coordinate
with T01 — a derived default is a consumer-resolved context requirement, and
the two mechanisms must not describe the same thing twice.
## Rewrite specification section 23
```task
id: CANP-WP-0002-T06
status: todo
priority: medium
```
After T01T05 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.
## Reference implementation conformance fixes
```task
id: CANP-WP-0002-T07
status: todo
priority: low
```
Two defects found in the same review, independent of the format questions:
1. **Prerelease versions sort as newest.** `parse_semver` in
`reference/canned_prompts.py` returns `(major, minor, patch, raw_string)`,
so `1.0.0-rc1` and `1.0.0` tie on the numeric fields and then compare as
strings — `"1.0.0-rc1" > "1.0.0"`. `resolve_installed` with no `--version`
therefore selects a prerelease over its own release. Order prerelease below
release, or state in § 17 that v0.1 ignores prerelease ordering.
2. **`copy_immutable` copies everything.** `add`/`publish` use
`shutil.copytree` over the whole source directory, so a stray `.git`,
`.venv`, or scratch file lands in the catalog and registry. § 2 says tools
MUST ignore unknown non-reserved files unless a manifest field references
them. Decide whether packaging is reserved-paths-only or an explicit
ignore list, then align the implementation and § 2.