Spec-driven development for AI coding agents. Spektacular helps you to generate specifications, the plan, and finally generate the code. Agent-agnostic and pluggable: Claude Code, Bob, Codex.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Spektacular — Spek for short. The specs it produces are called speks.
Agent-agnostic CLI tool for spec-driven development, providing skills and integrations for coding agents (Claude, Bob, Codex, GitHub Copilot CLI, oh-my-pi, Cursor) to plan and implement work from a written spek.
Status: early development — see the releases page for the latest version.
Spek is a self-contained Go binary that brings spec-driven development to AI coding agents. You write a markdown spek; Spek turns it into a reviewed implementation plan and then drives a coding agent to implement it — keeping your intent reviewable at every stage.
Its core competencies:
init, installs the skills (and commands) your coding agent needs to run the Spek workflows.new / goto / steps), so every stage is resumable — stop, inspect, edit, and resume without losing work.Spek follows a three-stage workflow — spec → plan → implement — each driven step by step by a state machine:
spec new scaffolds one from a template.plan new explores your codebase, asks clarifying questions, and writes a detailed implementation plan — plan.md, research.md, and context.md.implement new drives the coding agent through each phase of the plan and validates the result against your acceptance criteria.Where a piece of work stands can be inspected without reading Spek's state files directly. One command reports everything, from the epic down to each task:
spektacular status <name> # readable tree (--format pretty, the default)
spektacular status <name> --format json # the same report as structured data
spektacular status # every workflow in progress, if any
spektacular status [name] --watch # keep the readable tree on screen, scrollable, refreshed every --interval (2s)
<name> is a bare artifact name with no extension (20260922132517-a3f9c0de-git-commit, not 20260922132517-a3f9c0de-git-commit.md), the same name state.json records in data.name, and may be an epic, a spec or a plan. A spec that belongs to an epic, or that spec's plan, reports the whole epic, with the spec asked for named in requested; a standalone spec reports on its own. The JSON report always has the same shape, so callers never branch on kind: epic (null for a standalone spec, otherwise its document status and a roll-up of specs implemented and tasks complete), and specs, one entry per spec carrying its derived state (missing, stale, specified, planned, in_progress or implemented), document_status, current_step, depends_on, ready, blocked_by and its plan. A plan reports its document_status, current_step, task progress, and every task under specs[].plan.tasks with its id, title, milestone, repo, depends_on, execution, completed and acceptance_criteria counts; a plan without task structure omits progress and tasks.
current_step is the live workflow step when that artifact's workflow is in progress, finished for a closed document, stale for a stale plan, and empty otherwise. The report carries a workflow block (kind, name, current_step, completed_steps, updated_at) when the workflow in progress belongs to one of the reported specs or plans, and null otherwise. With no name, status reports every workflow in progress: the shared one, and each run kept in its own lane under .spektacular/workflows/ (an implement run in its own worktrees, or a run an epic's orchestrator started). workflows lists them, the shared one first and then the lanes, most recently updated first; workflow is the first, carrying orchestrated: true when an orchestrator started it, and the report shows its epic or spec in full, with the same run view as naming it. When an orchestrator started that first run, the report is its epic's and no single spec is requested. In the readable tree, the requested spec is expanded to its milestones and tasks; with no spec requested, every spec being implemented is expanded instead. With nothing in progress it is {"workflow": null}. The readable tree is coloured on a terminal and plain text when piped or when NO_COLOR is set. Long dependency lists wrap to the terminal, and task titles too wide for their columns are cut to fit. --watch keeps it on screen, redrawn in place every --interval (default 2s, at least 200ms) until q, Esc or Ctrl+C; a tree taller than the screen scrolls with the arrow keys, j/k, PgUp/PgDn, g/G and the mouse wheel, and keeps its place across a refresh; it is refused with --format json (status_watch_format_unsupported), and a shorter interval is refused with status_interval_invalid. An unknown name is refused with artifact_not_found, and an unsupported --format with status_format_unsupported; failures are always JSON, whatever format was asked for. Frontmatter dates are emitted as RFC3339 midnight UTC timestamps. spec file list and plan file list carry modified_at per entry, so polling many artifacts is one list call.
When plan.strict_spec_changes is true, status reports a plan's document_status: "stale" and current_step: "stale", and its spec's state as stale, once the linked spek is modified after a final plan; implement new and subsequent implement steps refuse that plan until it is replanned and re-approved. With the default non-strict setting, a later spek edit does not invalidate an existing plan. A spek change recorded with spektacular spec amend during an implement run does not make the plan stale in either mode; any other edit to the spek's body still does under the strict setting.
Spans. Every run's log shows where its time went. Spektacular records each command, each workflow step and, during implement, each plan task as a timed span by itself, with every command nested under its step. Agents mark the helper agents they launch and the questions they ask the user, as the workflow instructions tell them:
spektacular trace start <name> --type sub-agent|user-wait|work [--parent <id>] # prints a span id
spektacular trace stop <id> [--status ok|error] [--message <text>]
A span nests under the current step unless --parent names another open span, and one left open is closed, marked abandoned, when its step ends or its run finishes. With logging off both commands succeed and record nothing. See Logging and debugging.
Run statistics. Ask the logs where a run's time went; the timings need no agent transcript:
spektacular stats list # every recorded run, newest first
spektacular stats run [--name <spec>] # where one run's time went: steps, tasks, helpers, waits, gaps
spektacular stats compare # the two latest runs of a kind, side by side
spektacular stats summary --kind implement # median and 90th percentile per step
spektacular stats commands # every recorded command, with its step and arguments
spektacular stats spans # helpers and waits, including an epic orchestrator's
spektacular stats view [--name <spec>] # the run as a single-file HTML waterfall you can open or share
For runs driven by Claude Code, stats run, compare and summary also show tokens, cost and time per thousand output tokens for each step, task and helper, read from Claude Code's own session files with no extra setup (--sessions-dir <path> reads them from another folder). Set prices per million tokens under stats.prices in .spektacular/config.yaml; none are built in. Other agents show these figures as unknown.
Every form takes the same selection (--name, --kind, --agent, and a repeatable --run <id or 8+ character prefix>), prints a readable table by default, and gives the structured report with --format json, whose durations are integer milliseconds and whose field names are stable. --dir <path> reads a logs directory copied from anywhere, such as a test run's, with no project needed. Reporting never changes a log, and stats commands are never logged. The figures are defined on the documentation site's Run statistics page. stats view writes a single HTML file that opens in any browser without a server; see the Run viewer page on the documentation site.
Epics. When a piece of work is split into an epic, ask your agent to plan this epic, review the plans, then implement this epic. Independent specs are worked on in parallel, each spec is built in its own worktree and merged before the specs that depend on it, and repeating either request picks up where it stopped. Naming an epic in status adds a run view that says, for planning and for implementing, what each spec still needs and what blocks the epic. See Epics.
Every document a feature produces is addressed by the feature's bare name, the same name the workflow records in data.name, with no file extension:
spektacular spec file read 000059_normalise-artifact-addressing
spektacular plan file list 000059_normalise-artifact-addressing # plan, context, research, test-plan
spektacular plan file read 000059_normalise-artifact-addressing plan
spektacular changelog file read 000059_normalise-artifact-addressing [--repo <name>]
Every name a list prints is accepted unchanged by read, write, delete and set-document-status. path is where the store keeps the document, relative to the folder holding the configuration file that declares the store (specs/<name>.md, plans/<name>/plan.md, or changelog/<project>/<name>.md in a repo's changelog). The workflow commands' new and goto results report the plan the same way: plan_name and plan_document (always plan) are its address, and plan_path is its config-relative location, never a host path; spec_path follows the same convention.
A name with an extension, or a plan written as one path (<name>/plan.md), is refused with unexpected_extension; plan file read <name> with no document is refused with document_required. Both refusals' next_action shows the correct command, and a missing document's not_found points at the matching list command.
For the full pipeline, see the how-it-works documentation.
Spek is a single self-contained Go binary.
# Homebrew
brew install hivecommons/homebrew-repo/spektacular
# Go 1.21+
go install github.com/hivecommons/spektacular@latest
Or download a pre-built binary from the releases page. See the install docs for apt and other methods. You also need a supported coding agent CLI (claude, bob, codex, copilot, omp, or cursor) installed and configured.
Once installed, the minimal path is initialise → spec → plan → implement:
# 1. Initialise your project for a coding agent (claude, bob, codex, copilot, omp, or cursor)
spektacular init claude
# 2. Scaffold a spek, then fill in your requirements
spektacular spec new --data '{"name":"auth-feature"}'
$EDITOR .spektacular/specs/<returned-spec-name>.md
# 3. Generate an implementation plan
spektacular plan new --data '{"name":"<returned-spec-name>"}'
# 4. Implement the spec
spektacular implement new --data '{"name":"<spec-name>"}'
Spek names are normalised and prefixed by the CLI, so use the returned spec_name and spec_path for follow-up commands rather than the name you passed.
Speks are plain markdown with a small set of structured sections (overview, requirements, constraints, acceptance criteria, and so on), and spec new scaffolds the template for you. For the full walkthrough and spek format, see the getting-started tutorial and the how-it-works documentation.
Spek ships with six coding-agent integrations. spektacular init <agent> runs the chosen agent's install step, writing its workflow skills (and, where the agent has no skill mechanism, command wrappers) into your project:
.claude/skills/ and ensures the project's CLAUDE.md imports @AGENTS.md, so the Spek agent rules take effect..bob/skills/ and command wrappers under .bob/commands/. Bob stops every
sub-agent after session.maxTurns turns (100 by default) and hands back whatever it said last, which cuts
epic children off mid-step. init bob warns while a limit is set; spektacular init bob --raise-bob-turn-limit
sets it to 0 (no limit) in ~/.bob/settings/settings.json. The epic skills resume a cut-off child either way..agents/skills/..github/skills/ and the same standing rules in root AGENTS.md, with no command wrappers or extra instruction files..omp/skills/, Markdown wrappers and native input-preserving handlers under .omp/commands/, and eight always-applied spek-*.md rules under .omp/rules/. The rules mirror only Spektacular's sections of root AGENTS.md, keeping them visible even when omp selects another instruction file. Other project instructions, rules, skills and settings are left alone..cursor/skills/. Cursor reads root AGENTS.md itself, so no .cursor/rules/ files or command wrappers are written.The implement workflow's check_goal step has a separate, read-only reviewer judge each task's
acceptance criteria before any is ticked. spektacular init <agent> installs that reviewer as a
helper named spek-goal-reviewer, in the agent's own sub-agent format, on a cheaper model than the
agent's default:
| Agent | Helper | Default model | Read-only |
|---|---|---|---|
| claude | .claude/agents/spek-goal-reviewer.md | haiku | Read, Grep, Glob tools only |
| codex | .codex/agents/spek-goal-reviewer.toml | gpt-6-luna | by instruction only: Codex has no read-only mode for a helper |
| copilot | .github/agents/spek-goal-reviewer.agent.md | Claude Haiku 4.5 | read and search tools only |
| omp | .omp/agents/spek-goal-reviewer.md | @smol (your own cheap model role) | read, grep, glob tools only |
| bob | none | Bob's lighter model | Bob's built-in read-only explore helper is the reviewer |
Set implement.goal_check.models.<agent> in .spektacular/config.yaml to use another model for
one agent; the others keep their defaults. The model is written into the helper when it is
installed, so re-run spektacular init <agent> after changing it (migrate refreshes it on an
upgrade). Your own agent settings that pin a helper's model still win. Copilot ignores a helper's
model when the session model is Auto, and Copilot in VS Code also loads .claude/agents/, so a
project set up for both may list two copies of the same reviewer; both carry the same brief. An
agent without the helper falls back to any fresh-context sub-agent given
spektacular skill review-task-goal. Switch the check off with implement.goal_check.enabled: false.
Install Copilot CLI, then run:
cd my-project
spektacular init copilot
copilot
Inside the interactive session, start a workflow with /spek-new add user auth,
/spek-plan, /spek-implement, /spek-knowledge, /spek-manage-repos, or
/spek-design. Epic workflows are available through /spek-plan-epic and
/spek-implement-epic. Text after a command is passed to its skill. Start Copilot in the
project's top folder: Spektacular commands do not search parent directories.
Command names in non-interactive prompts such as copilot -p "/spek-new add user auth"
are not expanded; start workflows inside a session instead. This integration
covers the CLI, not editor Copilot or the cloud agent.
On shared projects, Copilot discovers each named skill once and prefers its
.github/skills/ copy over .agents/skills/ and .claude/skills/. Setup keeps
other agents' files and the project's own instruction files. A top-level
CLAUDE.md importing @AGENTS.md (as Claude setup creates) causes Copilot to
receive the standing rules twice: it reads AGENTS.md directly and follows the
import. Copilot-only setup does not create that import.
init copilot makes copilot the project's recorded agent. migrate refreshes
only that agent's files on upgrade; other agents' copies can fall behind. After
upgrading a shared project, run spektacular init <agent> once for each agent.
The last init determines the recorded agent.
If you previously ran init claude or init codex only as a Copilot workaround,
run spektacular init copilot. The old agent's files remain and may be removed
manually if that agent is unused. Keep the shared AGENTS.md. Rules remain
doubled for as long as a CLAUDE.md importing it remains.
Install and configure oh-my-pi, then run:
cd my-project
spektacular init omp
omp
In omp, start a workflow with /spek-new add user auth to the admin pages, or use /skill:spek-new. The same forms work for spek-plan, spek-implement, spek-knowledge, spek-manage-repos, and spek-design. Text after each plain command is forwarded unchanged, including quotes and whitespace. Skills remain available with foreign providers disabled.
Start omp in the project's top folder. Spektacular's commands only work there, and omp only discovers the plain commands there. Once root AGENTS.md exists, omp does not read root CLAUDE.md; put instructions omp should see in AGENTS.md instead. Spektacular does not create or overwrite .omp/AGENTS.md or .omp/RULES.md.
Shared projects: spektacular init omp records omp as the project's agent without removing another agent's files. Migration refreshes only the recorded agent's files; the other agent's copies fall behind and omp may list both current and stale skills. After upgrading, run spektacular init once for each agent to refresh every copy. Identical skill copies are listed once by omp.
Build a versioned binary with make build, then run the credential-free runtime regression check with python3 tests/omp_smoke.py ./bin/spektacular and omp on PATH. It checks actual command discovery, all six command inputs, instruction conflicts, deduplication, shared projects and disabled foreign providers. Verified against omp 18.6.0; it uses isolated temporary settings and a closed local model endpoint rather than a model service.
Run, in the project's top folder:
spektacular init cursor
Then open that folder in the Cursor editor, or start the Cursor CLI there. Start a workflow from Agent chat with /spek-new add user auth to the admin pages; every installed skill (spek-plan, spek-implement, spek-knowledge, spek-manage-repos, spek-design, spek-plan-epic, spek-implement-epic) is listed in the / menu the same way, and Agent may also pick one up when a request matches its description. The standing rules live in root AGENTS.md, which Cursor applies to every Agent chat; your own .cursor/rules/ are left alone.
Shared projects: Cursor also loads skills from .agents/skills/ and .claude/skills/, so a project initialised for codex or claude as well carries a second copy of each skill. Migration refreshes only the recorded agent's copy; after upgrading, run spektacular init once for each agent so no copy falls behind.
Each integration is deliberately small: an agent implements a narrow Agent interface — Name() (its CLI identifier) and Install() (which writes its workflow artefacts) — and registers itself with the agent package from an init() function. Adding a new agent means implementing those two methods and registering the type.
Both the coding agent and the storage layer are pluggable behind defined Go interfaces — the Agent interface in internal/agent and the Store interface in internal/store (the read/write/search surface backing the spek, plan, and knowledge stores). Only the file store ships today. For the full interface signatures and how to add your own backend or agent, see the extending documentation and the plugins overview.
Running spektacular init <agent> creates:
.spektacular/
├── config.yaml # agent, command, log, store settings, and the repo registry
├── repo.yaml # the colocated repo's own configuration
├── specs/ # your specification files
├── plans/ # generated plans (plan.md, research.md, context.md)
├── changelog/ # changelog records written by the implement workflow
└── knowledge/ # default project knowledge source
├── conventions/ # always-applied: standing rules, one per file
├── glossary/ # always-applied: shared domain/project terms
├── architecture/ # looked-up: how the system is built
├── gotchas/ # looked-up: sharp edges and traps
├── learnings/ # looked-up: empirical findings from past work
└── decisions/ # looked-up: the reasoning behind choices
Each knowledge category directory is scaffolded with a README.md describing what belongs in it. By default Spek reads .spektacular/knowledge/ as this repo's own store, addressed by the name the project registered the repo under; the project can declare additional shared stores — for example a team directory or a machine-wide global one — under knowledge.sources (see Configuration). See Knowledge for how it is organised and consumed.
Knowledge is the accumulated know-how a project draws on when planning — conventions, glossary terms, architecture notes, gotchas, learnings, and decisions. It is strictly a planning-time input: the planning agent reads it while producing a plan, and the relevant parts are written into the plan itself. The implement workflow then consumes only the plan documents — the plan is the contract.
Every entry belongs to exactly one of six categories, fixed by the first segment of its path. Each category has a retrieval tier that decides when its entries are loaded:
conventions (standing rules to follow) and glossary (shared domain and project terms). Loaded in full on every planning task, and deliberately excluded from search results so they are never surfaced twice.architecture, gotchas, learnings, and decisions. The larger reference body, fetched only when a search matches, so it can grow without weighing down every task.The category model — names, retrieval tiers, and per-category boundaries — is declared once in code, so it stays consistent across directory scaffolding, search labelling, and retrieval. A category's retrieval tier says when its entries are loaded; the addressing tier below says which knowledge a store holds. The two are different axes.
Knowledge lives in one of two tiers. Every registered repo contributes exactly one store, addressed by the name the project registered it under, holding knowledge about that repo's own code. The project declares any number of shared stores under knowledge.sources, each with its own name, for knowledge that belongs to no single repo (see Configuration). Every read, search, and always-applied load states a tier and, optionally, the store names to narrow to; every result reports the tier and store it came from.
Reading and writing name exactly one store, so they take a tier and a name alongside the path; a request that leaves either out is refused, and the refusal lists the names available in that tier. Searching, listing, conventions, and the always-applied load take --tier <project|repo|all> and a repeatable --filter <name>; omitting the narrowing covers every store the tier reaches, and no store is ever included or excluded implicitly.
Lookups are consolidated and de-duplicated across stores: each entry carries a SHA-256 checksum over its exact bytes, and byte-identical entries appearing in more than one store collapse to a single result. A search result looks like:
Hit {
tier // addressing tier of the originating store (project or repo)
name // name of the originating store (e.g. docs)
path // locator relative to the store root (e.g. gotchas/db-timeouts.md)
title // the document's first heading, or the locator when it has none
excerpts // compact matched excerpts
score // sum of query-term occurrences (ranking)
category // category derived from the path (e.g. gotchas, architecture)
checksum // SHA-256 over the entry's raw bytes; the byte-identity de-dup key
}
For the full model — every category definition, the retrieval tiers, the addressing tiers, and the de-duplication rationale — see the knowledge-base documentation.
Agents (and you) reach knowledge through the spektacular knowledge commands rather than reading the files directly, so access stays consistent across stores. The main subcommands:
knowledge search <query> — keyword-search the stores the request covers (excluding the always-applied categories), returning ranked, tier- and category-tagged hits, each carrying the entry's tags. A document need not contain every query word: it is returned if it carries evidence for any of them. Narrow with --tier, --filter, and a repeatable --tagknowledge tags — list the tag vocabulary already in use, with an entry count for each, most-used first; takes --tier and --filterknowledge conventions / knowledge always-applied — read the always-applied entries in full; both take --tier and --filterknowledge categories — list the categories and their retrieval tiersknowledge read / knowledge write — read and write one addressed entry, via --data '{"tier":"…","name":"…","path":"…"}'knowledge delete — remove one addressed entry, via --data '{"tier":"…","name":"…","path":"…"}'; an address holding nothing succeeds and changes nothing, and a category's own generated description is refusedknowledge list — list entries across the stores the request covers; takes --tier and --filterknowledge sources — list the configured stores by tier and name, with their locationsknowledge usage — show each entry's creation date and discovery and retrieval history from the store's usage index; takes --tier and --filterknowledge prune — remove old, unread entries, then check every remaining entry against the project, one instruction per entry answered with knowledge prune verdict; --dry-run previews without changing anythingEvery subcommand accepts --schema to print its input/output JSON schema and exit.
When research surfaces a durable learning, gotcha, or convention worth keeping, the agent writes it to the knowledge base itself through the spek-knowledge skill and tells you which entry it wrote, where (the tier, the store name, and the path) and why. Nothing waits for your approval: the report is how you see the knowledge base change, and the project's version history is the undo.
Each store keeps a usage index, .usage-index.json at its root, recording when every entry was created and each time a search surfaced it (a discovery) or a read returned it (a retrieval), written only by the knowledge commands and read with knowledge usage. spektacular knowledge prune (preview it with --dry-run) removes entries nobody has retrieved within knowledge.prune.max_age_days (90 days by default), then has the agent correct or delete the rest where they no longer describe the project, and reports every entry with its verdict and evidence.
In a Spek-initialised repo, the spek-knowledge skill is the entry point for reading, contributing to, and updating the knowledge base in any session, and coding agents route what they would otherwise save to their own per-user memory into the project knowledge base instead, so captured knowledge lands in git and travels with the project.
Configuration is split across two files, and a colocated single-repo project simply holds both in the same .spektacular/ directory. A repo's Spek files can also live apart from its code, in a folder that points at the code (see Repo configuration below).
.spektacular/config.yaml (project configuration). The project's identity, the coding agent Spek drives, the registry of member repos with the location of each repo's Spektacular files, the central spec, plan, and changelog stores, and the design sources the project declares. Spektacular always runs against a project: running it in a directory with no config.yaml produces an explicit error pointing at init (there is no parent-directory search)..spektacular/repo.yaml (repo configuration). A repo's own concerns only: what it is, where its code lives, its knowledge sources, and its changelog provider. It carries no pointer to any project, so one repo can belong to several projects at once.Breaking change: earlier releases used a single
config.yamlwithout a projectname. Existing setups re-initialize withspektacular init <agent>: init backfills the name (from the directory basename, or--name), seeds the colocated repo'srepo.yaml, and registers it in the newreposlist.
config.yaml)schema: 5 # settings format version, written by Spektacular; `migrate` raises it
written_by: 0.16.0 # the Spektacular version that last wrote this file (informational)
skills_version: 0.16.0 # the Spektacular version that last installed the agent skills
name: my-project # required, slug-safe; namespaces changelog entries
source: [email protected]:org/my-project.git # optional; the project's git address, recorded in derived changelog entries only
command: spektacular
agent: claude
auto_commit: "off" # off | workflow | full: automatic git commits (local only)
log:
provider: file # the logger; only file ships
level: standard # off | standard | verbose (full responses and state); status is never logged
retention: 10 # most recent workflow runs kept, and as many days of standalone commands
config:
directory: logs # relative to the folder holding config.yaml
spec:
provider: file
id_method: timestamp # timestamp + random suffix; counter is legacy and branch-local
config:
directory: specs # relative to the folder holding config.yaml
plan:
provider: file
strict_spec_changes: false # true marks final plans stale after later spec edits (recorded amendments excepted)
config:
directory: plans # relative to the folder holding config.yaml
changelog:
provider: file
config:
directory: changelog # central changelog; entries land under <directory>/<name>/
repos:
- name: my-project # the colocated repo, registered by init: this .spektacular/ folder
location: .
- name: docs # a repo checked out beside this one, with its own .spektacular/
location: ../../docs/.spektacular
- name: lib # a repo folder in this project; its code is cloned from a git source
location: ../repos/lib
knowledge:
sources: # optional, the project's shared stores only (e.g. a team share);
- name: team # each repo declares its own store in its repo.yaml
provider: file
config:
location: ../team-kb # relative to the folder holding config.yaml, as repos are
prune:
max_age_days: 90 # optional; days an entry may go unretrieved before prune removes it
design:
sources: # optional, where the project's design documents live;
- name: api # declared by the project only, never by a repo
provider: file
config:
location: ../design/api # relative to the folder holding config.yaml; may sit outside the project
Each repo entry needs a slug-safe unique name and a location: the folder holding that repo's repo.yaml (local is still accepted and means the same thing). A relative location is resolved from the folder holding config.yaml, and nothing is appended to it, so the project's own footprint is . and a repo folder in the project is ../repos/<name>. A repo is normally added through a guided flow: you are asked which repo to add, and its name, description, role and tags are each proposed for you from what the repo says about itself, one question at a time, with a plain-language confirmation before anything is written. Spektacular's files go inside the repo being added unless it cannot take them or you say otherwise, in which case they live in a folder under the project and the repo is left with only its code. An add can be started and finished while a spec or plan is already in progress. A caller that already knows every detail can still register a repo in a single command with repo add. Where the code lives is declared in the repo's own repo.yaml as source; the old address key is no longer read, and a config that still carries it fails to load with an error saying where the value now goes. description, role, and tags are optional metadata, also in repo.yaml, that cross-repo planning uses to attribute requirements to the right repo. Add to the registry with spektacular repo new, or spektacular repo add when every detail is already known, and inspect it with spektacular repo list; removal is a manual config edit. Cloned repos are never fetched or pulled automatically; a stale clone produces a warning only.
Relative locations everywhere in config.yaml share one base: the folder holding config.yaml. That covers a repos entry's location, a knowledge.sources entry's config.location, a design.sources entry's config.location, and the spec, plan and changelog store directories, so .. is the project's own root and ../team-kb a folder beside it. The store directories default to specs, plans and changelog, which land in .spektacular/specs, .spektacular/plans and .spektacular/changelog; a store directory outside the project is refused with the corrected value to write. An absolute location is used as written. A knowledge source that does not resolve to a directory fails fast, naming the store, the path it resolved to, and the base it resolved from; when the store is found where the pre-1.0 rule would have put it, the error also names the exact corrected value to write. A design source behaves the same way and fails fast with the same three facts, with one deliberate difference from the store directories: a design source's location is allowed to resolve outside the project, and is written back exactly as declared rather than re-expressed, because a design source points at a folder the team already keeps. Locations the CLI reports for stored documents (a list's path, status plan_path) use the same base, so they read the way the store directory is written, for example specs/<name>.md.
repo.yaml)schema: 2 # settings format version, written by Spektacular
written_by: 0.16.0 # the Spektacular version that last wrote this file
description: the documentation repo
role: documentation
tags: [docs]
source: # where the code is; omit when it is this folder
provider: file # file or git
config:
location: .. # a path relative to this file, or a git URL for the git provider
knowledge: # the repo's single store; synthesised if the file is absent
provider: file # addressed by the name the project registered this repo under
config:
location: knowledge
changelog:
provider: file
config:
directory: changelog # where this repo's derived entries land
A repo's Spek files can sit inside its code, in a .spektacular/ folder holding repo.yaml with a file source pointing at .., or in a folder of their own, for example one folder per repo under a project, with source pointing at a checkout on disk (absolute, relative to the folder holding repo.yaml, or using ${VAR}) or at a git repository that Spektacular clones into .spektacular/repos/<name>/ on first use. In the separate layout the code repository receives only code changes; knowledge and changelog entries land under the folder holding repo.yaml. spektacular repo list reports the resolved source as each repo's root. See Multi-Repo Projects for the layouts.
Knowledge aggregates across every registered repo's declared sources (in registry order) followed by the project-owned sources, so a repo's knowledge travels with it into every project that registers it. Changelog entries, central and derived per-repo, are namespaced under a folder named after the project (<directory>/<project-name>/<id>_<slug>.md), so multiple projects writing into one repo can never collide.
migrate)Every settings file records the format version it was written in. When a release changes that format, or when you install a new Spektacular, commands stop with an error naming migrate until the project is brought up to date; migrate, init, version check and help always run, so the way out is always reachable.
spektacular migrate --dry-run # list every change without touching disk
spektacular migrate # apply them
An upgrade brings config.yaml and every registered repo's repo.yaml present on disk to the current format, keeps a <file>.v<N>.old copy of each file it rewrites, names any repo it skipped because it is not checked out, and reinstalls the agent skills when they are older than the running Spektacular. Re-running spektacular init <agent> applies the same upgrades. Settings written by a newer Spektacular are refused rather than converted, with a message to update Spektacular; such a file is never rewritten.
.spektacular_ignore)Any source root (a repo, or the project's own storage locations) may carry a .spektacular_ignore file using gitignore pattern syntax. Matching paths are excluded from Spektacular's own listing and search results, keeping build artifacts and dependency directories out of planning research, but a directly named path is never blocked, and agents' native file tools are unaffected.
For the full reference (every key, the id-method semantics, name-normalisation rules, and ${VAR} expansion) see the configuration documentation. For the concept of multi-repo projects, why the configuration is split this way, and how work is attributed across repos, see Multi-Repo Projects.
Spektacular has two layers of tests: a fast Go unit suite, and an end-to-end Harbor harness that runs the workflows against real AI coding agents inside sandboxed Docker containers.
go test ./... # or: make test
make harbor-install # runs: uv tool install --upgrade harbor
The oracle agent runs a scripted solution to validate the test harness itself — no AI tokens required:
harbor run -p tests/harbor/spec-workflow -a oracle -o tests/harbor/jobs
Harbor needs an auth token to run Claude Code inside the container. If you use Claude Max (OAuth), export the token from your local credentials:
export ANTHROPIC_AUTH_TOKEN=$(python3 -c "import json; print(json.load(open('$HOME/.claude/.credentials.json'))['claudeAiOauth']['accessToken'])")
If you use an API key instead, export that:
export ANTHROPIC_API_KEY=sk-ant-...
Then run:
harbor run -p tests/harbor/spec-workflow -a claude-code -m claude-sonnet-5-5 -o tests/harbor/jobs
Makefile wrappers run the suites for you, building the binary and wiring up the agent-specific placeholders:
make harbor-test-spec # spec workflow (claude)
make harbor-test-spec-codex # spec workflow (codex)
make harbor-test-plan # plan workflow (claude)
make harbor-test-plan-codex # plan workflow (codex)
make harbor-test-plan-bob # plan workflow (IBM Bob)
make harbor-test-implement # implement workflow (claude)
make harbor-test-repo # guided repo add, answering each question (claude)
make harbor-test-repo-delegated # guided repo add, handing the whole set over (claude)
make harbor-test-epic # plan a whole epic through background sub-agents (claude)
make harbor-test-epic-codex # the same, with codex
make harbor-test-epic-bob # the same, with IBM Bob
make harbor-test-implement-epic # implement a planned four-spec epic, two specs in parallel (claude)
make harbor-test-implement-epic-codex # the same, with codex
make harbor-test-implement-epic-bob # the same, with Bob at its default turn limit
make harbor-test-implement-epic-bob-unlimited # the same, with Bob's turn limit removed
make harbor-test-standing-rules # standing rules in AGENTS.md, in a project initialised before the session (claude)
make harbor-test-standing-rules-codex # the same, with codex
make harbor-test-standing-rules-bob # the same, with IBM Bob
make harbor-test-background-child # probe: does headless Claude Code keep a background child past ten minutes?
harbor-test-background-child is a probe, not a workflow suite. Headless Claude Code starts one
background child that runs for fifteen minutes and ends its turn, and the verifier prints
PROBE OUTCOME: <ending> after <seconds> s; notice-woken turn: <bool> without asserting which way
it went. Its recorded run (2026-10-24, Claude Code 2.1.310: the child was stopped about ten minutes
after the main agent ended its turn) is checked in under
tests/harbor/shared/tests/data/probe-background-child/, and the epic skills' headless guidance
follows it. Re-run it when Claude Code changes, and update that guidance if the outcome does.
Bob has no Harbor agent of its own. harbor-test-epic-bob runs it through
tests/harbor/agents/bob_agent.py, which extends Harbor's generic ACP agent: it installs Bob from
IBM's published tarball and launches bob acp as described in tests/harbor/agents/bob.json. The plan suite's verifier reads Bob's run from the trajectory Harbor
records for an ACP agent, since Bob leaves no Claude transcript.
Like the Claude targets, it signs in with the login already on your machine, uploading your Bob
Shell login into the container before Bob starts, so run bob and sign in once first. Set
BOBSHELL_API_KEY to use an API key instead. If the container's Bob refreshes the session, you may
need to sign in to Bob again locally. Bob runs without -m, because its ACP server cannot choose a
model.
Results are written to tests/harbor/jobs/ (gitignored). Each run produces:
tests/harbor/jobs/<timestamp>/
├── result.json # Overall pass/fail and metrics
└── spec-workflow__<id>/
├── agent/ # Agent output log and native session transcripts
├── artifacts/
│ ├── spektacular/ # The project's .spektacular, with logs/ (Spektacular's run logs)
│ └── agent-logs/bob/ # Bob's own debug log, on Bob runs only
├── verifier/
│ ├── test-stdout.txt # pytest output
│ ├── junit.xml # pytest results, one testcase per check
│ ├── reward.txt # 1 = pass, 0 = fail
│ └── metrics.json # the run's metrics record (see Run metrics below)
└── trial.log # Full trial log
| Task | Description |
|---|---|
tests/harbor/spec-workflow | Full spec creation workflow, end to end |
tests/harbor/plan-workflow | Full plan generation workflow, end to end |
tests/harbor/repo-workflow | Guided repo add, checked against the agent's own transcript |
tests/harbor/epic-workflow | Plans a seeded three-spec epic with spek-plan-epic, checked against CLI state and Spektacular's logs, so the same verifier serves every agent |
tests/harbor/implement-epic-workflow | Implements a seeded, planned four-spec epic with spek-implement-epic: storage and format in parallel, commands after both are merged, export after commands. Checked against CLI state, git, the finished app's behaviour and Spektacular's logs |
Every suite records the agent's run at Spektacular's default log level, with no special setting
(the pre-built projects' seed scripts run with logging off and switch it back to the default as
their last step, and the verifiers switch logging off before their own calls). The project's
.spektacular/logs/, collected as each run's artifacts/spektacular/logs/, holds one file per
workflow run and one per day for commands outside any run, whichever agent ran, apart from
status, which is never logged. Harbor reads these logs only through spektacular stats (list,
run, commands and spans), never by parsing them, so running spektacular stats run --dir <trial>/artifacts/spektacular/logs on a collected trial shows the same step timings as its
metrics record. Reading them locally needs a spektacular that has stats on PATH, or one named
by SPEKTACULAR_BIN.
The epic suites print a timeline from those logs at the top of their results: when each spec's
child started and finished, its slowest step, and the hand-off gaps. The orchestrator starts a
sub-agent span when it launches a child and stops it when it handles the child's hand-back, so
spawned after runs from that span's start to the child's first command and picked up after
from the child's last command to the span's end (or, for a span never stopped, to the
orchestrator's next recorded command or span); merged after runs to the orchestrator's
epic merge. Because status is never logged, these gaps are not comparable with timelines from
before. The timeline also lists every child that had to be resumed and the step it stopped at,
and, on Bob runs, how often Bob ended a task at its turn limit. Run it on a collected job with
python3 tests/harbor/shared/timeline.py <job>/<trial>/artifacts/spektacular/logs [<job>/<trial>/artifacts/agent-logs/bob].
Every suite's verifier leaves verifier/metrics.json beside reward.txt, in the same shape for
every agent and suite. tests/harbor/shared/metrics.py writes it after the reward is written, and
the verifier ignores how it exits, so recording metrics never changes a run's pass or fail. The
record's identity comes from a build.json that each make harbor-test-* target writes into the
suite it renders, beside the recorder and the shared helpers it copies there. A suite run straight
from its source directory, without its make target, has no recorder in its tests and leaves no
record.
A value a run's logs do not supply is null, and its field is listed under unknown with the
reason. It is never written as zero.
| Field | Meaning |
|---|---|
schema_version | The record's shape, currently 3. Version 3 takes every step, helper, wait and gap from Spektacular's logs, in place of version 2's transcript-based step output, child placement and idle time; the report still reads version-1 and version-2 records |
run | suite, scenario, agent, model, spektacular_version (git describe), commit, and dirty (the tree had uncommitted changes when the suite was built) |
result | reward as written to reward.txt. checks holds passed, total and score (passed over total, skipped tests excluded). groups gives the same per group, under its name: the test class, or the test module for suites written as plain functions |
totals | From the agent's log: wall_time_s, model_time_s (time spent in model calls, summed across sub-agents, so it can exceed the wall time), turns, cost_usd, input_tokens (uncached), output_tokens, cache_read_tokens and cache_write_tokens. From Spektacular's logs: trace_calls and trace_calls_per_task (the run's spektacular trace calls, and their average per plan task), and, summed over every workflow run, user_wait_s (time waiting on the user), helper_wait_s (time a helper agent was running), gap_s and gaps (the time in, and the number of, stretches with no recorded event longer than gap_threshold_s, 300 by default) |
steps | One entry in workflows per workflow run in Spektacular's logs, in start order: its run_id, kind, name, status, wall_s, user_wait_s, helper_wait_s, gap_s and gaps, and in steps each step it entered, in order of first entry, with its step name, started_at, , (over every entry), its split into , and (which add up to its duration), , (how many helper agents were started in it) and (entries that did not finish). is when the logs are missing or hold no workflow run |
Everything but tokens, models, cost and briefs comes from the logs Spektacular keeps under
.spektacular/logs/, recorded at the default log level, which reach the trial's artifacts with
the project's .spektacular. The recorder reads them only through spektacular stats (list,
run, commands and spans), never by parsing them, so every step, helper and wait figure means
the same for every agent, and spektacular stats run --dir <trial>/artifacts/spektacular/logs on a
collected trial gives the same numbers. A step's figures add up over every entry into it, and a
resumed child's hand-off is not inside any step. When the logs are missing, or stats cannot read
them, every log-sourced field is null and listed under unknown with the reason, and none is
filled in from the agent's transcript.
Claude's totals come from the result summary at the end of its transcript, with tokens summed over
every model it used. When background sub-agents split a Claude run into several summaries, turns are
summed, cost, tokens and model time come from the last summary, and wall time is the span from the
transcript's first message to its last. The other agents report less, but only in the transcript's
fields: steps, helpers and waits are the same for every agent.
| Agent | Unknown in its record |
|---|---|
| Bob | Tokens, cost, model time and model. Each brief carries only its kind, description and prompt (and so prompt_chars); its models are unknown. Its wall time comes from Bob's own shell logs |
| Codex | briefs (null: Codex's log shows no helpers), model time and cache writes. Cost too, unless Harbor can price the model |
| Oracle (no agent) | Every agent total, the model and briefs. Wall time comes from Spektacular's logs |
tests/harbor/shared/report.py reads the records collected under tests/harbor/jobs/, and groups
runs by suite and scenario, so, for example, repo-workflow's one-at-a-time and delegated runs are
kept apart. It opens
only each trial's verifier/metrics.json and result.json, never the agent's own files, so it runs
as your normal user even though Claude runs leave root-owned files under agent/. Trials without a
record, such as runs from before the recorder, are skipped. make harbor-report runs it, passing
ARGS through:
make harbor-report # one row per run, grouped by suite
make harbor-report ARGS='tests/harbor/jobs/fix2-*' # only some jobs (directories or globs)
make harbor-report ARGS='--run plan-workflow-claude__<id>' # one run's step split, by a trial name from the table
make harbor-report ARGS='--run epic-workflow-claude__<id> --prompts' # ... and every helper's brief
make harbor-report ARGS='--compare plan-workflow-claude__<a> plan-workflow-claude__<b>' # two trials side by side
make harbor-report ARGS="--baseline 'tests/harbor/jobs/before-*' --candidate 'tests/harbor/jobs/after-*'"
The table shows each run's agent and model, wall time, user wait and gap time in minutes, turns,
output and cache-read tokens, cost, check score and pass or fail, and marks a run whose gaps take
more than a quarter of its wall time ◀ gaps (the threshold is GAP_SHARE at the top of
report.py). --run takes a trial directory, a trial name, or a prefix that names one trial. For
each workflow run it lists every step with its entries, duration and share of the run, and its
split into working, helper-wait and user-wait time, with the helpers it started; then the run's
totals (wall, user wait, helper wait and gaps over the threshold), the helper counts, a table of
the helpers (name, spec, step, seconds and how it ended), and every unknown measure with its
reason. A step taking more than a third of the run's wall time is marked ◀ dominant. Add
--prompts to --run to print each helper's brief after it; without --run it is a usage error.
--compare A B takes two trial directories or names and prints, for each workflow (matched by
kind and name, else in order within a kind, since a suite's generated spec names differ from
trial to trial), every step's duration, working, helper-wait and user-wait time and helpers for A
and B with B's difference from A, then the totals (wall, user wait, helper wait, gaps, cost and
tokens) the same way. A step only one trial entered shows - on the other side, never 0. A
version-1 or version-2 record holds none of the split, so those figures print unknown.
--baseline and --candidate each take job directories or globs: the runs before a change and the
runs after it. For each suite (and scenario) present in both, it prints the median of wall time,
user wait, helper wait, gap time, turns, output tokens, total tokens, cost and check score in each
set, and the change. A median is
taken over the runs whose value is known. A measure is marked
◀ regression when the candidate's median is at least twice the baseline's, or when the check
score falls by 0.1 or more. A measure no run in a set knows prints unknown there and is never
flagged.
Suites present in only one set are listed as unmatched. The thresholds are constants at the top of
report.py.
The recorder can also be run by hand on a collected trial, for example to re-record it:
python3 tests/harbor/shared/metrics.py --agent-dir <trial>/agent --log-dir <trial>/artifacts/spektacular/logs --bob-logs <trial>/artifacts/agent-logs/bob --junit <trial>/verifier/junit.xml --reward <trial>/verifier/reward.txt --build-info <build.json> --out <file>.
Reading agent/ there needs the same permissions as the files Harbor left in it.
The repo-workflow suite is the one whose assertions are about the conversation rather than the
files: that only the repo itself is asked for cold, that every later question carries a proposed
value, that the questions arrive one per exchange, and that none of Spektacular's internal
vocabulary reaches the user. Its rules are themselves checked by
tests/harbor/repo-workflow/tests/test_verifier_selfcheck.py, which runs locally under plain
pytest with no container and proves each rule fails on a transcript that breaks it:
python3 -m pytest tests/harbor/repo-workflow/tests/test_verifier_selfcheck.py
The shared helpers have self-checks of their own, also without a container: the run log reader
(over canned stats answers, which include the Go golden files, so a renamed field breaks them
too), the delegation rules, the recorder (including every way an input can be missing or broken),
the timeline, the report, the wiring of the recorder into every suite, and a check that the shared
scripts stay within 3,000 lines. An integration test builds a throwaway spektacular from the
working tree and runs the reader, the recorder and the timeline over fixture logs; it needs Go,
and skips without it. Some run on real runs: trimmed copies of
collected trials, checked in under tests/harbor/shared/tests/data/, with every message's text
blanked to the same length. python3 tests/harbor/shared/tests/trim_reference.py <trial dir> <name>
adds one, and writes nothing if trimming would change a value the recorder computes. They live in
tests/harbor/shared/tests/, which the make targets never copy into an image:
uvx --with pytest pytest --import-mode=importlib tests/harbor/shared/tests
# build binary
make build
# run tests
make test
# cross-compile for all platforms
make cross
make build produces the binary at ./bin/spektacular. The Makefile targets:
| Target | Description |
|---|---|
make build | Build the ./bin/spektacular binary |
make test | Run go test ./... |
make lint | Run go vet ./... |
make clean | Remove build artefacts |
make install-local | Build and copy the binary to /usr/local/bin |
make harbor-install | Install (or upgrade) the harbor CLI with uv |
make cross | Cross-compile for darwin/linux/windows (amd64 + arm64) |
git checkout -b my-feature)make test, make lint)entriesduration_sworking_shelper_wait_suser_wait_scommandshelpersincompleteworkflowsnullsubagents | From Spektacular's logs: started, completed, failed, unfinished (not stopped, or cut off), durations_s for each helper that finished, and helpers, every sub-agent span: span_id, parent, name, spec, kind, step, task_id, outside_run (an epic orchestrator's helper, started outside any run, whose spec is the lane of the child launched under it and whose step is null), started_at, ended_at, duration_s, end_reason, status and complete. From the agent's log: briefs, every child agent the run started, nested ones included, with its id (its launching call), parent, kind, description, models, prompt (the full context it was given) and prompt_chars |
unknown | One {field, reason} per measure the record could not fill |