From dc615ef530d834c87347899e3f3d792370492beb Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 6 Sep 2026 00:45:23 +0200 Subject: [PATCH] Register with Custodian State Hub and seed format open-questions workplan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- .custodian-brief.md | 27 + .gitignore | 10 + AGENTS.md | 203 ++++++ CannedPromptFormat-v0.1.md | 640 ++++++++++++++++++ INTENT.md | 247 +++++++ README.md | 56 +- SCOPE.md | 66 ++ examples/pqrst-estimate/README.md | 11 + examples/pqrst-estimate/examples/basic.yaml | 8 + examples/pqrst-estimate/prompt.md | 47 ++ examples/pqrst-estimate/prompt.yaml | 38 ++ reference/README.md | 49 ++ reference/canned_prompts.py | 418 ++++++++++++ reference/pyproject.toml | 16 + reference/requirements-dev.txt | 2 + reference/requirements.txt | 1 + reference/tests/test_canned_prompts.py | 53 ++ workplans/CANP-WP-0001-statehub-bootstrap.md | 57 ++ .../CANP-WP-0002-format-open-questions.md | 183 +++++ 19 files changed, 2130 insertions(+), 2 deletions(-) create mode 100644 .custodian-brief.md create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CannedPromptFormat-v0.1.md create mode 100644 INTENT.md create mode 100644 SCOPE.md create mode 100644 examples/pqrst-estimate/README.md create mode 100644 examples/pqrst-estimate/examples/basic.yaml create mode 100644 examples/pqrst-estimate/prompt.md create mode 100644 examples/pqrst-estimate/prompt.yaml create mode 100644 reference/README.md create mode 100755 reference/canned_prompts.py create mode 100644 reference/pyproject.toml create mode 100644 reference/requirements-dev.txt create mode 100644 reference/requirements.txt create mode 100644 reference/tests/test_canned_prompts.py create mode 100644 workplans/CANP-WP-0001-statehub-bootstrap.md create mode 100644 workplans/CANP-WP-0002-format-open-questions.md diff --git a/.custodian-brief.md b/.custodian-brief.md new file mode 100644 index 0000000..ab63265 --- /dev/null +++ b/.custodian-brief.md @@ -0,0 +1,27 @@ + +# 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc68d82 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a226865 --- /dev/null +++ b/AGENTS.md @@ -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//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": "", + "task_id": "" + }' +``` + +Omit `workplan_id` / `task_id` when not applicable. + +### Update task status + +```bash +curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ + -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/" \ + -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: `, 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. + +--- + + + + +--- + +## 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-.md` + +**Archived location:** finished workplans may move to +`workplans/archived/YYMMDD-CANP-WP-NNNN-.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: "" # 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: "" # 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. diff --git a/CannedPromptFormat-v0.1.md b/CannedPromptFormat-v0.1.md new file mode 100644 index 0000000..ad01f1a --- /dev/null +++ b/CannedPromptFormat-v0.1.md @@ -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 +/ +``` + +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 `@` 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 `@` 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 +@ +``` + +The reference implementation uses the filesystem layout: + +```text +registry/ +└── / + └── / + ├── 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. diff --git a/INTENT.md b/INTENT.md new file mode 100644 index 0000000..da11dd0 --- /dev/null +++ b/INTENT.md @@ -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 +canned-prompts search +canned-prompts show +canned-prompts render --set name=value +canned-prompts install +canned-prompts publish +``` + +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.** diff --git a/README.md b/README.md index 5e38edb..3f69cbf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,55 @@ -# canned-prompts +# canned-prompts seed -Collect, reuse and share prompts and prompt templates \ No newline at end of file +> **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. diff --git a/SCOPE.md b/SCOPE.md new file mode 100644 index 0000000..7e9503f --- /dev/null +++ b/SCOPE.md @@ -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/` diff --git a/examples/pqrst-estimate/README.md b/examples/pqrst-estimate/README.md new file mode 100644 index 0000000..6281813 --- /dev/null +++ b/examples/pqrst-estimate/README.md @@ -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. diff --git a/examples/pqrst-estimate/examples/basic.yaml b/examples/pqrst-estimate/examples/basic.yaml new file mode 100644 index 0000000..3b863ec --- /dev/null +++ b/examples/pqrst-estimate/examples/basic.yaml @@ -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 diff --git a/examples/pqrst-estimate/prompt.md b/examples/pqrst-estimate/prompt.md new file mode 100644 index 0000000..6c1c84c --- /dev/null +++ b/examples/pqrst-estimate/prompt.md @@ -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 }} diff --git a/examples/pqrst-estimate/prompt.yaml b/examples/pqrst-estimate/prompt.yaml new file mode 100644 index 0000000..e4f157c --- /dev/null +++ b/examples/pqrst-estimate/prompt.yaml @@ -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 diff --git a/reference/README.md b/reference/README.md new file mode 100644 index 0000000..3615dd3 --- /dev/null +++ b/reference/README.md @@ -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 +///... +``` + +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. diff --git a/reference/canned_prompts.py b/reference/canned_prompts.py new file mode 100755 index 0000000..d3c3ba8 --- /dev/null +++ b/reference/canned_prompts.py @@ -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()) diff --git a/reference/pyproject.toml b/reference/pyproject.toml new file mode 100644 index 0000000..725e527 --- /dev/null +++ b/reference/pyproject.toml @@ -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"] diff --git a/reference/requirements-dev.txt b/reference/requirements-dev.txt new file mode 100644 index 0000000..044d133 --- /dev/null +++ b/reference/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest>=8,<9 diff --git a/reference/requirements.txt b/reference/requirements.txt new file mode 100644 index 0000000..1910948 --- /dev/null +++ b/reference/requirements.txt @@ -0,0 +1 @@ +PyYAML>=6.0,<7 diff --git a/reference/tests/test_canned_prompts.py b/reference/tests/test_canned_prompts.py new file mode 100644 index 0000000..094fada --- /dev/null +++ b/reference/tests/test_canned_prompts.py @@ -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) diff --git a/workplans/CANP-WP-0001-statehub-bootstrap.md b/workplans/CANP-WP-0001-statehub-bootstrap.md new file mode 100644 index 0000000..92c332b --- /dev/null +++ b/workplans/CANP-WP-0001-statehub-bootstrap.md @@ -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 +``` diff --git a/workplans/CANP-WP-0002-format-open-questions.md b/workplans/CANP-WP-0002-format-open-questions.md new file mode 100644 index 0000000..ec47aee --- /dev/null +++ b/workplans/CANP-WP-0002-format-open-questions.md @@ -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 1–2 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 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. + +## 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.