ckb/.agents/modules/software/skills/ckb-spec/SKILL.md
Michał Kopeć 0c06cb64ab Restore point before wiki reset
Commit all in-flight work — ckb-module and ckb-reset skills, the
.agents/modules/ scaffold, OPENSPEC docs, decision records D-0001 and
D-0002, graph edges and workload summaries — so the reset that follows
is fully recoverable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 17:56:41 +02:00

9 KiB


Spec skill

Purpose

A code map says what the software is. A spec says what it is supposed to do — and a change proposal says what should become true next. This skill owns that axis, across two levels that deliberately differ:

Level Holds Owned by
openspec/ at the KB root Cross-cutting capabilities and contracts spanning several src/ repos. The what and why. This skill. A root openspec/ has no code beneath it and no single repo governing it, so OpenSpec's own repo-shaped workflow doesn't serve it.
src/<repo>/openspec/ How one repo implements those capabilities. The how. OpenSpec itself. This is its native case: specs travel with the code and ride along in the repo's own PRs.

The split is the point, and so is the risk it creates: two places a statement can live is two places it can diverge. The implements / implemented_by links below exist so divergence becomes a lint finding rather than a silent second source of truth. Maintaining those links is not optional bookkeeping — it is the reason the two-level arrangement is safe.

Trigger phrases

  • "write a spec" / "add a capability" / "what specs do we have" / "which spec covers X"
  • "propose a change" / "draft a change proposal" / "archive this change"
  • "sync the specs" / "wire OpenSpec into the wiki" / "are the specs in sync"

Scope rule

Writes openspec/ at the KB root, and wiki/. Inside src/<repo>/ it reads freely and writes only through OpenSpec's own workflow, on the user's explicit instruction. It never commits or pushes a src/ repo.

KB-root specs

Layout

openspec/
├── README.md            # what this layer is for
├── specs/
│   └── <capability>.md  # one capability, current truth
└── changes/
    ├── <change-id>/     # proposed delta, not yet true
    └── archive/         # applied changes, immutable

A KB-root capability spec

---
type: spec
spec_id: billing-invoicing
tldr: One sentence stating the capability, in the present tense, as a contract.
status: active
implemented_by: /src/billing/openspec/specs/invoicing.md, /src/portal/openspec/specs/invoice-view.md
affects: /wiki/entities/billing.md
confidence: 0.9
quality: 0.8
last_updated: YYYY-MM-DD
freshness_window_days: 180
retention: high
---

Body: Purpose (why this capability exists), Requirements (each one testable — "the system SHALL ..." beats "the system should be fast"), Out of scope, Implementations (which repos, dual-linked), Sources.

Write requirements a reader can disprove. A requirement nobody can fail is not a requirement, and a spec full of them reads as thorough while constraining nothing.

Writing one

  1. Check openspec/specs/ for an existing capability on the subject. Extend it rather than creating a near-duplicate — two overlapping specs is the failure this layer exists to prevent.
  2. Check the cascade (wiki/linked/libs/) for what the KB already knows about the subject, and link to it rather than restating it.
  3. Write the spec, then a type: spec mirror page under wiki/entities/ (or link the spec file directly from the relevant entity page — prefer one home plus links over two copies of the text).
  4. Log it per Rule B and update wiki/entities/index.md.

Per-repo specs

Delegate. If src/<repo>/openspec/ exists, follow the instructions OpenSpec installed there — do not substitute a workflow of your own, and do not "improve" its file format to match this KB's conventions. That repo's specs are governed by OpenSpec upstream; divergence there breaks its tooling and its PR flow.

Never run openspec init at the KB root

openspec init writes tool-integration files into .claude/skills/ and adds marker blocks to AGENTS.md / CLAUDE.md. At the KB root both are load-bearing: .claude/skills is a symlink to .agents/skills (so OpenSpec's files would join this KB's skill set), and CLAUDE.md is a symlink to AGENTS.md, the KB's own system prompt.

Run it only inside src/<repo>/, where the repo gets its own .claude/ and its own AGENTS.md. The KB-root openspec/ is not an OpenSpec install and never needs init. If the user asks for init at the root, explain this and offer the KB-root spec layer instead — don't run it and don't clean up afterwards.

Install is npm install -g @fission-ai/openspec@latest (Node 20.19.0+), then openspec init in the repo; openspec update after a CLI upgrade. See OPENSPEC.md for the user-facing guide.

If OpenSpec is not installed (command -v openspec fails, or the repo has no openspec/), say so plainly and stop rather than improvising:

"This repo has no OpenSpec setup, and the openspec CLI isn't on PATH. Per-repo specs are OpenSpec's own workflow — install it and run openspec init in src/<repo>, and I'll take it from there. I can write this as a KB-root cross-cutting spec instead if it isn't repo-specific."

That offer is genuine, not a consolation: a requirement that spans repos belongs at the root anyway.

The bridge

Three directions, all owned here.

1. Archived change → decision record

An applied-and-archived OpenSpec change is a decision that was made and acted on. When the user archives a change (either level), offer to record it via ckb-decide: the change's why becomes Context and Rationale, the delta becomes Decision, and rejected options in the proposal become Alternatives considered.

Cite the change id and its path in the decision's ## Sources. Do not auto-record without asking — not every archived change is a decision worth a permanent numbered record, and a decisions log padded with routine changes loses the property that makes it worth reading.

Both artifacts are append-only, which makes them a natural pair: neither is ever rewritten when the thinking changes later.

2. Spec → entity page

Current KB-root specs surface in wiki/entities/ as type: spec pages so ckb-retrieve can answer "what is this supposed to do" without opening the spec tree. Keep these thin and pointer-style: tldr, spec_id, status, links. The spec file stays the source of truth — a full copy in wiki/ is a second thing to keep in sync, and it will lose.

3. Level linking — the drift rule

  • A KB-root spec lists every repo-level spec implementing it in implemented_by.
  • A repo-level spec names its parent in implements.
  • Both become graph edges (implements) in wiki/graph/edges.json.

Set both sides, every time. A one-sided link is a lint finding, exactly as a one-sided decision supersession is.

Sync check

On "sync the specs" / "are the specs in sync", report:

  1. KB-root specs with empty implemented_byspecified but nobody builds it.
  2. Repo-level specs with implements pointing at a missing or archived root spec — building against something no longer true.
  3. One-sided links, either direction.
  4. Root specs whose last_updated is older than the mapped commit of every repo implementing them — possible drift; verify, don't auto-fix.

Report all four. Fix none of them automatically — each is a statement about intent, and only the user knows which side is right. ckb-lint runs the same four checks as part of its sweep when this module is installed.

Edge cases

  • src/ is empty — the KB-root layer still works. Cross-cutting specs can precede any code; that's often the point.
  • A spec that's really a decision ("we'll use Postgres") — that's ckb-decide. A spec states a standing contract; a decision records a choice at a point in time. If it doesn't constrain future behaviour, it isn't a spec.
  • A capability implemented by exactly one repo, forever — it probably belongs in that repo, not at the root. Say so; don't silently promote it.
  • OpenSpec's format changes upstream — for per-repo specs, follow upstream. This skill's formats govern the KB-root layer only.
  • A change proposal that was rejected — it still archives, and it is still worth a decision record with status: rejected. The reasoning is the value.

Licensed under the Apache License, Version 2.0 — see LICENSE at the repository root, or http://www.apache.org/licenses/LICENSE-2.0.