Commit graph

44 commits

Author SHA1 Message Date
f41d1b705f CANP-WP-0006 T06: container image and smoke checks
Two-stage python:3.12-slim build with no toolchain in the runtime layer,
running non-root (uid 10001) and writing nothing to disk — its state is the
database. reference/ is a real build input, because the service delegates
validation to it so the two cannot disagree about what a valid package is.

Migrations deliberately do not run at start-up. A schema change is a deployment
step with its own rollback, not something that races between replicas.

tools/smoke.py asserts what can be known about a running service: liveness,
readiness, fleet health shape, migration revision, and that the index and
registry are queryable. stdlib only, so it runs inside the runtime image;
non-zero exit, so a deployment gate can call it directly.

Verified by running it, not by inspection: the container starts, all six checks
pass against it, the reference CLI installs a package from it over HTTP, and
the checks fail correctly against a wrong --expect-migration — so
migration-at-head is a real check rather than a decorative one. Without
--expect-migration the check can only confirm the schema is stamped at all, and
says so rather than implying it verified the head.

Cluster-level checks a rapp contract also names — NetworkPolicies present,
external secrets ready, private-Service-only, live image digest match — are
properties of the deployment and belong to rapp-canned-prompts.

The digest rapp.yaml would pin does not exist yet. The image was built locally
and verified, but never pushed; that digest exists only once the image is
published to the fleet registry, which needs credentials and is outward-facing
enough not to do unasked. The follow-on sequence for rapp-canned-prompts is
recorded in the workplan.

CANP-WP-0006 is finished. Service tests 33, reference 105.

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
2026-09-06 21:25:48 +02:00
39b499bcec chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 20:39:26 +02:00
custodian-sync
36124ae634 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-09-06:
  - update .custodian-brief.md for canned-prompts

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 20:39:24 +02:00
c076d8395e CANP-WP-0006 T05: HTTP registries, and one finding
--registry now accepts an http(s):// URL as well as a path, implemented with
urllib so reference/ keeps PyYAML as its only dependency.

This was the first real test of INTENT principle 10's claim that a hosted
registry layers on without changing package semantics. Verified against the
running service, almost everything survived the transport unaltered: identity
and its ambiguity rules (a bare id in two registries returns 409 over HTTP just
as it does locally), immutability of a published id@version (identical content
accepted, changed content refused, version bump accepted), strict packaging,
validation, and the index. A package published and then installed over HTTP was
byte-identical to its source — diff -r clean — and its canonical-fidelity eval
still passed after the round trip.

One thing did not survive: a URL is not a registry. A filesystem registry IS
one registry and section 20.1 names it from its directory; an HTTP service
HOSTS SEVERAL behind one base URL. The address therefore cannot name the
registry, so it must be named separately — --as when publishing, a qualified
reference when installing.

Recorded as section 20.4 rather than worked around silently in the client,
because the gap is in the specification's list of registry kinds, not in the
CLI. Publishing to an HTTP registry without --as fails with that explanation
rather than guessing a registry name.

Registry responses are treated as untrusted input (section 19): decode_files
refuses path traversal, with a test. The wire shape round-trips binary content
through base64, also tested.

Reference tests 99 -> 105.

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
2026-09-06 20:39:04 +02:00
56b1708b58 chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 20:30:08 +02:00
custodian-sync
1ac24edddc chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-09-06:
  - update .custodian-brief.md for canned-prompts

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 20:30:07 +02:00
a7e12d5e04 CANP-WP-0006 T04: publish API
POST /packages takes {registry, source, files}, where files is the same shape
GET /archives returns — so an archive round-trips into a publish without
translation and a mirror is a GET followed by a POST. A test verifies the
round-trip by digest rather than asserting it.

Validation is the reference implementation's, applied to the posted files
materialized in a temporary directory. Only reserved paths and
manifest-referenced files are stored (section 2), path traversal is refused,
identical re-publishes are accepted, and different content under the same
id@version is 409 (section 17).

The identity mechanism, stated plainly rather than implied: a single shared
bearer token proving the caller is the operator of this service. It is not
per-publisher identity — every token holder is indistinguishable — and auth.py
says so where someone might otherwise assume more.

With no token configured the service is read-only. That is the correct default
rather than an inconvenience: section 20.1 asks a registry to refuse
publication into a closed namespace it does not consider the publisher to own,
and an unauthenticated service considers nobody to own anything.

Namespace claims live in namespace_claims (migration 0002) and are enforced
here, which a filesystem registry cannot do at all — but only as precisely as
the identity allows. A closed namespace is protected from anonymous callers; it
cannot be attributed among several publishers. Per-publisher identity is
deferred and is the main thing between this and a registry several people can
publish to.

Migration hygiene found while adding 0002: autogenerate proposed an ALTER
COLUMN TYPE on package_files.package_version_id, because the foreign key's type
was left to inference and compared as a variant against a reflected plain type.
SQLite cannot alter a column type, so 0002 failed halfway — table created,
revision unstamped, the partially-applied state that is worst to debug later.
Fixed at the cause: the column is typed explicitly, and 0001 was corrected
rather than patched over, which is legitimate only because it has never run
outside this repo's tests. alembic check now reports no drift.

Health tests now compute the expected migration head from the script directory
instead of hardcoding it, so adding a migration cannot fail them spuriously.

Service tests 22 -> 33; reference unaffected at 99.

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
2026-09-06 20:30:00 +02:00
133e796eff chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 20:23:54 +02:00
custodian-sync
5e80d8ff28 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-09-06:
  - update .custodian-brief.md for canned-prompts

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 20:23:34 +02:00
5f47da8036 CANP-WP-0006 T03: read API
Search, versions, manifests, archives and the index, over HTTP.

The route shape is the decision worth recording. Package ids contain `/`, so
the obvious /packages/{id}/{version} is ambiguous under a greedy path
parameter. Rather than invent an HTTP-specific identifier, the routes speak the
format's own <registry>:<id>@<version> syntax and parse it — `:` and `@` are
both legal in a path segment, and each route keeps a distinct prefix so
greediness cannot swallow a neighbouring one. The API therefore exercises
section 3.2's reference notation instead of working around it.

A bare id present in more than one registry returns 409 with the candidates,
never a guess. 409 rather than 300 because the request is answerable once the
caller says which registry they meant. Omitting a version applies section
17.1's selector rules, so a prerelease is never chosen implicitly.

Validation is delegated to reference/, installed into the service environment
rather than reimplemented. One validator means the service and the CLI cannot
disagree about what a valid package is; a service accepting something the CLI
rejects would be the divergence this project exists to prevent. Importing it is
not changing it — reference/ stays the dependency-light conformance witness.

Storage keeps the format's distinctions: an immutable package version, its
files as content rather than parsed rows, and an index entry recording arrival.
Only reserved paths and manifest-referenced files are stored (section 2), and a
re-publish of identical content is accepted while different content under the
same id@version is a conflict (section 17).

Handles a real test-vs-production difference: SQLite autoincrements INTEGER
PRIMARY KEY only, never BIGINT, so the SQLite-backed tests could not insert a
row. BigInteger().with_variant(Integer, "sqlite") keeps BIGINT on PostgreSQL
while letting the tests exercise the same models and migration.

Verified live against a seeded store holding this repo's examples and four
helix-forge prompt packages. Service tests 11 -> 22.

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
2026-09-06 20:23:25 +02:00
d5782d4d04 chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 19:58:21 +02:00
custodian-sync
80dc1e0dc1 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-09-06:
  - update .custodian-brief.md for canned-prompts

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:58:02 +02:00
ae52931be5 CANP-WP-0006 T01-T02: service skeleton and tenant-keyed schema
The foundation of the hosted registry, in canned-prompts so rapp.yaml gets
ownership_repo: canned-prompts — the sbom-nexus shape, where product ownership
stays out of the operations repo.

Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic,
PostgreSQL, in service/ with its own environment. reference/ is deliberately
untouched: it is the format's conformance witness and stays dependency-light,
and the service is a separate consumer of the same package semantics.

CANNED_PROMPTS_DATABASE_URL has no default. A service that silently falls back
to a local database when its real one is misconfigured is worse than one that
refuses to start.

Health surface per RailianceAppDeploymentGuide.md: unauthenticated /healthz and
/readyz, plus /state/health for fleet consistency. /healthz deliberately checks
nothing beyond the process being up, so a database blip does not restart pods;
/readyz asks the database something it can fail to answer.

Migration 0001 creates package_versions, package_files and index_entries, every
one carrying a tenant key per business-app-service-contract section 1.3 — the
service is single-tenant today, and the key is present so a later consolidation
is a data copy rather than a rewrite. A test asserts every table in the metadata
is tenant-keyed, so adding an unkeyed table fails the suite rather than being
discovered at consolidation time. Uniqueness is (tenant, registry, package_id,
version): registry-scoped because identity is, tenant-scoped so two tenants may
hold the same id.

The schema keeps the format's three things distinct — an immutable package
version, its files as content rather than parsed rows, and an index entry
recording how a version arrived here.

Fixes a bug its own test caught: check_readiness first caught every failure in
one except and reported "database unreachable", so an unmigrated but perfectly
reachable database sent an operator to credentials and networking when the fix
was alembic upgrade. Connectivity and schema are now checked separately.

Service tests 11 passing; reference tests unaffected at 99.

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
2026-09-06 19:57:47 +02:00
65a86acdc2 chore(consistency): register CANP-WP-0006 and regenerate WORK-RECORDS.md
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
2026-09-06 19:49:18 +02:00
c65cc4ec1e Register CANP-WP-0006: hosted registry and index service
Toward a production deployment as rapp-canned-prompts on railiance01.

A rapp repo packages a deployable its ownership_repo already produces —
rapp.yaml pins upstream_components.version to a container digest.
canned-prompts has no deployable: a CLI with no HTTP surface, no image, no
database. A rapp pointing at nothing is a stub that tells the fleet's tooling a
deployment exists when none does. So the service comes first, and
rapp-canned-prompts is created once there is a digest to pin.

Decisions recorded: hosted registry plus index as the v1 surface, the service
living in canned-prompts so rapp.yaml gets ownership_repo: canned-prompts (the
sbom-nexus shape), and deployment as a platform service whose data is
nevertheless tenant-keyed from the first migration per
business-app-service-contract section 1.3 — cheap now, a rewrite later.

Stack matches state-hub and sbom-nexus: FastAPI, SQLAlchemy, Alembic,
PostgreSQL. The deployment guide additionally requires unauthenticated
/healthz and /readyz.

INTENT's deliberate boundary still holds: the service hosts packages and does
not become an agent runtime. reference/ stays untouched and dependency-light as
the format's conformance witness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM
EOF

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 19:48:55 +02:00
34ca1cdb7f chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 19:32:52 +02:00
e1889b41a1 CANP-WP-0003: name the default registry default
Section 20.1 takes a registry's name from its directory basename, so the
reference tool's default store at ~/.canned-prompts/registry was named
`registry` — giving `registry:practice/thing` as a qualified reference.

The workplan had recorded this as reading poorly in one output line. It was
understated: the name reaches qualified references, the on-disk catalog layout
(catalog/registry/...), and index.yaml rows (registry: registry). Once packages
are installed and inclusions recorded, changing it becomes a migration rather
than an edit.

Renaming the directory to ~/.canned-prompts/default fixes it with no special
case in code, nothing written into the user's store, and no second naming
mechanism competing with section 20.1's manifest.

check_legacy_registry refuses to silently create a fresh empty store beside a
populated ~/.canned-prompts/registry — that failure would have been worse than
the wart being fixed. It names the exact mv, and the --registry flag for
keeping the old store. The check fires only for the default path; an explicit
--registry is never second-guessed.

Tests 95 -> 99.

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
2026-09-06 19:32:30 +02:00
03826eac4f chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 19:23:44 +02:00
75e0597019 CANP-WP-0005: document and detect inclusion diamonds
Section 10.4 said nothing about inclusion being textual and repeated, so an
author met the behaviour only by reading rendered output carefully. That
silence was the defect.

Writing it up got the mechanism wrong at first. I documented a dependency
"reached by two paths" through two include chains, and wrote a detector that
counted composer calls to match. Tested it against the real case that motivated
the task — helix/repo-advance — and it did not fire, though the conventions
block still rendered twice.

The actual mechanism is subtler. helix/commit-sync declares its own
`conventions` input; when composed, resolution passes the including package's
already-resolved values down by name, so it *inherits* the outer text rather
than resolving its own include, and renders it again. Nothing was included
twice; the text appeared twice regardless.

Both mechanisms are now documented with worked diagrams. duplicate_inclusions
detects both — included values that are equal, and an included value contained
within another — and resolve, render and eval warn.

Nothing is deduplicated, as leaned. Deduplicating means choosing which
occurrence survives, since position in a prompt carries meaning, and deciding
what happens when two paths select different versions of the same dependency,
which section 10.3 permits. That is resolver behaviour and section 10.4 keeps
composition declarative.

Verified by reintroducing the diamond in a scratch copy of repo-advance, which
warns, and confirming the shipped factored collection stays silent.
Tests 90 -> 95.

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
2026-09-06 19:23:16 +02:00
e16bf024d4 chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 17:14:49 +02:00
b3280df742 Add the catalog index, and package a real prompt collection
CANP-WP-0004. The operator asked canned-prompts to build a database of
versioned prompts recording where each came from and when — the first step
toward a platform for collaborative prompting.

The format had nowhere to put that. `provenance` records who wrote a prompt and
where the idea came from; nothing recorded how a copy arrived in a particular
store. Section 20.3 now specifies an `index.yaml` as store metadata rather than
package data: how a copy arrived differs for every consumer, and recording an
arrival must never rewrite the package that arrived.

`add`, `install` and `publish` record registry, id, version, name, source,
method, first-inclusion date, and the package's declared author, source and
licence — the last three copied so a listing is readable without opening every
package. A new `index` verb lists it. `included_at` is never overwritten; a
re-run updates `last_seen_at`, because when a package first entered a
collection is a fact about history rather than about the last command run.

Tests 84 -> 90.

Also records two findings from actually using the format:

CANP-WP-0004-T03 — inclusion has no deduplication, so a diamond dependency
renders shared content once per path. Found by composing a real collection.
Not fixed here: deduplicating means choosing which occurrence survives and
deciding what happens when two paths resolve different versions, which is
resolver behaviour that section 10.4 deliberately avoids. Handed to
CANP-WP-0005 with a leaning: document it, warn at validation time, do not
deduplicate.

The add_ons workaround in practice/pqrst-estimate is evidence about section
23's deferred "richer template syntax" — an optional appendix has to be an
input with an empty default, because CPF has no conditionals.

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
2026-09-06 17:14:21 +02:00
f55ef13c75 Package the canonical PQRST prompt faithfully
examples/pqrst-estimate was not the canonical PQRST prompt. The canonical one
is ~/pqrst-practice/PqrstPrompt.md, normatively specified in
spec/PqrstEstimationPractice.md, and hall-of-helix CLOSING.md requires pasting
that block unmodified. Ours was a paraphrase with a different output shape — no
Confidence, no Signature, no Dominant factors — and CANP-WP-0002-T03 made it
worse by prepending a house_style inclusion to a prompt whose governing
document says do not modify it.

A copied prompt that silently forked its source is the exact failure INTENT.md
opens with, sitting in this repo's own examples directory.

The template is now the canonical block extracted programmatically rather than
retyped, and the default render is byte-identical to it: 2886 bytes both ways.

The source documents two optional add-ons appended after the block. CPF has no
conditionals, so they are not a flag: `add_ons` is an input defaulting to the
empty string, and each sanctioned add-on is an example fixture. This is worth
noting as evidence about section 23's deferred "richer template syntax" — the
workaround is adequate here, but it is a workaround.

evals/canonical-fidelity.yaml guards the property with sixteen render checks,
including not_contains checks naming the paraphrase this package used to be, so
the drift cannot silently recur.

Version 0.2.1 -> 1.0.0: changed inputs and a materially different intended
output is the MAJOR case in section 17.

Section 22's worked example and the house-style README both claimed
pqrst-estimate composes the style fragment. It no longer does, by design, so
both are corrected; composition is illustrated in section 10.4 instead.

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
2026-09-06 15:26:36 +02:00
55b8600946 chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 14:48:27 +02:00
4b2bda0013 Report unsatisfied dependencies on add and install
Found while verifying the repo against INTENT.md's eight success criteria for
a first practical release, after CANP-WP-0002 finished.

Criterion 5 is "publish a version to a registry and install it elsewhere".
Installing practice/pqrst-estimate into a fresh catalog reported success, and
rendering it then failed with "included package not found:
practice/house-style". CANP-WP-0002-T03 introduced this — composition made a
package installable but unrenderable, and nothing said so at install time.

The error was honest, but reaching it after a successful-looking install is
the silently-incomplete failure section 23 now names as the thing this format
avoids.

`add` and `install` now name declared prompt dependencies the target catalog
cannot satisfy, including a package present at no matching version. They do
not fetch anything: section 10 leaves dependency resolution to the consumer,
and auto-installing transitively is a resolver — a separate decision from
refusing to hand over a package that looks fine and is not.

Whether `install` should offer `--with-dependencies` is left open as a design
question rather than smuggled in as a bug fix.

Tests 81 -> 84. Recorded as CANP-WP-ADHOC-2026-09-06.

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
2026-09-06 14:48:08 +02:00
526e9abdfd chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 14:23:23 +02:00
8376a00a88 CANP-WP-0002 T06: revision v0.2, and section 23 rewritten
Closes the workplan. The format becomes `canned-prompt/v0.2`, and packages
declaring v0.1 remain valid — everything added across T01-T05 is additive, so
a v0.1 package means exactly what it always meant. That is the MINOR case
section 17 itself describes.

The spec file loses its version suffix: CannedPromptFormat-v0.1.md becomes
CannedPromptFormat.md, with the revision stated inside. One stable path that
never breaks a link, and no rename per revision; the version belongs in the
`format` string where tools actually read it.

Section 23 is rewritten into three parts rather than the planned two.
"Settled since v0.1" tables the five resolved questions against where each
rule now lives. "Still deferred" carries the eight unpromoted items plus
pattern-matching render checks. "Decided against" holds template inheritance
alone, because calling it deferred would misdescribe it — reopening it means
overturning a decision and answering four recorded objections, not filling a
gap.

Section 23 also names the two habits the five decisions turned out to share,
so later revisions follow them rather than rediscover them: separate the
deterministic half from the rest, and a package never asserts what it cannot
back.

The eval-rubric and registry-manifest schemas keep their own v0.1. They are
new in this revision and sit on their own version lines.

Reference CLI: ACCEPTED_FORMATS; an unknown revision is rejected naming what is
accepted. Tests 78 -> 81.

Example packages declare v0.2 and are bumped 0.1.0 -> 0.1.1 and 0.2.0 -> 0.2.1
as section 17 PATCH — metadata corrections with behavior unchanged.

Also refreshes section 22's worked example, which had drifted: it showed
pqrst-estimate at 0.1.0 with no composition, contradicting the package
actually in the repo. It now mirrors the real package and doubles as a
composition illustration.

CANP-WP-0002 is finished. CANP-WP-0003 carries forward the one residual: the
default registry's basename-derived name reads as `registry:`.

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
2026-09-06 14:22:45 +02:00
be675d33ad chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 14:16:29 +02:00
95a8bb31d2 CANP-WP-0002 T07: semver precedence and strict packaging
Two defects from the original review of the seed.

Prerelease ordering was worse than first recorded. parse_semver returned
(major, minor, patch, raw_string), so 1.0.0-rc1 and 1.0.0 tied on the numeric
fields and then compared as strings — "1.0.0-rc1" > "1.0.0". A release
candidate therefore shadowed its own release for `newest` and for `>=`, not
just for the no-version case.

parse_semver now returns a SemVer section 11 precedence key: numeric fields, a
release/prerelease rank, then dot-separated prerelease identifiers with
numeric ones compared numerically. Build metadata is ignored. Beyond ordering,
prereleases are excluded from `any`, `newest` and `>=` entirely; only an exact
pin selects one, so publishing a release candidate never changes what existing
consumers resolve to. A package holding only prereleases now says so rather
than reporting a bare not-found.

Packaging copied the whole source directory, so a stray .git, virtualenv or
scratch file landed in the catalog and registry. Section 2 already required
otherwise — tools MUST ignore unknown non-reserved files unless a manifest
field references them — so this is conformance rather than a new rule. What is
new is that omissions are reported instead of silent:

  not packaged (not a reserved path, not referenced by the manifest): .git/, .venv/, notes.txt

LICENSE joins the reserved paths. Strict packaging would otherwise drop a
package's license text while faithfully copying its `license` field, which
contradicts section 14's instruction to surface licensing on publish and
install.

Spec: 2 (LICENSE, packaging obligation, reporting), 17.1 new, 10.3 note.

Reference CLI: parse_semver rewritten with is_prerelease; select_version and
pick_version updated; copy_package and report_skipped replace copy_immutable.
Tests 65 -> 78.

examples/pqrst-estimate carries a LICENSE and a license field, exercising the
new reserved path.

Also fixes a leaked loop variable in package_members that would have reported
a bad `template` path as an `evals` error.

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
2026-09-06 09:32:42 +02:00
e885eb7b05 chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 08:12:24 +02:00
c580bf63c9 CANP-WP-0002 T05: required versus observed, and typed context dependencies
The answer to the capability question was conditional: keep both fields if
they carry the required/observed distinction, fix the terminology if they do
not. They did not.

Section 9 opened with "records known requirements or observations", mixing
both in one field — `models` was observational ("known to be compatible or
evaluated") while `capabilities` was prescriptive ("expected from the
execution environment"). Section 10 then described dependencies as what a
prompt "expects". Both fields said expected, so the overlap was real
ambiguity rather than redundancy, and the fix is terminology.

`dependencies` now means **required**; `compatibility` means **observed**. A
consumer must not refuse to run a package because its environment is absent
from a compatibility list. The same capability name may legitimately appear in
both: required to run at all, and separately observed to work well on
particular models. `compatibility.aliases` records the same capability under
other names, so a consumer can recognize a requirement its environment labels
differently.

Dependencies now have three kinds, separated by what the format can do about
them: `prompts` it resolves by id and version; `context` names what it does
not package at all; `capabilities` are what the environment must be able to
do. Context entries use `name` rather than `id`, because nothing can look them
up, and `description` is required because nothing else can explain an
unpackaged dependency. A capability takes no version and no
`requirement: generate` — it is not an artifact and cannot be fetched, pinned
or generated. Capability names are free-form kebab-case, validated for shape
and not membership, exactly as tags are.

Section 10.1 also draws the line the format had never stated: an input is
content the caller passes for one use; a context dependency is a standing fact
about the environment.

Spec: 9 rewritten, 9.1 and 10.1 and 10.2 new, 10 reframed, 18 (rules 19-20),
4 updated. Former 10.1/10.2 renumbered to 10.3/10.4 with cross-references.

Reference CLI: validate_capabilities, validate_context_dependencies, and a
`resolve` section listing required capabilities and context under "this tool
cannot verify these" rather than implying it checked. Tests 51 -> 65.

Also drops an invented `session-review` capability from the example package in
favour of an honest `long-context` observation.

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
2026-09-06 08:11:13 +02:00
074fd79d53 chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 01:38:41 +02:00
c5f1640454 CANP-WP-0002 T04: eval schema, render checks and output criteria
`evals/` was a reserved path holding unvalidated blobs: section 12 named the
directory and gave an illustrative snippet, but nothing was specified, so no
tool could act on an eval file.

Every eval file must now declare a `schema`, and CPF defines exactly one —
`canned-prompts/eval-rubric/v0.1`. Unrecognized schemas stay legal and are
skipped rather than rejected, so the format gains something actionable without
becoming an evaluation language, which remains a non-goal.

The schema splits along the same seam as T01 and T03. Render checks
(`contains`, `not_contains`, `resolves_all`) assert properties of the rendered
prompt text, need no model, and are therefore run by the reference CLI. Output
criteria describe a good result and are declared but not run, because judging
them requires a model. That division is now the format's consistent answer to
"deterministic locally, or not".

An eval references a fixture already declared in the manifest's `examples`
rather than carrying its own copy, so an example that is also an eval fixture
stays honest — both break together.

An eval declares assessment and must not record outcomes. Results are run
evidence and live outside the immutable package, per INTENT.md and section 17.

Spec: 12 rewritten with 12.1, 18 (rules 17-18), 21 (`eval` verb).

Reference CLI: read_eval, validate_eval, load_example_values,
run_render_checks, cmd_eval; a failed render check exits non-zero.
Tests 42 -> 51.

examples/pqrst-estimate/evals/quality.yaml is a real eval with four render
checks and four output criteria, and it passes.

Fixes a latent bug reaching a fixture exposed: coerce_value assumed every
value was a command-line string, so a YAML fixture carrying a real type
(include_rationale: true) crashed on .lower(). Typed values are now validated
but not re-parsed.

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
2026-09-06 01:38:11 +02:00
cac6bc135f chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 01:32:39 +02:00
4a56f20209 CANP-WP-0002 T03: composition by reference, two kinds
T01 had already delivered half of composition without naming it: a derived
default binds an input to a prompt dependency, which is transclusion — run
package B, use its output. What was missing was the deterministic half.

`include` inlines another package's rendered template as text. No model is
involved, so the reference CLI can actually perform it, and a shared preamble,
rubric or style block becomes a versioned package instead of copied text. This
is the concrete way to honor INTENT principle 9 without any runtime. `derive`
stays as it was. Both are input defaults, so composition reuses the resolution
machinery rather than adding a second one.

No template inheritance. Four of this repo's own documents argue against it:
INTENT principle 3 (hidden context defeats reuse), section 19's "make package
contents visible before execution", section 17's requirement that behavior
changes produce a new version, and the non-goal on range resolution.

Version selectors: an exact pin is the expected form, with `any`, `newest` and
`>= X.Y.Z` as explicit opt-ins so looseness is written rather than implied by
absence. Selectors are evaluated per dependency against what is available —
no solver, no cross-dependency constraint satisfaction — which is what keeps
them outside the range-resolution non-goal, and the spec says so.

Also defines `type` (template | fragment), which appeared once in the section 4
manifest surface and was specified nowhere.

Spec: 3.2 (type), 5.1 (inclusion resolution rule, renumbered), 6.1 (included
default), 10.1 and 10.2 (new), 18 (rules 14-16), 21.

Reference CLI: validate_version_selector, select_version,
prompt_dependencies replacing prompt_dependency_ids,
check_composition_reference, CatalogComposer with cycle detection, and
resolve_inputs gaining composer= and inherited=. Tests 21 -> 42.

Examples: house-style is a real fragment package; pqrst-estimate composes it
and is bumped 0.1.0 -> 0.2.0 per section 17.

Fixes an ordering bug found while testing: inputs resolved before parameters,
so an included package could not see the including package's parameters and
silently fell back to its own defaults — the fragment rendered tone=neutral
where the including package said blunt. Parameters now resolve first; the
report still lists inputs first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 01:31:58 +02:00
352ed5b30a chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 01:15:49 +02:00
ea70a59610 CANP-WP-0002 T02: registry-scoped identity and namespace ownership
Section 17 already defined what a registry does when the same id@version is
republished; nothing defined what a *consumer* does. That is where namespace
conflict actually bites — in the catalog, after installing from two
registries.

Identity is now registry-scoped: an id names a package within a registry, the
way a path names a file within a repository. A local-first format with no
signing, no federation and no central authority cannot enforce global
uniqueness, and an unenforceable guarantee is worse than none — it invites
consumers to conflate two packages that merely share a name. A qualified
`<registry>:<id>` reference distinguishes them, and `:` is now barred from ids
so the separator stays available.

Installing the same id from two registries is therefore not a conflict. The
catalog is namespaced by registry and keeps both.

Ownership is registry policy, not package data. An optional `registry.yaml`
names a registry and records namespace claims. Those claims are explicitly
descriptive — a filesystem registry cannot authenticate a publisher, and
`publish` says so rather than implying it checked. Keeping the claim out of
packages leaves artifacts free of unverifiable assertions of authority, and
means package semantics do not change when a hosted registry appears later.

Spec: 3.2 (registry-scoped identity, qualified references), 17 (immutability
scoped to a registry), 20.1 and 20.2 (new), 18 (registry-manifest
validation), 21 (qualified references, reserved `local` name).

Reference CLI: parse_reference, check_registry_name, read_registry_manifest,
registry_name, namespace_policy; registry_package_path and
catalog_package_path split; resolve_installed reports ambiguity and returns
the source registry; iter_catalog; `add --as`; closed-namespace warning on
publish. Tests 11 -> 21.

The catalog layout changed. An existing catalog is detected and reported with
instructions rather than failing as "package not found". Signing, trust
scoring and federation remain non-goals and were not touched.

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
2026-09-06 01:14:54 +02:00
4a8422e1aa chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 01:00:15 +02:00
169db25d25 CANP-WP-0002 T01: input defaults, static and derived
Closes the gap that made optional inputs unusable: rendering rule 5.1(4)
made any unresolved placeholder an error while inputs had no `default`, so
an input marked `required: false` and referenced from the template failed
every render in which the caller omitted it — including the spec's own
section 4 example.

Section 10 already carried the derivation mechanism (`requirement: generate`,
resolution deliberately undefined), so a derived default needed a binding
rather than a new concept: the input's default names a declared prompt
dependency.

Spec:
- 5.1 rewritten as "Resolution and rendering". Resolution may be
  non-deterministic and must report what it derived; rendering is
  deterministic and must not derive. A tool that handles only supplied
  values and static defaults is stated to be conforming.
- 6.1 (new) covers both declaration forms. Reference form is preferred, with
  the reason stated — an inline prompt is anonymous, so unversioned,
  unprovenanced and un-evaluable — and validators should warn when a
  published package derives inline.
- A derived default may declare a static fallback `value`. Without one the
  input stays unresolved, which is an error; derivation never silently
  yields empty content.
- 4, 10, 18 (rules 11-13), 19 (two new MUST NOTs), 21 updated accordingly.

Reference CLI:
- New `resolve` verb reporting the origin of every value.
- `Resolution` dataclass and `resolve_inputs`; `resolve_values` kept as a
  wrapper so existing callers are unaffected.
- `render` refuses with a specific error naming underivable inputs rather
  than substituting empty text.
- Tests 3 -> 11. Example package lifecycle re-verified end to end.

INTENT.md is unchanged: splitting resolve from render preserves success
criterion 4 (deterministic rendering) as written.

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
2026-09-06 00:59:20 +02:00
0caf065544 chore(consistency): regenerate WORK-RECORDS.md
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
2026-09-06 00:48:12 +02:00
custodian-sync
a7bb79fdd6 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-09-06:
  - update .custodian-brief.md for canned-prompts

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 00:47:38 +02:00
7d278e28b7 Add repo classification and record workplan review context
- .repo-classification.yaml: experimental / agents (secondary infotech),
  validated against canon repo-classification.allowed.yaml v1.1. Clears
  consistency checks C-24 and C-35.
- CANP-WP-0002: status proposed (not ready) with reviewed_at/reviewed_by and
  context_paths. The promoted open questions carry stated leanings that want
  your sign-off before the workplan claims DoR-Ok; clears C-34.
- WORK-RECORDS.md and workplan state_hub ids as written by fix-consistency.

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
2026-09-06 00:47:23 +02:00
custodian-sync
1963e4b335 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-09-06:
  - update .custodian-brief.md for canned-prompts

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 388925@bnt-lap001
Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
2026-09-06 00:45:44 +02:00
dc615ef530 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
2026-09-06 00:45:28 +02:00
985b41dc87 Initial commit 2026-09-05 19:31:54 +00:00