The Harness
On this page
This page is generated from docs/source/concepts-harness/ — each section below links to its own source.
A Harness is the layer that makes a Knowledge Graph usable: it renders the graph, serves an interface over it, and supplies the Skills, Workflows and Tools an Agent works the graph with. The Knowledge Graph says what is true; the Harness is how anybody reaches it.
A checkout holds several. They stack, bottom to top — bootstrap, then
cat-harness, then the content core, then folio-assistant — and each
inherits what the ones below it declare. That stacking is the subject of this
page, along with the two things a Harness owes anything it takes charge of.
The key words MUST, MUST NOT, SHOULD and MAY are used here as RFC 2119 defines them, and only in upper case, per RFC 8174. Where a requirement below is not yet enforced by a gate, this page says so at the requirement rather than in a footnote: a conformance statement a reader cannot tell from a description of existing behaviour is worse than either.
Instantiation — a config file at a root
An instance becomes an instantiated Harness — as opposed to a dependency that merely sits in the tree — by carrying a config file at its root:
<stub>.config.json the instance is instantiated HERE, and how it is configured
<stub>.json what the instance DECLARES — its directories and assets
Two files, two questions, and since 2026-09-21 two filenames rather than two
files sharing one shape. Before the split both ended .config.json and were
told apart only by which directory they sat in and whether they carried a
name — two schemas and two readers behind one spelling, which is a
distinction a reader has to already know to make.
The presence of the config is the signal. It is what the left-hand navigation filters on to decide which instances get a tile, so a Harness that declares directories but carries no config is a dependency: its content is inherited by whoever depends on it, and it has no interface of its own. Nothing else is consulted for that question, which is why it could not be derived from the declarations alone.
A Harness MUST own the visualisation published at <baseurl>/<stub>. The
stem every published artefact is named with is computed in exactly one place —
artefactStub, which returns the declared stub when there is one and the
name otherwise — so the site build, the Knowledge Graph export and any future artefact
cannot disagree about what an instance is called.
The stub is intended to be free of the definition’s name. A Harness defined as
folio-assistantshould be instantiable asmy-folio-assistant.config.json, with the config pointing at the definition, and<baseurl>/my-folio-assistantthen live. That is not what the code does today — see the last section. It is stated here because it is the model the rest of this page assumes, and because a reader who tries it will be stopped by a gate rather than by a schema.
How the dependency tree is walked
Bottom-up, from the bootstrap
The stack is walked from the lowest instance up, and each layer overlays what the ones beneath it declared. A Skill, a Voice or a directory contributed low is visible everywhere above it; one contributed high is visible only there. Where two layers contribute the same name, the root wins — it is resolved last precisely so that it can.
The order is declared, not computed, in each instance’s needs array:
{ "name": "<content-core>", "needs": ["bootstrap", "cat-harness"] }
It is declared for a measured reason. The layering was real before it was written down, but it lived only in prose, so every consumer that needed the order had to already know it — and the first one that did would have hardcoded four names, which is a rule true only for the instances somebody remembered.
needs is OPTIONAL, and an absent value means undetermined rather than
needs nothing. The two are genuinely different: [] is an instance asserting
it sits on nothing, which is true of exactly one instance here, while absent is
nobody having said. A consumer treating absent as [] would place every
unlabelled instance on the floor beside the bootstrap. Names are used rather
than paths, because an instance is identified by its name everywhere else and
a path breaks the moment a directory moves.
Three relations, and they are not interchangeable
Between repositories — as opposed to between layers in one checkout — the Knowledge Graph page sets out three relations, and a fourth, associated, needs no order at all. Each has its own carrier here:
| relation | runs | carried by |
|---|---|---|
| depends | Content → Content | needs — the layer stack, foundation first |
| references | Tool → Content, Test → Content | remoteGraphs — a graph this instance knows about and does not hold |
| utilizes | Tool → Tool, Test → Tool, App → Content | dependencies in the config — what this instance USES, overlaid |
| associated | Content ↔ Content, any harness | associatedHarnesses — a harness this one knows, and where it is published; never loaded or materialized (issue #1146, skill associate-harness) |
Only depends fixes an order, so only depends can have a cycle in it,
and a cycle there is a defect rather than a shape. A Tool repository
references a Content repository without sitting above or below it; a consumer
application utilizes a Content repository it may depend on not at all. That
is why a consumer needs all three and collapsing them loses the distinction:
“what must be built first”, “what do I know exists”, and “what do I read at run
time” are three questions with three answers.
The bootstrap is the exception, and the floor rises
bootstrap sits at the bottom and does not render. It is the navigation
footer; what it owes instead is its own .json and .jsonld — “that is its
existence”. The rendering obligation is a floor that rises: it starts at
cat-harness, which is obliged because it supplies the layers above it with
folio/. An instance below that floor declares its exemption in
renderExemption rather than being silently excused, so the exemption is a
statement somebody made and not a gap somebody left.
What harnessing a directory obliges
A Harness does not only hold directories. It harnesses them — and a datastore such as a git repository is harnessed the same way, through Tools: the Harness names the Skills that reach the store, and a Tool supplies the mechanism. That indirection is what lets the same Harness be driven against a different store without rewriting a Skill.
Declaring a directory is a promise. It says this instance holds a graph of
that kind and a consumer may scan it, and the obligations below are what make
the promise keepable. They are stated in full by
harness-requirements
and measured by check:subgraph-coverage; this section says what they are and
why two of them are ranked differently.
| obligation | declared as | the question it answers |
|---|---|---|
| serialisations | coverage.serialisations | is each node addressable as json, jsonld and schema.json? |
| visualiser | coverage.visualiser | can a person LOOK at this? |
| docs | coverage.docs | can a person READ ABOUT this? |
| skill | coverage.skill | is an agent handed something that GOVERNS this? |
The first four are per declared directory. A fifth, a starting README, is per instance and is ranked harder still: an instance without one is not a gap somebody has not filled, it is an instance a reader cannot enter.
coverage.process is DECLARED and is not an obligation
A directory may also name the BPMN process that governs it —
coverage.process, a diagram’s basename such as document-ingestion. The
generated subgraph README then draws that diagram, links its .bpmn source and
lists the subprocesses it calls, so the page a reader lands on says how the
directory is worked rather than only what is in it.
It sits apart from the four above in two ways.
It is not owed. Most directories are not the subject of a process, so an
absent coverage.process means NOT DECLARED and never “no process” — the same
third state readOnly keeps — and a directory declaring none gets no such
section and is not a finding. What is a finding, ranked major, is a declared
name that resolves to no diagram: somebody said, and what they said points at
nothing. There is correspondingly no exempt.process; declaring nothing
already is the silence, and a waiver would be a second spelling of it.
It is declared rather than inferred, and the temptation is the point. document-ingestion.bpmn starts on an event named “A file lands in
uploads/“, so the directory could be matched out of that prose. An event’s
name is editorial text: rewording it would silently unlink the page, and
nothing could tell that from a directory that never had a process. A diagram
also touches several directories while being about one — document-ingestion
reads uploads/ and writes library/ — which is the direction name-matching
cannot see at all.
The serialisation is the one obligation a Harness MAY NOT waive
A Harness MUST serve json, jsonld and schema.json at a harnessed
directory’s own URL and at every node beneath it, and MUST NOT be excused
it. coverage.exempt carries three keys — visualiser, docs, skill — and
deliberately not a fourth, and the coverage check ranks a missing serialisation
as an unmet obligation with no by-kind test at all.
The reason is bootstrap. It is excused a visualiser precisely because its
json and jsonld are its existence — so the thing it is excused into
cannot itself be excusable, or the exemption would excuse everything. The
visualiser is the courtesy; the serialisation is the existence claim.
A Harness SHOULD provide a visualisation, and which kinds owe one is a
question about the kind rather than a blanket rule: an active state directory
in the repository root — beans/, todos/, fsh-guts/ — owes one because a
Harness renders something for each state directory it initialises, while a kind
that was never going to need one is not carrying a debt.
That difference is why the coverage axis ranks findings major and minor
rather than counting them: major is an obligation this kind owes and the
instance has not met; minor is nobody having said yet, for a kind that never
owed one. Merging them would rank beans/ having no viewer alongside a kind
that never needed one, and an axis that cannot tell those apart is an axis
whose count means nothing.
A Harness SHOULD also carry a docs/ directory documenting itself, and
every Harness SHOULD have one. cat-harness renders its own through the
plain documentation path — no extensions, no JavaScript where it can be avoided
— which is why it is the one layer that owns a renderable kind at all: a
layer owns the kinds it can render.
Three things the model asks for and the code does not do
Two parts of the model above are intended and not implemented. They are listed together, with what a reader will actually hit, because a page that described them as working would send somebody to look for code that is not there.
1. There is no HarnessKind
A Harness is intended to be declared as a subtype of a HarnessKind — the
name is itself unsettled. Nothing in the schemas registers one today: an
instance declares a name, directories, assets and needs, and what makes it
a Harness rather than a dependency is the presence of a config file, which is a
fact about the filesystem rather than a declared type.
The consequence is not cosmetic. Without a kind there is nowhere to say what a Harness of a given sort MUST supply, so every obligation on this page is prose rather than something a declaration can be validated against.
2. The stub is NOT free of the definition’s name
The model says a Harness defined as folio-assistant MAY be instantiated
as my-folio-assistant.config.json, with the config pointing at the
definition. Three places refuse it, and they refuse it consistently:
instanceConfigFilenamecomposes the filename from the declared name, so the stub is derived rather than chosen.- The left-hand navigation tests for
<declared name>.config.jsonwhen deciding whether an instance is instantiated — a differently-named config would leave the instance with no tile. check:instance-configreports a config whose filename does not match its instance’s name as a finding, reading it as a rename half-done.
And the config carries no field pointing at a definition, so even were the filename freed there is nothing for it to point with. Freeing the stub means adding that pointer first; the filename rule is the symptom.
A third claim was here, and it was wrong
This section listed the JSON-LD serialisation as a MUST carried by nothing.
It is carried by coverage.serialisations and checked by
check:subgraph-coverage — and it is the strictest obligation in the set,
the only one coverage.exempt refuses to waive. The claim was made by reading
coverage as three fields when it has four, which is the failure this page
warns about one section earlier: treating an overlapping set as the same set.
It is corrected rather than quietly deleted because a reader who saw the first version would go looking for a gap that is not there, and because “stated as a MUST, carried by nothing” is the specific kind of error a page written in RFC 2119 key words is most able to cause.
Neither remaining item blocks the other. Instantiation works, the dependency walk works, and all four coverage obligations are carried and checked. What is missing is the part that would let a Harness be validated as a Harness rather than recognised as one.
Where the rules live
As with every page here: the discipline is in the Skills, and where a Skill and this page disagree, the Skill wins and this page is wrong.
| question | where it is answered |
|---|---|
| What the Knowledge Graph is, and which way its references run | The Knowledge Graph |
| How an instance declares its directories, and every graph typology | directory-conventions |
| What a visualiser owes a declared directory | Subgraph viewers |
| What a Skill states and what a Tool supplies | skills-and-tools |
| Why a tile belongs to the Harness rather than to a node | harness-tiles |
| Context Overlays, and how an Agent’s conditions are set | Managing agent context |
The declaration schema, including needs, remoteGraphs, renderExemption
and coverage, is
schemas/cat-harness.ts;
the config schema, including dependencies, is
schemas/harness-config.ts.