Spec database OPAL command reference

Every task that reaches the spec database, and the rules each one enforces

Spec database OPAL command reference

The spec.* family is the only way anything reaches the spec database. No session opens the file; every access goes through a task, so it is mediated, permissioned and attributable.

Two companion families are documented here too: trove.dispatch, which turns a described work unit into a running session, and node.probe/fleet.describe, which say what a machine can do.

Reads never ask permission

spec.search, spec.get, spec.tree, spec.relations, spec.history, spec.blame, spec.dod.standards and spec.dod.check declare no permission scope, are idempotent, and never prompt.

That is deliberate and it is what makes the database useful: a session can read a spec before anything is committed. A model that had to ask consent to look something up would guess instead.

Writes are the opposite — one thing per call, always. There is no batch mutate, no glob, no "update all matching". Those are the surfaces where one careless call corrupts fifty rows, so they are not offered. Bulk reads are fine.

Keys, not paths

Every spec, section and work unit has a stable key — sqb4gsd1zn9 — issued by the database, never chosen by a caller. The human-readable path (CSCC.EFC.3) is a display address derived from position, so renumbering a section changes paths and changes nothing else. Always pass keys.

Reading

Task What it answers
spec.search q=<text> Full-text over titles, instructions and notes. Returns key + title only.
spec.get key=<key> [body=1] One entity. Bodies are opt-in, because a read that returns everything is a token bill somebody pays every call.
spec.tree [spec=<key>] The shape: specs, their sections, their units.
spec.relations key=<key> Units naming this one in depends_on or depended_on_by.
spec.history key=<key> Every change, appended and never altered.
spec.blame key=<key> field=<field> Who last wrote one field.

Deleted entities still resolve — that is what soft delete means. Ask the returned deleted flag rather than expecting a miss.

Authoring

Task Notes
spec.put kind=spec|section … Create or edit one spec or one section. Returns its key.
spec.workunit.edit key=<key> … Edit one unit.
spec.workunit.reparent key=<key> section=<key> A redirect: the unit keeps its key, status and history. An empty section leaves it free-standing.
spec.workunit.delete key=<key> Soft delete. The one task that requires physical presence — it is the only irreversible-shaped operation, so it cannot be invoked from a remote lane.

Every work unit needs a Definition of Done. The field is NOT NULL from the first migration and authoring rejects an empty one: a unit whose done-ness cannot be checked is a unit that will be argued about later.

Definition of Done

Task Notes
spec.dod.standards The catalog of standards a Definition of Done can cite.
spec.dod.record key=<key> standard=<id> passed=1 node=<box> [target=…] Record one mechanical result.
spec.dod.check key=<key> Is it met, and if not, what is missing.

The catalog is fixed: STD-UNIT, STD-COVERAGE-NEW, STD-E2E, STD-BUILD, STD-DOC, STD-VERIFY. A Definition of Done cites them by name — "meets STD-UNIT and STD-BUILD" — instead of re-describing testing every time.

A citation the catalog does not know is refused when you write it. A typo'd STD-COVERAGE would cite nothing at all, leaving a Definition of Done that reads rigorous and enforces nothing.

node= is not optional for STD-BUILD. A green build says nothing without the box it was green on — the same reason a green Linux build says nothing about Windows-only code. A pass recorded without one counts as unattributed, and blocks completion exactly as a failure would.

STD-VERIFY cannot be recorded at all. It is settled by a separate work unit of type Verify that names its subject in depends_on and finishes successfully. The implementer does not grade its own homework for the parts that need an opinion.

Status, claims and plans

Task Notes
spec.workunit.claim key=<key> owner=<who> Take a lease. A live lease blocks another taker; an expired one does not.
spec.workunit.renew key=<key> owner=<who> Extend it while working.
spec.workunit.set-status key=<key> status=<status> Report where the work stands.
spec.plan.put … Create or edit a plan: whole specs plus individually chosen side quests.
spec.plan.progress key=<key> Derived rollup — nothing is stored, so a spec can change mid-flight with nothing to resync.
spec.plan.regroup key=<key> [include_rejected=1] [dry_run=1] Re-open unsatisfactory or stale work for another attempt.

donesuccess is the one status the database will not take on trust. If the unit's Definition of Done cites standards, they must all be satisfied first, and the refusal names the gap. Every other terminal status is a report rather than a claim: doneerror says it was not met, rejected says it will not be attempted, and neither needs evidence.

A claim is a lease, not a lock. If a session dies mid-unit the lease expires and the work becomes available again — a crashed session cannot make a unit permanently unclaimable.

spec.plan.regroup runs one round and reports what it re-opened. It does not loop: how many rounds are worth trying depends on the budget you have left, which only you know. Rejected is left alone unless you ask, because it is a decision rather than a failure.

Publishing

spec.publish key=<key> [root=<worktree>] [spec_path=<tree>] [node=<box>]

Renders a spec into a .md under the repository's spec tree so the code arrives with a description of what it was for. It publishes terminal work only, and says how many units it left out.

The file says in its own header that it is generated and that editing it changes nothing. It is a re-derivable snapshot; the database is the source.

This is the one spec.* task that takes a coarse resource scope. It touches the working tree rather than the database, and there "another publish is already running" is the right answer.

Running work

trove.dispatch lane=<n> key=<plan-or-unit> [model=<model>]

Selects the next runnable unit of a plan (or takes one named unit), claims it, writes its briefing into the lane's CLAUDE.md, and queues the session.

It claims before it queues, because a queued-but-unclaimed unit is one a second lane can take. A plan with nothing left says "no work left" rather than failing — those two call for opposite responses.

What a machine can do

Task Notes
node.probe What this node brings: OS, arch, cores, disk, load, and the toolchains actually on PATH.
fleet.describe Every node's advertisement. Today that is one node, in a list.

Everything advertised is measured, not configured. An advertisement that can be optimistic routes work to a box that cannot run it, and the failure lands much later as a confusing error on somebody else's machine.

Calling these from the CLI

The --opal forms and their two silent traps are documented in the audio-recovery task reference and apply identically here:

hub --opal 'spec.put kind=spec code=gait title=Gait' -c
hub --opal '{"steps":[{"task":"spec.search","args":{"q":"gait"}}]}' -c

Remember that the short form splits on spaces, so any value containing one needs the plan form.

Topics
reference trove spec opal
See also