spec-graph: Agent Operator Skill
spec-graph is a CLI tool that layers a typed semantic graph on top of phase-based development. The structured graph — not markdown — is the source of truth. Agents receive computed impact sets and patch-target lists instead of reasoning over free-text documents.
Four core capabilities:
- Impact Analysis — compute what must change together when an entity changes
- Gap Detection — find missing implementations, verifications, plans, or open questions
- Consistency Validation — check graph integrity and workflow gates
- Agent Coordination — work only on computed affected targets, not entire documents
Three-Layer Architecture
v1 organizes the graph into three distinct layers. Each layer has its own entity types, relation types, edge matrix, and validation checks.
arch (architecture layer)
Contains the "what" and "why" of the system: requirements, decisions, interfaces, states, tests, and supporting entities. This is where semantic meaning lives.
Entity types: requirement, decision, interface, state, test, crosscut,
criterion, assumption, risk, question
Relation types: implements, verifies, depends_on, constrained_by, triggers,
answers, assumes, has_criterion, mitigates, supersedes, conflicts_with, references
exec (execution layer)
Contains the "when" and "how" of delivery: plans, phases, tasks, and changes. A plan groups phases into a single active delivery sequence. Only one plan may be active at a time. A change is a lightweight independent work unit (PR, bugfix, patch) that covers arch entities without belonging to any plan or phase.
Entity types: plan, phase, task, change
Relation types: belongs_to exactly for phase→plan and task→phase, task_depends_on
(dependent task→prerequisite task in the same phase), precedes (phase→phase), and
blocks (phase→phase).
Note: change entities do NOT participate in exec relations (belongs_to, precedes, blocks). They are independent units.
mapping (cross-layer)
Connects arch entities to exec entities. This is where intent meets delivery.
Relation types: covers (phase/change/task→arch entity), delivers (phase/task→arch entity).
When a phase has any child task, task mappings are canonical and direct phase mappings are forbidden.
Layer Classification
Layer is determined by entity type prefix. It is always deterministic:
| Prefix | Type | Layer |
|---|---|---|
| REQ | requirement | arch |
| DEC | decision | arch |
| API | interface | arch |
| STT | state | arch |
| TST | test | arch |
| XCT | crosscut | arch |
| ACT | criterion | arch |
| ASM | assumption | arch |
| RSK | risk | arch |
| QST | question | arch |
| PLN | plan | exec |
| PHS | phase | exec |
| TSK | task | exec |
| CHG | change | exec |
Entity IDs
IDs are decentralized and sortable: PREFIX-<unixSeconds>-<rand3>, e.g.
REQ-1752239482-k3f. The unix-seconds segment makes IDs sort chronologically as
strings; the three-character random suffix (Crockford base32 lowercase, minus
i/l/o/u) prevents same-second collisions.
Never invent or predict an ID. There is no counter to continue and no next number
to guess. Always omit --id and read the assigned ID from the response:
REQ_ID=$(spec-graph entity add --type requirement \
--title "All payments must be idempotent" \
--metadata '{"priority":"must","kind":"non_functional"}' | jq -r '.entity.id')
Then reference "$REQ_ID" in later commands. For multi-entity flows, capture every ID
into a shell variable before wiring relations.
Why this matters: the old scheme was MAX+1 per type, which collided whenever two
branches created entities in parallel — both produced REQ-6 and clashed on merge.
The current scheme needs no central coordination, so concurrent and branch-parallel
creation is safe.
--id still exists and legacy PREFIX-NNN IDs remain valid (the validation regex
accepts both forms, so no migration is needed). Pass --id only when reproducing a
specific existing ID, such as restoring an entity or importing from an external
system. Do not use it to keep sequential numbering.
Core Principles
- English only: all spec-graph content — entity titles, descriptions, metadata,
--reasonmessages, criteria text, and any other field written into the graph — MUST be in English. Regardless of the language used in conversation, never write non-English text into spec-graph entities or relations. - Compute first: never modify by guesswork. Always run
impactandvalidateto identify targets before making changes. - JSON contract: command output is JSON on stdout (except
export --format dot|mermaid). Parse it to decide the next action. - Layer discipline: arch entities belong in arch, exec entities in exec. Do not mix concerns.
- Phase gates: always run
validate --layer mapping --phasebefore starting or completing a phase. - Git as audit log: each commit is a logical changeset. The project's git history is the sole audit trail for spec-graph changes.
- covers/delivers: use the v1 mapping relations.
coversexpresses planning intent,deliversexpresses completion. - Graph-native plans: new plans create
TSKentities and no Markdown plan files. Never auto-import, delete, or reinterpret old Markdown. - Never guess IDs: IDs are generated, not sequential. Omit
--idon create and capture.entity.idfrom the response. Never construct an ID by incrementing a number you saw.
Task Contract and Scope
Every task has a non-empty title and description plus a closed six-field metadata contract. Unknown keys are rejected:
{
"order": 1,
"instructions": ["Implement the scoped behavior."],
"acceptance": ["The behavior is verified."],
"must_not": [],
"references": [],
"qa": [{"command":"go test ./...","expected":"exit 0","evidence":""}]
}
orderis a positive integer.instructions,acceptance, andqaare non-empty arrays.must_notandreferencesare required arrays and may be empty.- Every QA item has non-empty
commandandexpected.evidenceis empty before resolution and must identify a repository-relative regular file when the task resolves.
Canonical scope is selected per phase: a task-managed phase (any task belongs_to it) derives
scope and delivery from the union of child task covers/delivers; a taskless phase keeps its
direct phase mappings unchanged. Never mix direct phase mappings with child-task mappings. Tasks
remain exec entities and are never members of the architecture satisfaction closure.
Task lifecycle is draft → active → resolved or draft|active → deprecated; resolved and
deprecated are terminal, and deprecation requires a reason. Activation requires an active parent
phase and resolved prerequisites. Resolution requires resolved prerequisites, QA evidence, and
delivers for every deliverable target the task covers.
Storage Architecture
v0.3.0 uses TOML-first storage with SQLite as a disposable index.
- Source of truth: TOML files at
.spec-graph/entities/{type}/{id}.toml - Relations: embedded in entity TOML files (outbound only)
- SQLite index:
.spec-graph/graph.db, disposable, auto-rebuilt from TOML on any command if stale - Staleness detection: content-hash fingerprint per entity file
- Gitignored:
.spec-graph/graph.db*and.lockare never committed
The SQLite index exists purely for fast queries (neighbors, impact, path). If deleted or corrupted, it rebuilds automatically. Never treat it as authoritative.
TOML File Format
Canonical entity file at .spec-graph/entities/requirement/REQ-1752239482-k3f.toml:
schema = 1
id = "REQ-1752239482-k3f"
type = "requirement"
title = "User authentication"
description = "All APIs require OAuth2"
status = "active"
created_at = 2026-05-23T17:00:00+09:00
updated_at = 2026-05-23T17:30:00+09:00
[metadata]
priority = "must"
kind = "non_functional"
[[relations]]
to = "ACT-1752239485-q7m"
type = "has_criterion"
[[relations]]
to = "DEC-1752239490-b2x"
type = "constrained_by"
weight = 0.8
Fields: schema (always 1), id, type, title, description (optional), status,
created_at, updated_at, [metadata] (type-specific), [[relations]] (outbound edges).
Git Workflow
TOML files are designed for git-friendly collaboration:
- Each entity is a separate file, so merge conflicts are entity-scoped
- After
git mergeorgit pullwith conflicts, resolve TOML files then runspec-graph doctor - SQLite index is never committed (listed in
.gitignore) - Commit messages serve as the audit log. Git history is the sole mechanism for tracking entity changes over time.
Audit Trail
spec-graph does NOT maintain its own history. The project's git history is the sole audit trail.
- Entity changes:
git log -- .spec-graph/entities/{type}/{id}.toml - Relation changes: tracked via the owning entity's file history
- Phase transitions:
git log -- .spec-graph/entities/phase/PHS-XXX.toml
Recommendation: Commit .spec-graph/ changes after each logical unit of work (phase activation, delivers batch, entity registration).
Quick Reference: CLI Commands
See references/cli-reference.md for full options.
Project Init
spec-graph init
spec-graph init --path /custom/path
Do not duplicate .spec-graph/.gitignore entries in the root .gitignore.
Entity CRUD
spec-graph entity add --type <TYPE> --title "..." [--description "..."] [--metadata '{}'] [--metadata-file <PATH>]
# Omit --id. The ID is generated; capture it with jq -r '.entity.id'.
spec-graph entity get <ID>
spec-graph entity list --type <TYPE> [--status <STATUS>] [--layer arch|exec|mapping|all]
spec-graph entity update <ID> --title "..."
spec-graph entity update <ID> --status resolved [--force] [--reason "..."]
spec-graph entity revise <ID> --reason "..." [--title "..."] [--description "..."] [--metadata '{}']
spec-graph entity delete <ID>
spec-graph entity import --input <PATH>
entity import requires id, type, and title in every item. It writes accepted items in one
transaction and one index refresh, so another process cannot interleave items. Existing IDs are
skipped; any other failure, including an unknown type, malformed ID, missing required field, or
write failure, aborts the import without changing the graph and exits non-zero. Its response
contains only created and skipped.
Relation CRUD
spec-graph relation add --from <ID> --to <ID> --type <RELATION_TYPE>
spec-graph relation list --from <ID> [--layer arch|exec|mapping|all]
spec-graph relation delete --from <ID> --to <ID> --type <RELATION_TYPE>
Impact Analysis
spec-graph impact <ID> [<ID>...]
spec-graph impact <ID> --follow implements,verifies,covers
spec-graph impact <ID> --min-severity medium
spec-graph impact <ID> --dimension structural|behavioral|planning
spec-graph impact <ID> --layer arch
Validation
spec-graph validate
spec-graph validate --layer arch
spec-graph validate --layer exec
spec-graph validate --layer mapping
spec-graph validate --check orphans|coverage|cycles|conflicts|invalid_edges|superseded_refs|unresolved
spec-graph validate --check phase_order|single_active_plan|orphan_phases|exec_cycles|invalid_exec_edges
spec-graph validate --check plan_coverage|delivery_completeness|mapping_consistency|invalid_mapping_edges
spec-graph validate --phase <PHS-ID>
spec-graph validate --entity <ID>
Query
spec-graph query neighbors <ID> --depth 2
spec-graph query path <FROM-ID> <TO-ID>
spec-graph query scope <PHS-ID>
spec-graph query unresolved --type question|assumption|risk [--phase <PHS-ID>]
spec-graph query sql "SELECT ..."
Phase Lifecycle
spec-graph phase next [--activate]
spec-graph phase context <PHS-ID>
Export
spec-graph export --format json|dot|mermaid
spec-graph export --center <ID> --depth 3 --format json
spec-graph export --layer arch --format dot
Bootstrap
spec-graph bootstrap scan --input ./docs/ [--format json]
spec-graph bootstrap import --input extracted.json --mode review
Migration & Integrity
# One-shot migration from old SQLite-only format
spec-graph migrate [--dry-run] [--keep-db]
# Integrity validation (run after git merge/pull)
spec-graph doctor [--check <name>] [--fix]
Entity & Relation Quick Reference
See references/data-model.md for full type catalog, metadata schemas, and edge matrices.
Entity Types (14)
| Prefix | Type | Layer | Purpose |
|---|---|---|---|
| REQ | requirement | arch | functional / non-functional requirement |
| DEC | decision | arch | policy / architecture decision |
| API | interface | arch | API contract, module interface, event contract |
| STT | state | arch | state or state-transition rule |
| TST | test | arch | test case / scenario |
| XCT | crosscut | arch | cross-cutting concern (auth, audit, etc.) |
| QST | question | arch | unresolved question |
| ASM | assumption | arch | unverified assumption |
| ACT | criterion | arch | acceptance criterion |
| RSK | risk | arch | explicit risk item |
| PLN | plan | exec | delivery plan grouping phases |
| PHS | phase | exec | development phase or milestone |
| TSK | task | exec | graph-native unit of implementation work |
| CHG | change | exec | lightweight work unit (PR, bugfix, patch) |
Entity Status: draft → active → deprecated / resolved / deleted
Auto-activation (v0.4.0+): When a delivers relation is added targeting a draft arch
entity, the CLI transitions it to active. The source may be a phase or a task. Entities in any
other status are left alone. This means:
draft= registered, no delivery evidence yetactive= either set explicitly (on create with--status active, or via update), or auto-promoted fromdraftby adeliversedgeresolved= verified complete (only spec-verifier should set this)
Do NOT manually transition arch entities to active after adding delivers — the CLI
handles it. Do NOT expect delivers to auto-resolve entities; resolution requires
explicit verification.
Gated transitions (v0.3.1+): Transitioning a phase or plan to resolved is gated.
The CLI automatically runs delivery_completeness + gates checks (for phases) or
plan_coverage (for plans). If issues are found, the transition is blocked (exit 2).
Completion findings can be accepted with --force --reason "..."; structural findings
cannot be bypassed. A successful forced completion emits outcome = "applied_with_force",
reports the accepted findings, and records top-level completion_forced = true plus
completion_reason = "..." in the entity TOML. A blocked transition emits
outcome = "blocked" and exits 2. Phase gate checks evaluate only the target phase,
its child tasks, and their effective mapping scope; unscoped validate remains graph-wide.
PLN / PHS Lifecycle
Status State Machine
PLN: draft → active → resolved (gated: plan_coverage)
→ deprecated
PHS: draft → active (gated: predecessors resolved)
→ resolved (gated: delivery_completeness + gates)
→ deprecated
Transition Ownership
| Transition | Owner | Precondition |
|---|---|---|
| PLN: draft → active | spec-planner | Only one active plan allowed |
| PHS: draft → active | spec-executor | Predecessor phases resolved (blocking) |
| PHS: active → resolved | spec-verifier | All deliverables verified, gate passes |
| Any → deprecated | User (manual) | No --force needed; --reason required for TSK only |
Rules
- Only one active PLN at a time —
single_active_plancheck enforces this. - PHS activation order is enforced: activating a phase whose
precedespredecessors are not yet resolved is blocked (outcome: blocked), not merely warned. Activate in order, or usephase next --activate, which only selects phases whose predecessors are resolved. - PHS resolution is gated:
entity update <PHS-ID> --status resolvedauto-runsdelivery_completeness+gates. Blocked (exit 2) if issues exist. - PLN resolution is gated: requires
plan_coverage— all active arch entities must be covered. - No skipping states:
draft → resolvedis invalid. Must pass throughactive. - deprecated is terminal for PLN/PHS/TSK: the lifecycle gate rejects any transition out of a
terminal status, but it only runs on transitions into
active/resolvedfor plan, phase, and task. Arch entities reach no policy, sodeprecated → activeis accepted there — terminal is a convention for them, not an enforced invariant. Deprecating never needs--force;entity update <ID> --status deprecatedis enough. Task deprecation requires--reason.questionentities cannot be deprecated — resolve them instead.
Relation Types (18)
Architecture layer (12):
implements, verifies, depends_on, constrained_by, triggers, answers,
assumes, has_criterion, mitigates, supersedes, conflicts_with, references
Execution layer (4):
belongs_to, task_depends_on, precedes, blocks
Mapping layer (2):
covers, delivers
Agent Workflow Patterns
This section is the heart of this skill. Agents follow these patterns.
Pattern 1: Plan and Phase Setup
Create a graph-native plan, add phases and tasks, then wire task scope. This path creates no Markdown. Direct phase mappings shown in older projects are a legacy taskless path only.
Note: IDs are generated, so a cross-referencing flow like this must capture each ID into a shell variable as it creates the entity. Do not pass
--id, and do not assume the plan isPLN-001.$REQ_AUTHand$REQ_SESSIONbelow are requirement IDs captured the same way when those requirements were created.
# 1. Create the plan (only one active plan allowed)
# --status active sets the entity status; a "status" key inside --metadata does NOT.
PLN=$(spec-graph entity add --type plan \
--title "v1 Delivery Plan" \
--status active | jq -r '.entity.id')
# 2. Create a phase
PHS=$(spec-graph entity add --type phase \
--title "Phase 1 - Auth" \
--metadata '{"goal":"Build authentication","order":1,"exit_criteria":["Auth API complete","E2E tests pass"]}' | jq -r '.entity.id')
# 3. Assign the phase to the plan
spec-graph relation add --from "$PHS" --to "$PLN" --type belongs_to
# 4. Create tasks with the closed TaskContract
TSK1=$(spec-graph entity add --type task --title "Implement auth API" \
--description "Implement the authentication API and tests." \
--metadata '{"order":1,"instructions":["Implement the auth API."],"acceptance":["Auth tests pass."],"must_not":[],"references":[],"qa":[{"command":"go test ./...","expected":"exit 0","evidence":""}]}' | jq -r '.entity.id')
TSK2=$(spec-graph entity add --type task --title "Integrate auth flow" \
--description "Integrate the completed authentication API." \
--metadata '{"order":2,"instructions":["Integrate the auth flow."],"acceptance":["Integration tests pass."],"must_not":[],"references":[],"qa":[{"command":"go test ./...","expected":"exit 0","evidence":""}]}' | jq -r '.entity.id')
# 5. Wire membership, dependency, and canonical task scope
spec-graph relation add --from "$TSK1" --to "$PHS" --type belongs_to
spec-graph relation add --from "$TSK2" --to "$PHS" --type belongs_to
spec-graph relation add --from "$TSK2" --to "$TSK1" --type task_depends_on
spec-graph relation add --from "$TSK1" --to "$REQ_AUTH" --type covers
spec-graph relation add --from "$TSK2" --to "$REQ_SESSION" --type covers
# 6. Validate and obtain the executor/verifier contract
spec-graph validate --layer exec --check task_graph
spec-graph validate --layer mapping --phase "$PHS" --check task_scope
spec-graph phase context "$PHS"
If you are working across separate command invocations rather than one script, recover IDs by querying instead of guessing:
spec-graph entity list --type phase --status active | jq -r '.entities[].id'
spec-graph query sql "SELECT id, title FROM entities WHERE type = 'task' ORDER BY id"
phase context returns {plan,phase,tasks,scope,delivery,blockers,ready_task_ids,blocked_task_ids}.
Each task entry is {entity,contract,prerequisite_ids,covers,delivers}. The same result is available
as RPC phase.context and MCP phase_context.
Pattern 2: Change Handling
When an existing entity changes, always run impact first:
# 1. Compute impact — what else must change
spec-graph impact "$DEC_ID" --dimension behavioral
# 2. Inspect affected targets (parse JSON)
spec-graph impact "$DEC_ID" | jq '.affected[] | {id, type, impact, reason}'
# 3. Check unresolved items
spec-graph query unresolved --type question
# 4. Modify only affected targets (do not touch unrelated entities)
spec-graph entity update "$DEC_ID" --title "New decision"
# 5. Full validation
spec-graph validate
Never modify related entities by guesswork without running impact first.
Pattern 3: Phase Exit
Phase completion is gated by the CLI (v0.3.1+). Running entity update --status resolved
automatically enforces delivery_completeness + gates checks. If issues exist, the
transition is blocked with exit code 2.
# Direct completion attempt — gate runs automatically
spec-graph entity update "$PHS_ID" --status resolved
# If blocked, resolve issues first:
# 1. Review graph-native phase context
spec-graph phase context "$PHS_ID"
# 2. Check what's missing
spec-graph validate --layer mapping --phase "$PHS_ID" --check delivery_completeness
spec-graph validate --layer mapping --phase "$PHS_ID" --check gates
# 3. Fix issues (add delivers, answer questions, mitigate risks)
spec-graph relation add --from "$TSK_ID" --to "$REQ_ID" --type delivers
# 4. Retry
spec-graph entity update "$PHS_ID" --status resolved
# Force completion findings only (structural findings remain blocked)
spec-graph entity update "$PHS_ID" --status resolved --force \
--reason "Accept the documented completion risk"
Update responses expose one of three outcomes: applied, applied_with_force, or
blocked. blocked always means the TOML was left unchanged; a non-nil gate report
does not by itself mean the update was blocked.
Pre-flight checks (optional, for visibility before attempting completion):
# Review scope
spec-graph query scope "$PHS_ID"
# Arch coverage
spec-graph validate --layer arch --check coverage
# Mapping consistency
spec-graph validate --layer mapping --phase "$PHS_ID" --check mapping_consistency
# Exec ordering
spec-graph validate --layer exec --check phase_order
If validate reports issues, resolve them before attempting --status resolved.
Handling "covered but not delivered" mapping failures
When delivery_completeness reports a covered arch entity has no delivers relation, the fix is
to deliver that exact entity. The check compares covered IDs against delivered IDs directly —
it does not traverse implements/verifies. Delivering an implementing interface or test does
not satisfy a covered requirement.
# 1. Identify what the phase covers vs delivers
spec-graph query scope "$PHS_ID"
# 2. Add delivers for the covered entity that lacks it.
# In a task-managed phase, delivers must come from the child task that covers it,
# not from the phase.
spec-graph relation add --from "$TSK_ID" --to "$REQ_ID" --type delivers
# 3. Re-validate
spec-graph validate --layer mapping --phase "$PHS_ID" --check delivery_completeness
Rules:
- Deliver the covered entity itself. There is no proxy resolution — a "minimal proxy set" of implementing entities will not clear the finding.
- If a phase covers something it did not actually deliver, the honest fix is to narrow the
coversscope, not to add adeliversedge that misstates what happened. - Only entity types that may legally receive
deliversfrom a phase are checked; others in scope are skipped by the edge matrix. - Before adding any
delivers, ask whether it accurately represents work completed in this phase, or is just silencing the check.
Pattern 4: Full Patch Orchestration (recommended)
The safest change-handling flow:
1. Identify change target
2. spec-graph impact → compute affected set
3. spec-graph validate → check currently broken rules
4. Modify only affected targets (entity update, relation add/delete, etc.)
5. Semantic review → does each added relation accurately represent the intended meaning?
6. spec-graph validate → re-verify after modifications
The agent modifies only entities in the affected list from step 2.
If an entity outside the list needs modification, first run query neighbors to verify the relationship.
Step 5 (semantic review) is critical. Before re-validating, review every relation you added and ask: "Does this relation reflect a real semantic relationship, or am I adding it to pass a check?" Check passage alone does not prove graph correctness. A graph that passes all checks but contains over-broad relations is worse than one that fails a check with an honest gap.
Pattern 5: Adding a Requirement
Typical flow for adding a new requirement and wiring it into the graph. $PHS and $XCT are
existing IDs obtained from entity list or a prior capture.
# 1. Create requirement, capturing the generated ID
REQ=$(spec-graph entity add --type requirement \
--title "All payments must be idempotent" \
--metadata '{"priority":"must","kind":"non_functional","owner":"payment-team"}' | jq -r '.entity.id')
# 2. Attach acceptance criterion
ACT=$(spec-graph entity add --type criterion \
--title "Duplicate request within window processed only once" \
--metadata '{"given":"Payment request already sent","when":"Same request resent","then":"No duplicate processing; return existing result"}' | jq -r '.entity.id')
spec-graph relation add --from "$REQ" --to "$ACT" --type has_criterion
# 3. Map to phase using covers (not planned_in)
spec-graph relation add --from "$PHS" --to "$REQ" --type covers
# 4. Link crosscut constraint (if applicable)
spec-graph relation add --from "$REQ" --to "$XCT" --type constrained_by
# 5. Validate arch layer
spec-graph validate --layer arch --entity "$REQ"
Pattern 6: Bootstrap (graph from existing docs)
When existing markdown documents are available:
# 1. Extract candidates — generates review candidates, not auto-committed
spec-graph bootstrap scan --input ./docs/ --format json
# 2. Review — filter low-confidence items
cat extracted.json | jq '.entities[] | select(.confidence >= 0.7)'
# 3. Preview candidates in review mode (the default; does not write)
spec-graph bootstrap import --input extracted.json --mode review
# 4. Apply the reviewed candidates atomically
spec-graph bootstrap import --input extracted.json --mode apply
--mode apply writes the batch as one transaction and returns only created and skipped.
Candidates below 0.5 confidence, candidates that already exist, and relation candidates whose
endpoint types fail the edge matrix are skipped. An unknown entity or relation type, a malformed
entity ID, a missing relation endpoint, or a write failure aborts the batch without changing the
graph and exits non-zero. Malformed input is rejected regardless of its confidence. A human must
confirm, or the agent must cross-reference against the source document before applying candidates.
Pattern 7: Revising an Arch Entity
When an arch entity's meaning changes and the prior wording must stay on the record, use
entity revise rather than editing in place. entity update rewrites history; revise
preserves it as a chain.
# Compute impact first — revision moves relations
spec-graph impact "$REQ" --dimension behavioral
spec-graph entity revise "$REQ" \
--title "All payments must be idempotent within a 24h window" \
--reason "Original requirement left the dedup window unspecified"
This is one atomic operation that:
- creates a new entity with a newly generated ID, in
draftstatus - carries the prior entity's outbound relations onto the revision
- adds
revision supersedes prior, recording--reasonin the edge metadata - repoints inbound relations onto the revision
- deprecates the prior entity
Capture the new ID from the response — the revision has a different ID than the original:
NEW=$(spec-graph entity revise "$REQ" --reason "..." | jq -r '.revision.id')
The response is {revision, superseded, carried_relations, retained_relations}.
carried_relations lists inbound relations moved onto the revision; retained_relations lists
those deliberately left on the superseded entity. Both are null rather than [] when empty.
Which relations stay behind: mapping relations (covers/delivers) from a resolved phase
or task are retained, not moved. A resolved phase delivered the wording that existed at the time,
so crediting it with delivering a later revision would be false. Mapping relations from
non-resolved phases and tasks move forward with everything else. mapping_consistency exempts
resolved sources, so these retained edges do not produce findings.
Constraints:
- arch entities only. Revising a
PLN/PHS/TSK/CHGfails with exit 3 — exec entities do not form revision chains. --reasonis required (exit 3 when omitted).- an entity that is already superseded cannot be revised again (
CONFLICT, exit 2). Revise the latest revision instead; the error message names it. - a
deprecatedentity cannot be revised — chains start from a live entity. --title,--description, and--metadataare each optional and carry the prior value forward when omitted. Supplying--metadatareplaces the metadata object wholesale.- the revision is created in
draft, so it needs the usualcovers/deliverswiring to be delivered by a phase.
revise is CLI-only. It is not exposed over RPC or MCP.
Pattern 8: Atomic Batch (RPC only)
Pattern 1 wires a plan with one command per entity and per relation. Each command commits on its
own, so a failure partway through leaves a half-built plan that must be unwound by hand. The
batch.apply RPC method commits an entire graph shape as one transaction instead.
Because IDs are generated, a relation cannot name an entity the same request is still creating.
Each entity may therefore declare a ref, a name valid only inside that request, and relations
address endpoints by ref. The response reports the generated ID next to each ref.
{"jsonrpc":"2.0","id":1,"method":"batch.apply","params":{
"entities":[
{"ref":"plan","type":"plan","title":"v1 Delivery Plan","status":"active"},
{"ref":"phase","type":"phase","title":"Phase 1 - Auth",
"metadata":{"goal":"Build authentication","order":1,"exit_criteria":["Auth API complete"]}}
],
"relations":[
{"from":"phase","to":"plan","type":"belongs_to"},
{"from":"phase","to":"REQ-1752239482-k3f","type":"covers"}
]
}}
The response is {entities,relations}, where each entity entry is {ref,entity}. Capture the
generated IDs from it; a ref has no meaning after the call returns.
Rules:
- an endpoint is resolved as a ref when one was declared in the same request, and otherwise as the
ID of an entity that already exists. The
coversrelation above mixes both forms. refis optional, and it is what makes a generated ID reachable. An entity declared without a ref is still created, and relations can address it by an ID the caller supplied itself; only an ID the engine generated is unreachable within the same request.- a ref must not be shaped like an entity ID, which is what keeps the two namespaces apart.
- every entity is created before any relation, so ordering matters only among relations.
- nothing is skipped. Any rejected item aborts the batch and leaves the graph untouched, unlike
entity importandbootstrap import, which skip already-existing items. Errors name the position of the offending item, as inrelation 0: .... - each item faces the same validation as its single-item counterpart: the edge matrix, symmetric
relation ownership, phase scope, and
delivers-driven activation of a draft target all apply.
batch.apply is RPC-only. There is no CLI command and no MCP tool for it.
Validation Checks Guide
When to use each check. See references/validation-rules.md for detailed rules.
Architecture Layer Checks (--layer arch)
| Check | Purpose | When to Run |
|---|---|---|
orphans |
isolated arch entities with no relations | periodic cleanup, before phase start |
coverage |
missing implementations / tests | required before phase exit |
cycles |
circular references in depends_on chains | after adding relations |
conflicts |
semantic conflicts between entities | after changes |
invalid_edges |
arch edge matrix violations | after adding relations |
superseded_refs |
active refs to deprecated entities | after deprecation |
unresolved |
open questions, unverified assumptions, unmitigated risks | before phase start |
Execution Layer Checks (--layer exec)
| Check | Purpose | When to Run |
|---|---|---|
phase_order |
phases with precedes/blocks form a valid sequence | after adding exec relations |
single_active_plan |
only one plan is active | after plan creation or status change |
orphan_phases |
phases not belonging to any plan | after adding phases |
exec_cycles |
circular precedes/blocks chains | after adding exec relations |
invalid_exec_edges |
exec edge matrix violations | after adding exec relations |
orphan_changes |
changes with no relations to other entities | after adding changes |
task_graph |
task parents, same-phase dependencies, and cycles | after changing tasks or dependencies |
Mapping Layer Checks (--layer mapping)
| Check | Purpose | When to Run |
|---|---|---|
plan_coverage |
all active requirements are covered by some phase | before phase start |
delivery_completeness |
covered arch entities have delivery evidence | auto-enforced on phase → resolved |
mapping_consistency |
covers/delivers targets exist and are arch entities | after adding mapping relations |
invalid_mapping_edges |
mapping edge matrix violations | after adding mapping relations |
gates |
unresolved questions, unmitigated risks, draft decisions | auto-enforced on phase → resolved |
task_scope |
task coverage, delivery subset, and no mixed mappings | after changing task mappings |
For task-managed completion, four checks/gates matter: structural task_graph, mapping
task_scope, task completion delivery/evidence gates, and phase child-resolution plus existing
delivery_completeness/gates checks.
Common Combinations
# Before phase start
spec-graph validate --layer exec --check single_active_plan
spec-graph validate --layer exec --check phase_order
spec-graph validate --layer arch --check unresolved
spec-graph validate --layer mapping --check plan_coverage
# Before phase completion (now auto-enforced by entity update --status resolved)
# These are still useful for pre-flight visibility:
spec-graph validate --layer arch --check coverage
spec-graph validate --layer mapping --phase "$PHS_ID" --check delivery_completeness
spec-graph validate --layer mapping --phase "$PHS_ID" --check gates
# After any change
spec-graph validate
Interpreting Impact Results
Key fields in impact JSON output:
{
"affected": [
{
"id": "API-1752239500-t7n",
"type": "interface",
"depth": 1,
"impact": {
"overall": "high",
"structural": "high",
"behavioral": "medium",
"planning": "low"
},
"reason": "direct implementation"
}
],
"summary": {
"total": 5,
"by_type": {"interface": 2, "test": 3},
"by_impact": {"high": 1, "medium": 2, "low": 2}
}
}
Agent behavior rules:
overall: high→ must review and modify if neededoverall: medium→ inspect content, modify if actually affectedoverall: low→ scan list only, modification rarely needed
Dimension filtering: use --dimension to focus on specific concerns
- interface change →
--dimension structural - policy/behavior change →
--dimension behavioral - schedule/scope change →
--dimension planning
Exit Codes
| Code | Meaning | Agent Action |
|---|---|---|
| 0 | success | proceed to next step |
| 1 | runtime error | check error message, retry or report |
| 2 | validation failure / gate blocked | resolve issues from output, or use --force |
| 3 | invalid input | check arguments / schema, retry |
Caveats
bootstrap importdefaults to--mode review, which does not write. Use--mode applyonly after review.supersedesrequires both entities to be the same type. It is directional: stored in thefromentity's file, sorevision supersedes priormeans thefromentity is newer. Preferentity reviseover adding this edge by hand — it wires the edge, moves relations, and deprecates the prior entity in one operation.conflicts_withdoes not allow self-loops. It is symmetric: stored in the lexicographically smaller entity's file. Both directions are queryable via the index.- Adding a relation that violates the allowed edge matrix fails with exit code 3.
On failure, consult the edge matrix in
references/data-model.md. metadatais a JSON string. Each type has required fields — seereferences/data-model.md.--phaseis only valid with--layer mappingor--layer all. Using--phasewith--layer archor--layer execreturns an error.- Only one plan may have
activestatus at a time. Thesingle_active_planexec check enforces this. - Entity timestamps (
created_at,updated_at) are stored in TOML and populated automatically on create/update. - After
git mergewith conflicts in TOML files, runspec-graph doctorto validate integrity. - The SQLite index is rebuilt automatically on each command if TOML files changed. No manual sync needed.
- Generated IDs are
PREFIX-<unixSeconds>-<rand3>. Collisions are avoided by a crypto-random suffix plus an existence retry, so parallel branches are safe in practice. Never predict one; read it from.entity.id. LegacyPREFIX-NNNIDs remain valid — both forms pass validation and no migration is required.
Anti-Patterns
These are known failure modes. If you catch yourself doing any of these, stop and reconsider.
1. Inventing or sequencing entity IDs
Symptom: passing --id REQ-004 because REQ-003 exists, or referencing PHS-001 in a
relation command without ever reading that ID from a response.
Why it's wrong: IDs are generated as PREFIX-<unixSeconds>-<rand3>. There is no counter to
continue. A guessed ID either fails as not-found or, worse, silently attaches a relation to an
unrelated entity.
Correct approach: omit --id, capture .entity.id into a variable, and reference the
variable. Recover forgotten IDs with entity list or query sql, never by guessing.
2. Mixing arch and exec concerns
Symptom: adding a requirement directly to a phase using arch-only relations,
or treating a phase as an arch entity by linking it with arch-only relations.
Why it's wrong: arch and exec are separate layers with separate edge matrices. Cross-layer
connections belong in the mapping layer using covers and delivers.
Correct approach: use covers (phase→arch) to express intent, delivers (phase→arch)
to express completion.
3. Editing SQLi
…(truncated)