Generated from
cat-harness/skills/kg/kg-core/index-config.md— do not edit here.
index.config.json: what this checkout instantiates
The owner, 2026-10-07: “migration to index.config.json
importing <harness>.config.json information as needed”, and later that day “go ahead
and start the migration NOW”. The schema is folio-index-config/v1 in
cat-harness/schemas/index-config.ts, and the proposal with the full
argument is cat-harness/docs/proposals/index-config.md.
The index is the ONE config. Owner, bean ymvt, restated 2026-10-14:
“i thought ONLY index.config.json”. Each instance’s config is written inline
in its instances[] entry. A <name>.config.json is a legacy file: imported
only as a migration step, then retired with index-config:migrate --inline
--write. Never create one — not to make a tool find a recipe, not for a
new instance. If a reader only finds config in the file, that reader is the
bug; fix it to read the index entry first.
A checkout has one <base>/index.html. Before this file, three facts about
how a checkout is instantiated sat in N+1 places:
- which harnesses are instantiated: every root
*.config.json, re-scanned by five scripts; - which one is
/: asite.landingflag that could be in any of those files; - where each one comes from:
remoteMountson the root declaration.
index.config.json holds all three. When it exists at the root it is
authoritative. When it is absent, every reader falls back to the old
behaviour, unchanged.
{
"$schema": "folio-index-config/v1",
"instances": [
{ "name": "folio-assistant", "source": { "local": { "at": "." } } },
{ "name": "cat-harness" },
{ "name": "my-ig", "source": { "remote": { "repository": "owner/my-ig", "ref": "<40-char sha>",
"overrides": { "my-ig": { "whole": true } }, "trust": { "consent": { "...": "..." } } } } },
{ "name": "my-guide", "contentType": "document", "site": { "build": { "sourceSteps": ["site:document"] } } }
],
"site": { "landing": "cat-harness" }
}
Instantiated is a different fact from declared
only the instiatiated harnesses (not all dependent ones) in teh folio
A declaration (<name>.json) says what an instance declares. It travels
with the harness into every checkout that uses the harness. An
index.config.json entry says the instance is instantiated in this
checkout. Without an index, a <name>.config.json at the root says the
same thing. Tiles, the landing and the hub listing are all drawn from the
instantiated set, which instantiatedHarnessNames(root) returns. Never
derive that set from declarations or from a directory listing.
The dependencies of an instantiated harness are not instantiated just because a mount’s closure brought them in. They keep a group of their own rather than disappearing.
A harness that a KG subscription chose is instantiated the same way:
kg:instantiate adds its entry, with its config inline, when the checkout
has an index (and only without one writes a root <name>.config.json).
Issue #1719.
Inline, over a legacy import while one remains
Each entry’s effective config is built like this:
- Start from a legacy imported file, while one remains. That is
importwhen it is given, else<name>.config.json, and only if the file exists. The target state is that no such file exists. - Overlay every other key of the entry (everything except
name,import,sourceand_comment) as an inlineHarnessConfigfield. Objects merge key by key, and arrays and scalars are replaced. Inline fields win.
effectiveInstanceConfig(root, name) computes it. readHarnessConfig(dir)
returns it for any instance that an index lists, walking outward from the
instance root and stopping at the first directory that holds either the index
or the instance’s own config. An instance the index does not list is read
exactly as before. Readers that are handed a root and a name (the site
recipe in build-instance-site.ts) use readInstanceConfig in
cat-harness-tools/scripts/instance-config.ts, which reads the same way and
REPORTS every legacy file it reads.
Retiring the files is index-config:migrate --inline --write: each
imported file is folded into its entry by exactly the overlay above, so no
effective config changes, and the file is deleted.
index-config:inline:check fails while one remains.
Local or remote
sourceabsent: local, at<name>/.source.local.at: local, at that path. Use.for the instance declared at the root.source.remote: a remote mount. It is aremoteMountsentry minusharness, which is the entry’sname. It reusesRemoteMountSchemainstead of restating it, so it hasref(notpin),overrides.<name>.wholeand.path,noteandtrust.consent, and every field a later change adds there (such astrack) arrives with no edit here.
There is one read path and one write path for remote mounts:
- Read:
readDeclaredMounts(instanceRoot)returns the index’s remote entries when an index exists, else the declaration’sremoteMounts. If both carry mounts it throws and names both files. It never picks one. - Write:
writeDeclaredMounts(instanceRoot, mounts)always writesindex.config.json, seeding the index from the checkout first if there is none. It refuses while the declaration still carriesremoteMounts. Nothing writesremoteMountson a declaration any more.
Which harness / is
resolveLandingInstance(root) is the only reader. It applies these rules in
order:
| condition | / is |
|---|---|
| the index is unreadable | invalid: the gate fails. There is no fallback to the flags, because the index was written to override them |
the index’s site.landing names an instance it lists | that instance (by: "index") |
the index’s site.landing is "hub" | the neutral hub: the harness listing and the todo panel |
the index’s site.landing names an instance it does not list | invalid: the gate fails, and it is never a silent fallback |
no site.landing (or no index), one instance | that instance (sole) |
no site.landing, several instances, exactly one config flags site.landing: true | the flagged one |
| several flagged | the hub |
| none flagged | ambiguous: check:landing-instance fails, because the answer is never guessed |
| nothing instantiated | none: a state of its own, not a default |
The owner’s 2026-10-02 ruling still holds where no index exists: “If exactly
one harness is instantiated, it is the landing page and no flag is needed …
If there are several and none is flagged, a gate fails. If more than one is
flagged, then neutral hub.” The index makes the choice explicit, in one
place. gen-landing-data.ts and sync-docs-harness.ts ask this resolver.
They do not use their own location.
check:landing-instance also checks that the index and the root
*.config.json files agree:
- A root config that the index does not import fails the check. Under the old rule that config instantiates a harness and under the index it does not, and nothing should pick between them silently.
- An
importthat names a missing file also fails the check. - An entry with no config file at all is the target state, and passes.
- A legacy file an entry still imports is reported every time. It passes while it is a pending migration (every key it holds is either not inline or inline with the same value). It fails when the entry overrides one of its keys with a different value: the file’s value is then dead text that a reader of the file alone takes for the config.
A harnessed repository runs its checks on its own pull requests
Instantiating a harness in a repository includes turning its checks on there. Owner, 2026-10-14: “There should be CI running”, through one reusable workflow the harness owns, never a copy per repository. Before this ruling, seven layers were judged by nothing until an index re-pinned them: the 2026-10-06 “defined, not run on events” ruling for the WHO forks switched off their harness gates along with their IG builds, and no initialization step turned the gates back on.
The workflow is the tools layer’s .github/workflows/layer-pr-checks.yml. A
repository calls it from a workflow of about fifteen lines that runs on
pull_request:
jobs:
checks:
uses: <owner>/<tools-layer>/.github/workflows/layer-pr-checks.yml@main
with:
mode: layer # or `index`: the repository is its own index
instance: <instance> # layer mode: its directory in the folio-assistant index
path-in-repo: <instance> # where the instance sits in the repository ('.' at the root)
layermode is for a repository that an index mounts, such as cat-harness or the DAK layer. The workflow checks out folio-assistant and puts the PR in place of the locked mount.indexmode is for a repository that is its own index: an IG repository over the DAK layer, smart-ra, or a folio. Its ownindex.lock.jsonis replayed.- The gates are the harness’s graph checks over the repository’s own
instance, plus every
check:*/*:checkscript of that instance and its whole dependency harness (its transitiveneeds). Each is named<owning-instance>:<script>. Owner, 2026-10-14: “A harnessed repo should run not just core but all of the checks in the dependency harness.” - A PR fails only on what it newly breaks. Each gate and test runs on main and on the PR. Anything already red on main is listed, never hidden, and never counted against the PR.
A layer PR that declares a new directory is red on two index gates it
cannot clear. The index README lists every declared directory, so the PR
leaves index:render:check and readme:sync:root:check stale, and only the
index re-pin that takes the PR regenerates the README. Read those two as
expected when they are the only new failures, and run them green in the
index re-pin instead (2026-10-23, a layer PR that added an IG AST viewer).
Disabling a repository’s automatic builds is not disabling its checks.
When an IG repository’s automatic gh-pages or IG Publisher triggers are
switched off (the onboarding question in the FHIR IG layer’s ig-build-pipeline),
the harness caller stays on.
The exemptions of the index’s own workflows
.github/workflows/ belongs to the index, so the steps CI runs there that the
local gate set does not are exempted HERE, under gateExemptions.steps, each
with a kind and a reason (GateExemptionsSchema, the same shape an
instance declaration carries). They land in the same change as the workflow
step, never ahead of it in a layer: an exemption a layer declared for a step
still on an unmerged branch matched nothing CI ran on the next re-pin
(qa:publish --add, 2026-10-14), and gates-workflows.test.ts refuses a stale
exemption as it refuses an unclassified step.
"gateExemptions": {
"steps": [
{ "match": "bun run cat state:push -- --id health", "kind": "ci-only", "reason": "…" }
]
}
An instance still exempts its OWN scripts and steps in its own declaration.
index.lock.json, and the legacy name
The lock records what the declared mounts resolved to. It is generated by
mount:remote and kept as a separate file. It was
<root-instance>.mount-lock.json until 2026-10-07. The rules for each reader
and writer:
- Writers write
index.lock.json.mount:remotefirst renames a legacy lock onto the new name, so the old file is moved, never dropped. - Readers take
index.lock.jsonfirst, then fall back to the legacy*.mount-lock.json. Downstream folios and old branches keep replaying through that fallback. The readers are:mount-from-lock.tsmountedInstanceRootsmountedUnderingit-corpus.tsmount:remote:check
- Both files present is an error that names both. Two locks are never merged.
mount-from-lock.ts imports only node:*, so it restates the rule inline
and its test holds it to lockFilesIn.
The reserved index stem
index.config.json ends in .config.json, so a scan that treats “each root
*.config.json is a harness” would find a harness called index. The
index stem is therefore reserved across the board:
rootConfigStemsskips everyindex.*file. It is the one root scan, and the five scripts that re-implemented it now call it orinstantiatedHarnessNames.findDeclarationFileskips everyindex.*file too.- The index schema refuses an instance named
index, andhub, because that is the landing keyword.
Do not add a sixth copy of the scan. A new copy is how the reserved name leaks back in.
The generated .gitignore block
# BEGIN index mounts (generated from index.config.json — do not edit)
/my-ig/
# END index mounts
The block holds one /<path>/ line per remote instance in the index, where
the path is overrides.<name>.path, else <name>/. After those it lists
every further instance root the lock records, because a mount’s closure can
land instances that the index never names. Three things write the block:
mount:remotewriteDeclaredMountsindex-config:migrate --write
check:index-ignores fails when the block is missing or disagrees with the
index. When an index exists, mount:remote no longer appends to
.git/info/exclude. A local exclude rule is invisible to every other clone,
so a path the committed block does not cover is reported as not ignored.
Checkouts without an index keep the old behaviour.
Converting a checkout: index-config:migrate
bun run cat index-config:migrate # print the index it would write, and its findings
bun run cat index-config:migrate --write # write it; move remoteMounts; write the block; rename the lock
bun run cat index-config:migrate --check # exit 1 when --write would change anything (a gate here)
bun run cat index-config:migrate --inline --write # also fold each <name>.config.json into its entry, and delete it
bun run cat index-config:inline:check # exit 1 while any imported <name>.config.json remains
bun run cat index-config:migrate --root <clone> # a separated repository's standalone checkout
The converter reads only the files under --root, so it runs on a
standalone clone of any separated repository.
- Which root configs it imports: a root
<name>.config.jsonis imported only whennameis an instance the checkout declares (at the root or one level down) or mounts. Anything else becomes anunmatched-configfinding and is not imported. A repository cut from a fork, for example, can carry the fork’s own<fork>.config.json. Importing that file would instantiate a harness the repository does not hold. - Re-runs: it is idempotent. A re-run moves only the
remoteMountsentries that the declaration has gained since the last run. An entry the index already holds identically is dropped from the declaration. An entry the index holds differently throws, and you reconcile it by hand.
Processes that run this skill
| process | step(s) that name it |
|---|---|
| Remote-mount a dependency | Declare the mount: harness, repository, pin |
| Subscribe to an external knowledge graph | Instantiate the harness: write its config |