SPEC-021
ID:SPEC-021Status:draft

Plan Runes

Runes for spec-driven project management in AI-native development workflows. Package: @refrakt/plan.

v1.0

Problem

Spec-driven development has emerged as the dominant workflow for AI-assisted coding. Developers write specifications in Markdown, break them into work items, and AI agents implement from them. The tools supporting this workflow — GitHub’s Spec Kit, CCPM, planning-with-files — all converge on the same pattern: Markdown files in a directory with ad hoc conventions for structure.

These conventions are fragile. A task file is just headings and checkboxes. The AI parses it by guessing at the format. There’s no schema validation, no cross-referencing between specs and work items, no dependency tracking, no visual interface. The planning content is raw files that never render into anything browsable.

The plan runes provide structured, schema-validated, cross-referenced, renderable plan management content using the same rune system that powers the rest of refrakt.md.

Design Principles

Specs are the primary artefact. Not user stories, not tickets. The specification describes what the software should do. Work items reference specs. Decisions support specs. AI agents implement from specs. The project is organised around specs and their implementation status.

Acceptance criteria are the contract. They are verifiable statements that both humans and AI agents can check against. Every work item must have them. The content model enforces this.

Decisions are institutional memory. Architecture decision records capture why the system is the way it is. AI agents read them to make consistent choices. Without decision records, every AI session starts from zero context.

No agile ceremony baggage. No sprints, no story points, no velocity, no burndown charts. These concepts were designed around human team coordination constraints that don’t apply when AI agents do most of the implementation. Instead: specs, work items, milestones, and decisions.

AI-native from the start. Structured content models that AI agents can read, navigate, and author. The entity registry provides a knowledge graph. Validation ensures completeness. The claude.md convention orients agents to the project.

Rune Inventory

RunePurpose
specSpecification document with status tracking and version
workWork item with acceptance criteria, references, and implementation tracking
bugBug report with reproduction steps and severity
decisionArchitecture decision record
milestoneNamed release target with scope and goals
backlogFiltered, sorted view of work items and bugs
decision-logChronological view of all decisions

spec

Wraps a specification document, giving it status tracking, versioning, and entity registry integration. Specs are the source of truth for what the software should do.

Attributes:

AttributeTypeRequiredValuesDescription
idStringYesUnique identifier (e.g., SPEC-008)
statusStringNodraft, review, accepted, superseded, deprecatedCurrent status
versionStringNoVersion (e.g., 1.0, 1.2)
supersedesStringNoID of the spec this replaces
tagsStringNoComma-separated labels

Content model:

contentModel: {
  type: 'sequence',
  fields: [
    { name: 'title', match: 'heading', optional: false,
      template: '# Specification Title',
      description: 'Spec headline' },
    { name: 'summary', match: 'blockquote', optional: true,
      template: '> Brief description of scope and purpose.',
      description: 'Scope summary' },
    { name: 'body', match: 'any', optional: true, greedy: true,
      description: 'Full specification content — prose, diagrams, examples, code' },
  ],
}

The spec rune is intentionally minimal in structure. The body is freeform because specs vary wildly in shape — some are narrative, some are tables of attributes, some are code examples. The rune adds metadata and entity registration, not content structure.

Example:

{% spec id="SPEC-008" status="accepted" version="1.2" tags="tint,theming" %}

# Tint Rune

> Section-level colour context override via CSS custom properties.

## Problem

A page has a single colour context...

## Solution

`tint` is a core rune that overrides colour tokens within its parent
rune's scope...

{% /spec %}

Entity registration: The spec registers in the entity registry with its ID, title, status, and version. Work items reference specs by ID. The cross-page pipeline resolves these references to navigable links. The plan dashboard can show implementation coverage — which specs have work items, which are fully implemented, which have no work started.

work

A discrete piece of implementation work. Not a user story — no "as a / I want / so that" ceremony. A work item has a clear description of what needs to change, acceptance criteria that define done, and references to the specs and decisions that inform it.

Attributes:

AttributeTypeRequiredValuesDescription
idStringYesUnique identifier (e.g., RF-142)
statusStringNodraft, ready, in-progress, review, done, blockedCurrent status
priorityStringNocritical, high, medium, lowPriority level
complexityStringNotrivial, simple, moderate, complex, unknownComplexity signal for prioritisation
assigneeStringNoPerson or agent working on this
milestoneStringNoMilestone this belongs to
sourceStringNoComma-separated IDs of specs/decisions this implements
tagsStringNoComma-separated labels

Content model:

contentModel: {
  type: 'sequence',
  fields: [
    { name: 'title', match: 'heading', optional: false,
      template: '# Work item title',
      description: 'What needs to be done' },
    { name: 'description', match: 'paragraph', optional: true, greedy: true,
      template: 'Description of what needs to change and why.',
      description: 'Context and motivation' },
    {
      name: 'sections',
      type: 'sections',
      sectionHeading: 'heading:2',
      knownSections: {
        'Acceptance Criteria': {
          alias: ['Criteria', 'AC', 'Done When'],
          type: 'sequence',
          fields: [
            { name: 'criteria', match: 'list', optional: false },
          ],
        },
        'Edge Cases': {
          alias: ['Exceptions', 'Corner Cases'],
          type: 'sequence',
          fields: [
            { name: 'cases', match: 'list', optional: false },
          ],
        },
        'Approach': {
          alias: ['Technical Notes', 'Implementation Notes', 'How'],
          type: 'sequence',
          fields: [
            { name: 'approach', match: 'any', optional: true, greedy: true },
          ],
        },
        'References': {
          alias: ['Refs', 'Related', 'Context'],
          type: 'sequence',
          fields: [
            { name: 'refs', match: 'list', optional: true },
          ],
        },
        'Verification': {
          alias: ['Test Cases', 'Tests'],
          type: 'sequence',
          fields: [
            { name: 'verification', match: 'any', optional: true, greedy: true },
          ],
        },
      },
      sectionModel: {
        type: 'sequence',
        fields: [
          { name: 'body', match: 'any', optional: true, greedy: true },
        ],
      },
    },
  ],
}

complexity instead of story points. Complexity is a qualitative signal, not a numerical estimate. It helps humans prioritise and helps AI agents gauge what they’re getting into:

ValueSignal
trivialSingle file change, obvious implementation
simpleOne package, clear approach, few edge cases
moderateMultiple files/packages, some design decisions needed
complexCross-cutting change, architectural implications, many edge cases
unknownNeeds investigation before complexity can be assessed

Example:

{% work id="RF-142" status="ready" priority="high" complexity="moderate" 
       milestone="v0.5.0" tags="tint,theming" %}

# Implement tint rune dark mode support

The tint rune currently handles single-scheme colour tokens. It needs
to support dual light/dark definitions so that tinted sections look
correct regardless of the user's colour scheme preference.

## Acceptance Criteria
- [ ] Tint rune accepts `## Light` and `## Dark` content sections
- [ ] Identity transform emits `data-tint-dark` when dark values present
- [ ] Theme CSS swaps tokens in `prefers-color-scheme: dark` media query
- [ ] Inline tints without dark values fall back to page tokens in dark mode
- [ ] Inspector audits contrast ratios for both light and dark tint variants

## Edge Cases
- Tint with only dark values and `mode="dark"` — should work without light section
- Nested tints — inner tint should override outer tint's dark values
- Sandbox inside tinted section — should inherit dark tint tokens

## Approach
The identity transform parses `## Light` / `## Dark` headings within the
tint child rune body using the sections content model pattern. Dark values
are emitted as `--tint-dark-*` CSS custom properties alongside the light
values. The theme's base CSS swaps them via `@media (prefers-color-scheme: dark)`.

> Depends on {% ref "RF-138" /%}

## References
- {% ref "SPEC-008" /%} (Tint Rune Specification)
- {% ref "ADR-007" /%} (CSS custom properties for token injection)

## Verification
```markdoc
{% recipe name="Test" %}
{% tint %}
## Light
- background: #fdf6e3
## Dark
- background: #2a2118
{% /tint %}
{% /recipe %}
```

Expected: `data-tint-dark` attribute present, both `--tint-background`
and `--tint-dark-background` in inline style.

{% /work %}

bug

Bug report with structured reproduction steps. Separate from work because bugs have different required sections (reproduction steps, expected/actual behaviour) and different status values.

Attributes:

AttributeTypeRequiredValuesDescription
idStringYesUnique identifier
statusStringNoreported, confirmed, in-progress, fixed, wontfix, duplicateCurrent status
severityStringNocritical, major, minor, cosmeticImpact level
assigneeStringNoPerson or agent working on this
milestoneStringNoMilestone for the fix
sourceStringNoComma-separated IDs of specs/decisions this relates to
tagsStringNoComma-separated labels

Content model:

contentModel: {
  type: 'sequence',
  fields: [
    { name: 'title', match: 'heading', optional: false,
      template: '# Bug title' },
    {
      name: 'sections',
      type: 'sections',
      sectionHeading: 'heading:2',
      knownSections: {
        'Steps to Reproduce': {
          alias: ['Reproduction', 'Steps', 'Repro'],
          type: 'sequence',
          fields: [
            { name: 'steps', match: 'list:ordered', optional: false },
          ],
        },
        'Expected': {
          alias: ['Expected Behaviour'],
          type: 'sequence',
          fields: [
            { name: 'expected', match: 'any', optional: false, greedy: true },
          ],
        },
        'Actual': {
          alias: ['Actual Behaviour'],
          type: 'sequence',
          fields: [
            { name: 'actual', match: 'any', optional: false, greedy: true },
          ],
        },
        'Environment': {
          alias: ['Env'],
          type: 'sequence',
          fields: [
            { name: 'environment', match: 'list', optional: false },
          ],
        },
      },
      sectionModel: {
        type: 'sequence',
        fields: [
          { name: 'body', match: 'any', optional: true, greedy: true },
        ],
      },
    },
  ],
}

Example:

{% bug id="RF-201" status="confirmed" severity="major" %}

# Showcase bleed breaks with overflow:hidden parent

## Steps to Reproduce
1. Create a feature section with a parent that has `overflow: hidden`
2. Add a showcase with `bleed="top"` inside the feature
3. Observe the rendered output

## Expected
Showcase extends above the section boundary with visible displacement.

## Actual
Showcase is clipped at the section edge.

## Environment
- Browser: Chrome 122, Firefox 124
- Theme: default
- refrakt.md: v0.4.2

{% /bug %}

decision

Architecture decision record. Captures the context, options considered, the decision made, the rationale, and the consequences. The most important rune in the package for AI-native workflows — without decision records, every AI session lacks the "why" behind the system’s architecture.

Attributes:

AttributeTypeRequiredValuesDescription
idStringYesIdentifier (e.g., ADR-007)
statusStringNoproposed, accepted, superseded, deprecatedDecision status
dateStringNoDate decided (ISO 8601)
supersedesStringNoID of the decision this replaces
sourceStringNoComma-separated IDs of specs/entities this decision informs
tagsStringNoComma-separated labels

Content model:

contentModel: {
  type: 'sequence',
  fields: [
    { name: 'title', match: 'heading', optional: false,
      template: '# Decision title' },
    {
      name: 'sections',
      type: 'sections',
      sectionHeading: 'heading:2',
      knownSections: {
        'Context': {
          type: 'sequence',
          fields: [
            { name: 'context', match: 'any', optional: false, greedy: true },
          ],
        },
        'Options Considered': {
          alias: ['Options', 'Alternatives'],
          type: 'sequence',
          fields: [
            { name: 'options', match: 'any', optional: false, greedy: true },
          ],
        },
        'Decision': {
          type: 'sequence',
          fields: [
            { name: 'decision', match: 'any', optional: false, greedy: true },
          ],
        },
        'Rationale': {
          alias: ['Reasoning', 'Why'],
          type: 'sequence',
          fields: [
            { name: 'rationale', match: 'any', optional: false, greedy: true },
          ],
        },
        'Consequences': {
          alias: ['Impact', 'Trade-offs'],
          type: 'sequence',
          fields: [
            { name: 'consequences', match: 'any', optional: false, greedy: true },
          ],
        },
      },
      sectionModel: {
        type: 'sequence',
        fields: [
          { name: 'body', match: 'any', optional: true, greedy: true },
        ],
      },
    },
  ],
}

Example:

{% decision id="ADR-007" status="accepted" date="2026-03-11" source="SPEC-024" tags="tint,css" %}

# Use CSS custom properties for tint token injection

## Context
Tint runes need to override colour tokens within a section scope.
The solution must work without JavaScript and cascade through nested elements.

## Options Considered
1. **CSS custom properties on the container** — inline styles setting `--tint-*`
   tokens, theme bridges via `var()` fallbacks.
2. **Generated CSS classes per tint combination** — build step creates
   per-tint classes. Avoids inline styles but combinatorial explosion.
3. **JavaScript runtime token injection** — behaviour script reads data
   attributes and sets styles. Most flexible but requires JS.

## Decision
CSS custom properties via inline styles on the container element.

## Rationale
Custom properties cascade naturally through the DOM subtree without
JavaScript. Themes opt into tint support by including bridge CSS.
The `--tint-*` namespace avoids collisions with theme-internal tokens.

## Consequences
- Themes must include the tint bridge CSS
- Inline styles cannot use media queries — dark mode handled separately
- Inspector must audit tint token contrast ratios

{% /decision %}

milestone

A named release target or goal. Not a sprint — no timebox, no velocity, no ceremonies. A milestone is a coherent set of capabilities that together deliver value. When all work items assigned to it are done, the milestone is complete. If it takes three days or three weeks, that’s fine.

Attributes:

AttributeTypeRequiredValuesDescription
nameStringYesMilestone name (e.g., v0.5.0)
targetStringNoTarget date (aspirational, not a commitment)
statusStringNoplanning, active, completeCurrent status

Content model:

contentModel: {
  type: 'sequence',
  fields: [
    { name: 'title', match: 'heading', optional: true },
    { name: 'goals', match: 'list', optional: true,
      template: '- Goal one\n- Goal two\n- Goal three',
      description: 'What this milestone delivers' },
    { name: 'notes', match: 'paragraph', optional: true, greedy: true,
      description: 'Context, retrospective notes, lessons learned' },
  ],
}

The milestone automatically includes a backlog view of all work items and bugs with milestone matching its name.

Example:

{% milestone name="v0.5.0" target="2026-03-29" status="active" %}

# v0.5.0 — Layout & Tint

- Complete alignment system migration
- Ship tint rune with dark mode support
- Publish layout spec as site documentation
- Resolve showcase bleed overflow bug

{% /milestone %}

Renders the goals followed by all work items and bugs assigned to milestone:v0.5.0, grouped by status, with aggregate progress (checked acceptance criteria across all items).

backlog

Aggregation rune that queries the entity registry and renders a filtered view of work items and bugs.

Attributes:

AttributeTypeRequiredValuesDescription
filterStringNoFilter expression: field:value pairs
sortStringNopriority, status, id, assignee, complexity, milestoneSort order
groupStringNostatus, priority, assignee, milestone, type, tagsGroup items by field
showStringNoall, work, bugWhich entity types to include

Filter syntax:

{% backlog filter="status:ready priority:high" %}
{% backlog filter="milestone:v0.5.0" sort="priority" group="status" %}
{% backlog filter="assignee:bjorn status:in-progress" %}
{% backlog filter="tags:tint" show="work" %}