Create a doctrine artifact
This guide walks through authoring one new doctrine artifact — from picking a kind through
verifying it is live in governed mission context. It uses a tactic as the worked example
because tactics have the simplest schema and the most built-in precedent to copy from, but the
same six steps apply to any of the eight doctrine artifact kinds. The
loose-contract asset kind (a shipped blob, not an activatable artifact) works differently and
has its own short recipe at the end: Author an asset.
This guide covers project-tier artifacts — the fast, self-serve path for one project's own doctrine. If you are building a shareable org pack (doctrine distributed across multiple projects), the file layout differs; see Understanding the Org Doctrine Layer after finishing this guide.
Prerequisites
- A Spec Kitty project with governance set up (
.kittify/charter/charter.mdexists — see How to Set Up Project Governance if it does not yet). - The
spec-kittyCLI on yourPATH.
Step 1: Pick a kind and its project directory
Every kind has its own directory under .kittify/doctrine/, its own file suffix, and its own
schema. Project-tier directories use singular names for four kinds and plural names for the
rest — this is a real, code-verified asymmetry, not a typo. The mapping has a single canonical
home: PROJECT_KIND_DIRS in src/doctrine/artifact_kinds.py (the lowest doctrine layer, imported
downward by charter and the CLI — there is no second copy to drift). The table below is that
mapping:
| Kind | Project directory | File suffix | Schema | ID field |
|---|---|---|---|---|
directive |
.kittify/doctrine/directive/ |
.directive.yaml |
directive.schema.yaml |
id |
tactic |
.kittify/doctrine/tactic/ |
.tactic.yaml |
tactic.schema.yaml |
id |
styleguide |
.kittify/doctrine/styleguide/ |
.styleguide.yaml |
styleguide.schema.yaml |
id |
procedure |
.kittify/doctrine/procedure/ |
.procedure.yaml |
procedure.schema.yaml |
id |
toolguide |
.kittify/doctrine/toolguides/ |
.toolguide.yaml |
toolguide.schema.yaml |
id |
paradigm |
.kittify/doctrine/paradigms/ |
.paradigm.yaml |
paradigm.schema.yaml |
id |
agent_profile |
.kittify/doctrine/agent_profiles/ |
.agent.yaml |
agent-profile.schema.yaml |
profile-id |
mission_step_contract |
.kittify/doctrine/mission_step_contracts/ |
.step-contract.yaml |
(Pydantic model, no standalone JSON Schema file) | id |
Schemas live under src/doctrine/schemas/. If you are working from a project that installed
spec-kitty as a package rather than from this source checkout, the fastest way to see a kind's
required fields is to copy a real built-in file of that kind and edit it — every built-in
artifact under packs/built-in/<kind-plural>/ is already schema-valid.
This walkthrough creates a tactic, so the target directory is .kittify/doctrine/tactic/.
Step 2: Choose an ID
Doctrine artifact IDs are the config-stem — the filename with its kind suffix stripped — and for
most kinds must match ^[a-z][a-z0-9-]*$ (kebab-case, starting with a letter; see the id
pattern in tactic.schema.yaml). This ID is what you pass to spec-kitty charter activate, so
pick something you'll type again: example-driven-api-design, not Tactic For API Design.
Step 3: Write the artifact file
Create .kittify/doctrine/tactic/example-driven-api-design.tactic.yaml. A tactic's schema
(src/doctrine/schemas/tactic.schema.yaml) requires id, schema_version, name, and at
least one step (each step requires at least a title):
schema_version: "1.0"
id: example-driven-api-design
name: Example-Driven API Design
purpose: >
Design a new API surface by writing the concrete request/response examples first,
then deriving the interface from what makes the examples read cleanly. Prevents
designing an interface that is technically coherent but awkward for real callers.
steps:
- title: Write three realistic call examples
description: >
Before writing any interface signature, write out three concrete example calls a
real caller would make, including the exact request and response shape you want
them to see.
examples:
- "Good: a runnable curl example with real field values, not placeholders."
- title: Derive the interface from the examples
description: >
Only after the examples read naturally, write the interface (types, endpoint
names, parameters) that would make those exact examples true.
- title: Check for awkward callers
description: >
Re-read each example as if you were a caller who has never seen the design.
If an example needs a comment to explain why a field is shaped the way it is,
the interface needs another pass.
failure_modes:
- "Skipping straight to the interface and retrofitting examples afterward — the examples end up justifying the design instead of shaping it."
Every field here maps directly onto the schema: schema_version must be the literal string
"1.0"; steps is a non-empty list of objects with at least a title; purpose and
failure_modes are optional but recommended (see the real built-in
problem-decomposition.tactic.yaml cited on the doctrine kinds page
for a fuller example with references to other tactics).
For a different kind, swap the required fields per the table in Step 1 — for example an
agent_profile additionally requires purpose, specialization, and either role or roles
(see src/doctrine/schemas/agent-profile.schema.yaml), and its ID field is profile-id, not
id.
Step 4: Confirm the artifact is discovered
Project-tier doctrine is read directly off disk — no separate "import" step. Confirm your new file is found and parses:
spec-kitty charter list --show-available
Your new tactic should appear as an available-but-not-yet-activated ID under the tactic row.
If it does not appear, re-check the filename suffix (.tactic.yaml, not .yaml) and the
directory (.kittify/doctrine/tactic/, singular).
Step 5: Activate it
An artifact existing on disk is not the same as it being active — activation is what makes an artifact eligible for context injection into governed mission actions. Activate by kind and ID:
spec-kitty charter activate tactic example-driven-api-design
This is a fast, config-only write to .kittify/config.yaml's activated_tactics list (see
plan_activation/commit_plan in src/charter/activation_engine.py) — it does not by itself
regenerate the derived bundle. If your new tactic references other artifacts (via a references
field) that are not yet activated, the command warns you and suggests --cascade:
# Activate the tactic and everything it references, in one pass
spec-kitty charter activate tactic example-driven-api-design --cascade all
# Also eagerly refresh the derived bundle/DRG immediately (otherwise this
# happens lazily on the next synthesize)
spec-kitty charter activate tactic example-driven-api-design --resynthesize
Kinds that skip explicit activation.
agent_profileandmission_step_contractdo not use theactivated_<kind>list at all — all built-ins for those two kinds are available without an activation step (spec-kitty charter list --allreports them as "All built-ins — no explicit activation"). If you are authoring one of those two kinds, Steps 4–5 collapse into "confirm the file is present and well-formed"; there is nocharter activatecall to make.
Step 6: Verify it took effect
# Confirm the ID now shows under "Activated" for its kind
spec-kitty charter list
# Confirm overall charter health
spec-kitty charter status
# Confirm the artifact actually surfaces in a real mission action's context —
# pick an action your tactic is relevant to
spec-kitty charter context --action specify --json
If charter status reports the bundle as stale, run spec-kitty charter synthesize (dry-run
first) to promote it — see
How to Synthesize and Maintain Doctrine for the full
synthesis workflow. If something looks wrong at any step, spec-kitty doctor doctrine and
Troubleshooting Charter Failures are the first places to
check.
Modeling relationships between artifacts, including tension
Every relationship between doctrine artifacts — obligation (requires,
suggests), lineage (specializes_from), augmentation (enhances,
overrides), and tension (in_tension_with, reconciles_tension,
rejects) — is an authored DRG edge in a graph.yaml fragment, never a
field on the artifact body. The full authoring reference — worked examples
for every relation, plus the URN-ordering mechanic for in_tension_with — is
Doctrine relationships; see
its "Tension vocabulary" section
for how to model two co-valid artifacts that disagree (for example
DIRECTIVE_024's locality-of-change vs. DIRECTIVE_025's boy-scout rule) and
how to bridge a tension pair with a reconciler.
opposed_by is retired, not an authoring option. Use in_tension_with /
reconciles_tension / rejects instead — see the linked reference above for
the full semantics. It survives only as a legacy input an unmigrated
org/downstream pack may still carry; spec-kitty migrate rewrite-opposed-by --pack <path> rewrites those legacy entries into the correct typed edges
(idempotent, safe to run repeatedly).
Undoing this
spec-kitty charter deactivate tactic example-driven-api-design
Deactivating removes the ID from activated_tactics; it does not delete the file. Delete
.kittify/doctrine/tactic/example-driven-api-design.tactic.yaml directly if you want the
artifact gone entirely.
Author an asset (a shipped blob)
The walkthrough above covers the eight activation kinds. The asset kind works differently and
gets its own short recipe here — it is the canonical way to ship an image, font, template fixture,
or an executable script (a lint, a hook) to a downstream repo, instead of naming a repo-local
scripts/… or .github/… path a consumer does not have (see
review-gates.md). An asset is a blob plus a sidecar
manifest; there is no schema on the blob and — unlike the eight kinds above — no
charter activate step. It is delivered when a reachable artifact points at it, not when you
activate it (see Delivery verdicts).
This recipe is executable against a fresh project — copy it verbatim.
Step A: place the blob
The project-tier asset directory is .kittify/doctrine/assets/ (from the single canonical
PROJECT_KIND_DIRS mapping). Put the blob there. For a worked example, a shared release checklist:
mkdir -p .kittify/doctrine/assets
printf '# Release checklist\n- [ ] Tests green\n' > .kittify/doctrine/assets/team-release-checklist.md
Step B: write the sidecar manifest
Alongside the blob, create a manifest named <blob>.asset.yaml — here
.kittify/doctrine/assets/team-release-checklist.md.asset.yaml. The manifest is the validated
surface; it requires id, mime, and path, with an optional title:
id: team-release-checklist
mime: text/markdown
path: team-release-checklist.md
title: Team release checklist
Field rules (doctrine.assets.models.AssetManifest, enforced by the pack validator):
id— a stable identifier, unique per pack per kind. This is what you resolve by.mime—type/subtypeform (e.g.text/markdown,image/png); when the extension implies a type,mimemust agree with it.path— the blob's path relative to theassets/root. It must resolve inside that root: an absolute path, a..-escape, or a symlink that leaves the root is rejected (NFR-006 containment).title— optional human-facing display name.
Step C: resolve it
Assets are not activated — they are resolved on demand, from any installation, with no charter step. Confirm the asset is discoverable and resolves to your blob:
# List every resolvable asset and its source tier (built-in / org / project)
spec-kitty doctrine asset list
# Resolve one identifier to a filesystem path (exit 0 on success;
# an unknown id exits non-zero and names the id)
spec-kitty doctrine asset path team-release-checklist
The path command prints the absolute path to your blob and exits 0. Downstream code (a mission
step, a hook, a shipped lint) consumes the asset by calling spec-kitty doctrine asset path <id>
and reading the file at the returned path — never by hard-coding a source-tree path. A more
specific tier wins: a project or org asset of the same id shadows the built-in, and the shadow is
reported by asset list.
There is nothing to undo — no activation entry was written. Delete the blob and its
*.asset.yaml manifest to remove the asset entirely.
See also
- Doctrine artifact kinds — what each of the eight kinds is for, with a real example of each.
- Doctrine relationships — the full DRG relation
reference, including the tension vocabulary (
in_tension_with,reconciles_tension,rejects) that supersedes the retiredopposed_byfield. - Understanding the Org Doctrine Layer — how to package and share doctrine artifacts across multiple projects instead of authoring them project-local.
- How to Synthesize and Maintain Doctrine — the broader synthesis/resynthesis maintenance workflow this guide's Step 6 hands off to.