Spec 08 — Tooling: the normative tool surface
The enforced loop and the tools that apply the rules mechanically.
All authoring work on a Quoin site goes through a tool surface that enforces
the constitution mechanically. This spec is the contract for that surface.
The reference implementation is the standalone quoin-cli package (D-005):
one global CLI and one stdio MCP server, site-agnostic, resolving the site
from the working directory. Site repos contain no tool code; they expose the
site adapter (D-005).
1. The enforced loop
Every change follows one loop, and every arrow refuses when its invariant fails:
begin_change → put_page | put_menu | put_tokens | put_theme_file
→ validate → open_pr → CI build → merge
begin_changecreates achange/<slug>branch. It refuses onmainand on a dirty worktree.- Every write tool refuses on
main— Constitution §4 at the tool layer. validateruns the full schema, menu, ref, and theme-conformance gate without a full build.open_prrefuses while validation fails.- The CI build is the final gate (Constitution §2); merge is the only path to production (Constitution §4).
A tool MUST report refusals as structured, one-line reasons naming the invariant. A refused operation MUST NOT partially apply.
2. Tool catalogue
| Tool | Purpose | Enforcement highlights |
|---|---|---|
quoin_status |
Site state: identity, branch, collections and counts, migration stubs, placeholder flags, tokens, active theme and capabilities | read-only |
quoin_list_pages / quoin_get_page |
Enumerate and read pages | read-only |
quoin_begin_change |
Start a change branch | refuses on main, dirty tree, duplicate branch, bad slug |
quoin_put_page |
Create or update a content page | strict schema (§3); ref integrity; blob justification; update-intent rule (§4) |
quoin_put_menu |
Write a named menu | menu schema (exactly-one-of rule, parent exemption D-003); ref integrity |
quoin_put_tokens |
Write design-token values | validates merged set against the active theme manifest’s token list; unknown names refused listing the valid names; hex format |
quoin_put_theme_file |
Write any file inside any theme directory (themes/**) |
the jail (§5); post-write validation gate |
quoin_validate |
Full gate without a build | schemas + menus + refs + theme conformance; --all-themes checks content against every installed theme |
quoin_open_pr |
Commit, push, open the PR | refuses while validation fails; the only sanctioned shipping path for agent work |
quoin_preview |
Build, serve, open the rendered page in a browser | the visual review surface; --theme <name|all> renders any installed theme via a QUOIN_THEME build override without touching site.yaml |
quoin_blocks |
List block types available to the active theme | manifest-driven |
quoin_themes / quoin_theme_set / quoin_theme_chrome |
Multi-theme support (D-007) | themes read-only; theme_set edits ONLY the theme: field (one-line diff; refuses themes that cannot render current content); theme_chrome edits ONLY the chrome menu wiring |
quoin round (CLI, D-010) |
The round mechanism (spec/03 §7): fold all open change/* PRs, gate, build, stamp expiry ribbons, upload the browsable preview (+ optional review dir) to stamped append-only slugs |
refuses on dirty tree, fold conflicts, or any gate failure; reloads the site adapter from the fold; append-only slugs — never overwrites what a requestor saw; --dry-run |
3. Creation and update are distinct intents
put_page on a page id that already exists MUST be refused unless the caller
passes an explicit update flag (update: true). The result reports whether
the page was created or updated. Rationale: page ids are stable URLs; a
silent overwrite can mask a typo’d id landing on the wrong page, and the
editorial workflow treats “new page” and “edit page” as different intents
with different risk. The PR diff remains the authoritative record either way.
4. The theme jail
put_theme_file is path-jailed to themes/** (D-007):
- Any theme directory — active or not, existing or new — kebab-case dir names enforced. Building a theme alongside the live one requires no switch.
- Refuses absolute paths, any
..traversal, trailing-slash (directory) targets, and anything resolving outsidethemes/. - Refuses
src/styles/tokens.yamlwith a redirect toquoin_put_tokens(token values are schema-validated data). - Unreachable by any tool, always:
site.yamlexcept the theme knobs (quoin_theme_setedits only thetheme:field;quoin_theme_chromeedits only the chrome menu wiring — both PR-gated),trust.yaml,src/lib/**,src/content/**(useput_page/put_menu),tools/**,src/content.config.ts. - Every write is followed by the full validation gate; the write is kept on
failure (the agent iterates on the branch) but
open_prrefuses until validation passes.
Token semantics (companion fix, D-007): tokens.yaml is the site’s knob
set; a theme manifest declares which knobs it consumes. Validation requires
every consumed knob to exist (missing → build failure) and every site token
to be hex; site tokens a theme does not consume are allowed — other themes
may declare them.
5. Site adapter and discovery
Normative from D-005: the tool resolves the site by walking up from the
working directory to the first directory containing site.yaml (env override
QUOIN_SITE), then loads <site>/src/lib/schemas.ts — the site adapter
— and verifies the contract symbols on every load. A missing symbol is a hard
error naming the contract. Site repos contain no tool code.
6. Region annotation and inspect mode
Every page rendered by a theme MUST annotate its regions so a human can give precise, nameable feedback:
- Chrome landmarks carry
data-quoin-regionwith the reserved namessite-header,banner-nav,page-main,site-footer. - Each rendered block is wrapped with
data-quoin-region="block: <type>"(e.g.block: feature). Wrappers MUST be layout-neutral. - When a page is viewed with
?inspectin the query string, the theme draws labelled outlines over every annotated region (dashed border + name badge). Without the parameter the annotation MUST be fully inert — no styles, no layout effect, script no-op. - Region names are a reserved vocabulary: themes MUST NOT repurpose them, and
new region kinds (e.g. named columns) MUST follow the same
data-quoin-regionmechanism so tooling can enumerate them.
7. What no tool may reach
Whatever the enforcement boundary, some files change only through human PRs,
with no tool path at all: page-type schemas and the content config (the
structural vocabulary), site.yaml identity/contact/registration/footer
fields, trust.yaml (the dial), and the tool surface itself. This is
deliberate: the rules that define the enforcement boundary must sit outside
it.