The node card (YAML)
One YAML file per node: what the step is, what it is told to do, which model and tools it may use, what arrives and what must never arrive. The validator accepts the same fields as JSON. A published version is never edited in place, so a change means a new file and a new version number.
modelclaude-sonnet-5reaches The model this step runs on, named the way the provider names it.A stylesheet on the graph is a default for the nodes that name no model; a line here outranks it. A reader can still point the run at something else.toolsnonereaches Capabilities it may reach for: a shell, a search index, a browser.The card names a capability rather than a vendor, so a graph says what it touches and never what you bought.mcpfilesystemreaches An MCP server this step may talk to, by the name it is registered under on the machine that runs the graph.Two nodes naming the same server have the same reach; two naming different servers do not. Reach is declared per card, so a graph says what each node may touch rather than what the whole system may.skillskills/code-builder.mdreaches A written procedure it follows. A pointer only.Nothing here reads what this path points at, so no skill document travels in the folder. Each blueprint’s README lists the ones you supply yourself.cannotacceptance-criteriacannot be What must never arrive. The validator enforces it against every incoming edge, no matter which node draws the edge.Data types from the vocabulary, and nothing else. Writing one here is what turns a stated rule into a checked one.will_notread the checks the work will be run againstcannot be What the card promises, in the author’s own sentences. It reaches whoever runs the node, and the model that is handed the specification at run time.The prohibitions nothing can check, kept apart from the ones the validator can, so a reader can tell them apart without running anything.risk_markersnone declaredreaches The blast radius, declared. The card states what this step could break if it goes wrong.Risk-marker terms from the vocabulary, and nothing else. Writing one here puts the blast radius in the file, where the validator can hold the author to a word the vocabulary defines.
A node is a card, and the card is checkable
This is code-builder@1.0.0 as the archive stores it. Nine places on the card decide what the node is, what it does, the brief it is handed, which model it runs, what it may reach, what arrives and what must never arrive. The last of those is why whoever writes the code never reads the tests the work is judged against. Pick one on the right and the lines it is about light up on the left.
Every field, and what holds it
The ids under a field are the curated core, spelled as a card has to spell them.
- an error code refuses the bundle
- a warning is reported and the bundle loads
- free text is shown and checked by nothing
- id · version
A lowercase hyphenated id, optionally namespaced, and the card's own semantic version. A graph pins a node to the pair as
id@version.card/bad-idcard/bad-versionrefuses the bundle- name · action · agent · notes · author · provenance
Prose for whoever opens the card, carried into the download.
provenancesays where the card came from when its author says so, and most cards leave it out.- free text
- type
One node-type term, and the only field that says what kind of actor the node is. The autonomy reading asks two things of it: whether the type is a kind of
human-in-the-loop, and whether it decides which other nodes run. The exporter draws the node's shape from it. Ashell-toolwith no command inparams.tool_commandis reported ascard/missing-field, and the bundle still loads. Three of the curated values are categories:human-in-the-loop,evaluativeandorchestration. The validator accepts a category, and the exporter has no shape for one and falls back tobox, so write the concrete subtype.card/unknown-termcard/wrong-term-kindrefuses the bundle- phase
Where in the lifecycle the node stands: any number of the five. The key takes a single term or a sequence, because both spellings read naturally in YAML. The five are closed and nobody may add one, so a namespaced entry is refused. An empty list is a complete answer rather than a hole, because the phases describe the blueprint rather than every node in it. An intake, a retrieval step or a memory store stands in none of them. An empty list is shown as empty, never as missing.
5 curated values
card/unknown-phasecard/namespaced-phaserefuses the bundle- spec
The instruction handed to the agent when the graph runs.
card/spec-too-thinreported, still loads- model
Written the way the provider writes the identifier. A default rather than a binding: a graph's model_stylesheet sets the model for every node matching a shape. An explicit field here outranks the sheet (Attractor spec §8.5). Whoever runs the blueprint outranks both.
- free text
- tools
The capabilities the node is permitted to reach for, as tool terms rather than labels.
toolssays what the node may do andmcpsays which process supplies it, and a node can carry either without the other.card/unknown-termcard/wrong-term-kindrefuses the bundle- mcp
The MCP servers the node needs. The vocabulary has no term for a server and is not going to grow one, so every entry is free text.
- free text
- skill
The path, inside the folder, of the document that defines the agent's behaviour.
- free text
- inputs · outputs
The ports, each with a
name, atype, adescriptionand, on an input,required. The type is one data-type term, and it is the only part of a port the resolver pairs on. This is what makes an edge checkable at all: an edge holds when the source's output type is the target's input type or a narrower kind of it. The name is the end of an edge rather than a label. A DOT edge writes[out="build", in="brief"]to say which pair of ports it joins, so a name is unique within a side. The description is free text for whoever wires the graph, where a port says the part its type cannot.requiredis true unless the card says otherwise. On an output it describes nothing, and the validator reports it ascard/bad-typeagainst the exact path rather than dropping the key in silence. The bundle still loads.card/unknown-termcard/wrong-term-kindcard/duplicate-portrefuses the bundle- dependencies
Which cards this one receives from, checked both ways. A declared dependency with no edge into the node is refused. An edge from a card the node does not list is reported as
bundle/undeclared-dependency, and the bundle still loads.bundle/missing-dependencyrefuses the bundle- cannot
Data types the node must never receive: the same data-type terms a port takes, and nothing else. Each entry is a prohibition the resolver enforces, held to the same rule that pairs an edge's ports. An incoming edge able to carry that type fails the whole blueprint. A sentence written here does not resolve, because the validator has no way to hold a graph to a sentence and this is the field it holds graphs to.
card/unknown-termcard/wrong-term-kindbundle/prohibition-violatedrefuses the bundle- will_not
The prohibitions the author states and the validator cannot check: “never opens a shell”, “does not edit the code under test”. Nothing reads them, and they are addressed to whoever runs the node and to the model that is handed the specification at run time.
cannotholds the rules the validator enforces andwill_notthe ones it cannot. Both are legitimate. A reader has to be able to tell which is which without running anything. The one thing checked here is that no data type is written in it by mistake.card/prohibition-misfiledreported, still loads- risk_markers
What could go wrong if this step misbehaves, as risk-marker terms rather than sentences. Seven core markers carry a weight in DarkPrint's configuration, and Security subtracts each one it finds. A marker coined in a blueprint's own namespace sets its own weight. A marker with no weight, including the two category terms
execution-riskandisolation-breach, is declared and never scored.card/unknown-termcard/wrong-term-kindrefuses the bundle- params
Nested configuration, free in shape, which must survive a JSON round trip. The card's digest is a hash of its JSON form, and a blueprint pins a card by that digest. A value that cannot be serialised is refused where it is written rather than later, when two digests disagree. Two keys are read:
max_iterationsis the iteration cap, andtool_commandis the command ashell-toolruns.card/bad-typerefuses the bundle- a second version of a card in one blueprint
When a blueprint carries two versions of one card, the newer one must bump its version at least as far as the change requires. Adding to
cannotnarrows what the node accepts, so it needs a major bump. Changingmodelneeds a minor bump.card/version-bump-too-smallrefuses the bundle
What the validator checks
Every code beside a field is a real diagnostic. The validator prints it on the upload page and in the build when the rule is broken, so a code can be grepped for and tripped on purpose, which is what separates a checked rule from a promise.
This message is the validator’s own output rather than text written for this page. It comes from adding one edge, planner -> builder [label="acceptance criteria"];, to the starter blueprint.
errorbundle/prohibition-violated
Card `code-builder@1.0.0` declares that `builder` cannot receive `acceptance-criteria`, and edge `planner -> builder` carries that type.
`planner` declares the output `criteria` (`acceptance-criteria`). Remove the edge, or take `acceptance-criteria` out of `cannot` on `code-builder`. Moving it to `will_not` states the same rule and stops it being checked.
The topology pageshows the same prohibition from the other side: the starter’s DOT file, where the planner -> builder edge is deliberately absent.
The whole vocabulary, and what names each term
The 54 curated terms the fields draw on, and every term a published blueprint declares in its own namespace, each with its parent and how many cards name it.