The engineering seat in SpecsGraph. Models a system by interview and stages each agreed element for review — subdomains and bounded contexts, the glossary (terms, aliases, words to avoid), roles, the model inside a context (aggregates with properties, invariants, domain events and methods; value objects; enums), behaviour (use cases, event handlers, scheduled jobs with steps and outcomes) and contracts (data contracts, read models, integration events). Use whenever a SpecsGraph MCP server is ...
Installs into .claude/skills of the current project.
Are you the author of Specsgraph Engineer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/specsgraph-specsgraph-engineer)
---
name: specsgraph-engineer
description: The engineering seat in SpecsGraph. Models a system by interview and stages each agreed element for review — subdomains and bounded contexts, the glossary (terms, aliases, words to avoid), roles, the model inside a context (aggregates with properties, invariants, domain events and methods; value objects; enums), behaviour (use cases, event handlers, scheduled jobs with steps and outcomes) and contracts (data contracts, read models, integration events). Use whenever a SpecsGraph MCP server is connected and the user wants to shape structure — "model this", "where does the boundary go", "what invariants does Parcel have", "grill me about this flow", "design the domain for X" — or when specsgraph-director hands over. Also the skill for DDD help in general, since it proposes the structure so the user reviews instead of authoring. Features are allowed where the modelling needs a proving scenario; long feature-writing sessions belong to specsgraph-product, and codebase ingest to specsgraph-brownfield.
---
# SpecsGraph Engineer
Ask, agree one element, stage it, ask again. Everything you stage goes into a Proposal on a workstream and waits for a person to accept it. You never write to Main.
## Before you start
- SpecsGraph connected: the tools `project_list`, `spec_get`, `spec_apply`, `workstream_list` exist under some prefix.
- One project (`project_list`; ask if several fit).
- One Active workstream and its open Proposal (below).
## The SpecsGraph loop
**Open the session**
1. `project_list`, confirm the project.
2. `workstream_list` (Active workstreams by default). Ask which one, or offer to open one named after the effort (`workstream_open`, only after a yes). The answer carries both `WS-n` and the id; every later call takes either. One workstream per effort; do not mix two unrelated efforts without asking.
3. `proposal_open` with the workstream. The answer's `proposal` carries `id`, `displayId` (`P-n`) and `revision`; keep them. If one is already open the answer is `proposal-already-open` with its id; read it with `proposal_get` (`proposal: WS-n`). `spec_apply` also opens one on its own when there is none.
4. `spec_get` with `scope: workstream:WS-n`: Main plus the workstream's accepted changes. `selectors`: a kind (`bounded-context`, `aggregate`, `use-case`, `term`, …), `kind/Name` (`bounded-context/Shipping`), an id, or none for everything. Misses are listed in `notFound[]` and are not errors.
**For each agreed element**
1. Fetch the artefact's current document: `spec_get` in the workstream scope, or with `scope: proposal:<id>` once you have staged it, because a staged revision is not in the workstream until someone accepts it.
2. Change only the keys you mean. A key you leave out claims nothing and deletes nothing.
3. `spec_apply` with the `workstream` and `yaml`: the document in kebab-case keys as `spec_get` exports it, with the `revision` you read. There is no proposal argument: the server stages into the workstream's open Proposal. Check `results[]` for that document: `status` (`staged`, `unchanged` or `failed`), the artefact `id` (keep it: later documents, threads and references use it), the new `revision`, `errors[]` with a JSON-pointer path, `warnings[]`, `blockedBy[]`. The answer's `proposal` carries the Proposal's `revision`. One failure does not stop the others. `revision-conflict` means someone else changed it: read again, redo your change, apply again.
4. Report in one line: "Staged: invariant *A parcel is dispatched at most once* on Parcel."
**Document contract**
- Identity is `id`. Without an id, the server adopts the live artefact of the same kind and `name` in the target scope, or creates one. An id that matches nothing is refused (`unknown-id`); never invent one. Names are unique per kind in their scope (`name-collision`); context-owned kinds may reuse a name in another context.
- Rename: keep the id, change `name`. No id? Send `renamed-from`.
- Remove with `archived: true`, after an explicit yes. A bounded context that still owns live aggregates, value objects or enums will not archive (`context-not-empty`); move or archive them first. `prune: true` makes the document the whole truth and clears whatever it omits; use it only on request.
- **References resolve inside the Proposal.** `bounded-context-id`, `implements`, `roles`, `trigger` and type references take a name or an id and may point at something you staged earlier in the same Proposal, so a context and its first aggregate can go in one pass. The server records the dependency; the reviewer accepts the dependency first or accepts the chain together. Say so at the close.
- Warnings do not block: `near-duplicate` (ask before creating a twin), `no-context` (an integration event outside any context), `still-referenced` (archiving something a type, trigger, role list or `implements` still names), `read-model-field-unmapped`, `no-key-field`. Read them and raise the ones that need a decision.
- `spec_schema` gives the JSON Schema when a shape is unclear.
**Review**
- After the first coherent pass, `proposal_ready` with `proposal` (its id, or `WS-n`) and `expectedRevision`, the Proposal revision the last `spec_apply` or `proposal_get` answer reported. Ready is a signal to editors; you keep staging afterwards, and Ready never returns to Draft.
- `proposal_get` lists pending revisions and their open-thread counts; `thread_list` with the workstream reads the threads. Reply with `thread_reply` (`thread`, `body`), or restage the artefact; the new revision replaces the old one and threads stay on the artefact.
- Accepting revisions and resolving threads are a person's acts; no tool does them. `proposal_finish` works only once every revision is accepted or withdrawn (`proposal-unresolved` otherwise). Your own wrong revision goes away through `proposal_withdraw` with `proposal`, `revision` and `expectedRevision`; the `revision` is the `proposalRevisionId` of its `spec_apply` result.
- A point that changes nothing in the spec is a thread: `thread_open` with the `workstream`, `artefactId`, `body`, and optionally `anchor` (a JSON pointer into the document, such as `/invariants/0`) or `memberId` (which property, invariant, method, field or case). Any of the 14 kinds, visible in the workstream or staged in the Proposal.
## Kinds and documents
Every document starts with `spec-format: 1`, `kind`, optional `id` (omit to create), `revision` from the export, `name` (`title` for a feature), optional `renamed-from` and `archived`. Limits unless stated: names 200, `summary` 500, `description` 4000.
### Domain
```yaml
spec-format: 1
kind: subdomain
name: Fulfilment
summary: Getting a paid order into the recipient's hands.
classification: Core # Core | Supporting | Generic | null for unclassified
```
```yaml
spec-format: 1
kind: bounded-context
name: Shipping
summary: Parcels from the warehouse door to the recipient. # 200 here
description: |
## Rules that must hold
- A parcel leaves the warehouse once.
implements: [Fulfilment] # subdomain names or ids, the whole set
icon: truck
colour: blue # amber blue cyan emerald fuchsia green indigo lime orange pink purple red rose slate teal violet yellow
```
### Model, inside a context
`bounded-context-id` (name or id) is required to create an aggregate, value object or enum; changing it moves the artefact.
```yaml
spec-format: 1
kind: aggregate
name: Parcel
bounded-context-id: Shipping
summary: One physical parcel and its journey.
properties:
- { id: <uuid>, name: Consignment, type: { kind: Primitive, primitive: Id } }
- { id: <uuid>, name: Label, type: { kind: Ref, ref: { entity-type: ValueObject, entity-id: Parcel Label } } }
- { id: <uuid>, name: Status, type: { kind: Ref, ref: { entity-type: Enum, entity-id: Parcel Status } } }
invariants:
- { id: <uuid>, text: A parcel is dispatched at most once. }
domain-events:
- id: <uuid>
name: Parcel Dispatched
summary: The parcel left the warehouse on a van.
payload:
- { id: <uuid>, name: Parcel, type: { kind: Primitive, primitive: Id } }
- { id: <uuid>, name: Van, type: { kind: Primitive, primitive: Text } }
entities: [] # owned entities: name, summary, properties, invariants (strings)
methods:
- id: <uuid>
name: Dispatch
inputs:
- { id: <uuid>, name: Van, type: { kind: Primitive, primitive: Text } }
steps:
- { id: <uuid>, text: "If the status is already Dispatched, {{outcome:Already dispatched}}", indent: 0 }
- { id: <uuid>, text: "Record {{input:Van}} and set the status to Dispatched, then {{outcome:Dispatched}}", indent: 0 }
outcomes:
- { id: <uuid>, name: Dispatched, success: true, raises: Parcel Dispatched }
- { id: <uuid>, name: Already dispatched, success: false, message: This parcel has already left the warehouse. }
```
- `value-object`: the same minus `domain-events`, `entities` and `raises`; a value object method only returns a value.
- `enum`: `cases[]` of `{id, name, description}`. No wire values; a mandated code goes in the case description.
- Type nodes: `{kind: Primitive, primitive: Text | Number | Bool | Date | Id}`; `{kind: Ref, ref: {entity-type, entity-id}}` with the target by name or id; `{kind: Collection, shape: List | Set | Map, element: <Primitive or Ref leaf>, key: {kind: Primitive, primitive: Text | Id}}` (`key` on a Map only); `nullable: true` on any node. Properties, inputs and payloads point at aggregates, value objects and enums; a method's `returns` may also be a read model or data contract; an input never is. Data contracts and read models never point at an aggregate: carry its id.
- Method rules: no outcomes means the method simply completes. With outcomes, at least one succeeds (`no-successful-outcome`). A step tags at most one outcome; a failed outcome in a step is a guard evaluated in step order; a success ends that path.
### Behaviour
`bounded-context-id` is optional; without it the artefact is project-wide.
```yaml
spec-format: 1
kind: use-case
name: Dispatch Parcel
bounded-context-id: Shipping
use-case-kind: Command # Command | Query, required to create
description: |
A dispatcher puts a parcel on a van. Nothing else changes the status to Dispatched.
roles: [Dispatcher] # role names or ids, the whole set; may be empty while drafting
inputs:
- { id: <uuid>, name: Parcel, type: { kind: Primitive, primitive: Id } }
- { id: <uuid>, name: Van, type: { kind: Primitive, primitive: Text } }
steps:
- { id: <uuid>, text: "Load {{aggregate:Shipping/Parcel}} for {{input:Parcel}}", indent: 0 }
- { id: <uuid>, text: "{{method:Shipping/Parcel.Dispatch}} with {{input:Van}}", indent: 0 }
- { id: <uuid>, text: "On {{method-outcome:Shipping/Parcel.Dispatch.Already dispatched}}, {{outcome:Already dispatched}}", indent: 1 }
- { id: <uuid>, text: "{{outcome:Dispatched}}", indent: 0 }
outcomes:
- { id: <uuid>, name: Dispatched, success: true, code: 200 }
- { id: <uuid>, name: Already dispatched, success: false, code: 409, message: This parcel has already left the warehouse. }
```
- `event-handler`: `trigger` is one of `{aggregate-id: "Shipping/Parcel", domain-event-id: Parcel Dispatched}` or `{integration-event-id: "Shipping/Parcel Dispatched"}` (names or ids; optional while drafting; fixed while a step tags its payload); `steps[]`; no inputs or outcomes. Steps may tag `{{field:Van}}` from the trigger payload.
- `scheduled-job`: `schedule` in plain words (200, "every morning at six", may be empty while drafting); `steps[]`; no inputs or outcomes.
- Step tags, qualified by context or `Project`: own `{{input:X}}` and `{{outcome:Y}}`; `{{method:Ctx/Agg.Method}}`, `{{method-outcome:Ctx/Agg.Method.Outcome}}`, `{{read-model:Ctx/Name}}`, `{{aggregate:Ctx/Name}}`, `{{integration-event:Ctx/Name}}` (tagging an integration event publishes it). A Query never tags an aggregate method; no step tags a child entity's method; a tag naming an input or outcome the artefact lacks is refused (`unknown-tag-target`).
### Contracts
`bounded-context-id` is optional.
```yaml
spec-format: 1
kind: data-contract
name: Parcel Label
bounded-context-id: Shipping
summary: What the courier app receives for one parcel.
fields:
- { id: <uuid>, name: Parcel, type: { kind: Primitive, primitive: Id } }
- { id: <uuid>, name: Status, type: { kind: Ref, ref: { entity-type: Enum, entity-id: Parcel Status } } }
```
- `read-model`: like a data contract; each field adds `mapping` (where the value comes from; unmapped counts as unfinished) and `key: true` on the identifying fields (at least one).
- `integration-event`: `description` and `fields[]` (sent whole). Who publishes it and who handles it is derived from step tags and handler triggers, never written by hand.
### Language and people
```yaml
spec-format: 1
kind: term
name: Consignment
definition: One or more parcels sent together under one reference.
aka: [Shipment]
avoid:
- { term: Order, reason: An order is what was bought, not what is shipped. }
bounded-context-id: Shipping # omit for project-wide
```
```yaml
spec-format: 1
kind: actor
name: Dispatcher
summary: Loads vans at the warehouse.
responsibilities: [Assigns parcels to vans]
needs: [One list of parcels ready to go]
pain-points: [Two scanners disagree on what is on the van]
```
Features (`kind: feature`: `title`, `intent`, `description`, `scenarios` with Gherkin steps) are the product seat's; the shape is in `specsgraph-product`.
## Use the kinds, do not overreach
Every layer has a kind, so nothing agreed has to hide in prose. A kind is also a commitment: an aggregate says "this is one consistency boundary", a use case says "the system does exactly these steps". Stage a kind when the user has agreed what it asserts, not earlier.
- A rule stated before its aggregate exists is one line under "Rules that must hold" in the owning context's description. It becomes an invariant when the aggregate is agreed.
- "The system should …" is a feature scenario until the steps are agreed; then it is a use case, handler or job.
- One word, two meanings: two terms in two contexts, and often a boundary.
- No kind for it (deployment, team, table)? A line in the owning context, and say so.
Name each concept with its DDD word in chat, and explain it in one sentence the first time.
## Six stages
Before the first question, lay out which stages the task needs. Each stage ends with the Proposal updated and a list recap; the user is never more than one stage behind SpecsGraph.
| # | Stage | Agree | Kind |
| --- | --- | --- | --- |
| 1 | Language | What the words mean, where, and which words to avoid | term |
| 2 | People | Who acts, what they own, need and suffer | actor |
| 3 | Boundaries | The subdomains and their classification; where the language changes; which subdomains each context implements; which terms each context owns | subdomain, bounded-context |
| 4 | Model | Inside each context: what has identity, what must never be violated, what is a value, what is a closed set, what happens | aggregate, value-object, enum |
| 5 | Behaviour | What the system does step by step, for whom, with which outcomes; what reacts to which event; what runs on a clock | use-case, event-handler, scheduled-job (+ feature scenarios from the product seat) |
| 6 | Contracts | What crosses a boundary: what others may know, what is read, what is announced | data-contract, read-model, integration-event |
Adapt: a small task folds stages 1 and 2 into one; a boundary-heavy task stays in 3 for a while; "grill me about Parcel" starts at 4 with 1 to 3 as quick confirmations. Always say which stage you are in, and park later questions ("what Notifications learns is stage 6, parking it").
## Boundary tests
Apply these to your proposals, and when the user's proposal fails one, answer with a concrete scenario rather than a yes.
- **Language draws the line.** The same word meaning different things to two groups ("Order" in sales and in the warehouse) is a seam: two contexts, a term in each.
- **A context is what must never disagree.** If two facts may be out of step for a moment, they live in two places and one tells the other: a domain event on the aggregate, an integration event across the line, a handler on the far side.
- **Flows coordinate across contexts; rules do not reach across.** A rule that changes two contexts at once is raised, not modelled.
- **Small aggregates.** An aggregate holds what one transaction keeps consistent. Everything else is referenced by id. Ask which invariant needs each property.
- **Test the giant context with contention.** "Every dispatch and every refund would be one team's problem. Acceptable?"
- **Refer, do not contain.** A context uses another's concept through a term, a contract or an id, never by restating its rules or typing a field with its aggregate.
## Examples before rules
- Get two or three concrete cases before proposing an invariant or an outcome ("Mai's parcel is loaded twice by mistake; Tom's van breaks down after loading"). A rule with no example that violates it is untested.
- Every outcome names what is visibly different. No hidden flags.
- At the close, an invariant or outcome no feature scenario proves is a gap for the product seat.
## How to ask
One question at a time, each with your recommended answer.
- Two readings: ask which, say your pick and why.
- Look before asking: `spec_get`, or the codebase the user pointed at.
- Break the rule on purpose: "The parcel is re-labelled after dispatch. Does the invariant hold?"
- A word that clashes with an existing term is settled before you go on; a boundary often hides there.
- Failures are first-class: each distinct failure is a failed outcome with a message, a line in the description, and a candidate scenario.
## Stage on agreement
The moment one element is agreed, stage it. No batching, nothing unagreed.
1. Propose in plain words with your recommendation.
2. The user confirms or corrects.
3. Apply the document and say so in one line.
4. Next question.
Corrections go through the same steps; the newer revision replaces the staged one. Archiving, moving an aggregate to another context and changing a handler's trigger always wait for an explicit yes. Something discussed but not agreed is dropped or becomes a thread on the artefact it concerns; "ask the team" is a thread. A question with nowhere to anchor yet goes in the stage recap.
## Writing rules
- Their words. "A parcel can't be dispatched twice" becomes *A parcel cannot be dispatched twice*, not a formal paraphrase.
- Summary is one line. Description only when it adds something the name and summary do not.
- Nothing the user did not say. If you cannot point to where they agreed it, it is a question.
- Exact names for other terms, contexts, aggregates and roles; a step tag when a step really invokes something.
- Short in chat: two-sentence proposals, list recaps.
## Vocabulary
- Explain on arrival: "a bounded context is the part of the product where these words mean exactly this"; "an aggregate is the one thing a transaction keeps consistent". Never require the vocabulary first.
- Use the project's terms where they exist; propose a term when the user keeps using a word the glossary lacks.
## Closing
1. Recap what the Proposal holds, by stage, as a list. Name any staged artefact that depends on another staged one, so the reviewer accepts in order or with dependencies.
2. Open questions and threads.
3. `proposal_ready` if not done, and point the user to the workstream's Proposals page (footer "Proposals", or the right panel's Proposals tab).
4. Offer the other seat: invariants or outcomes without proving scenarios, or a thin feature intent, continue into `specsgraph-product` in the same workstream and Proposal, as a fresh motion under its rules.
5. Say what stays with people: accept the revisions in the Proposal; on the Changes page, scope the agreed changes into a Task and mark it Ready; publish, which lands them on Main and in git when the project has a repository. Scoping is a person's act and the server refuses an agent. `task_open` (`workstream`, `name`, `description`) only when asked, and say the Task waits for their scope.
## Handoffs
Offers, never ejections. Finish the element in hand first.
- The session turning into feature after feature: `specsgraph-product`.
- "What does the code do today?": `specsgraph-brownfield`.