v1.26.0 ·
The flag meant “render this way”, not “this is ours”
The problem
A guidance layer reaches an agent because it is registered. The file on disk is inert; the entry in the guidance index is what puts a pointer into every emitted skill. Delete the file and you get a loud warning. Delete the registration and you get silence, which is the more expensive of the two.
aidlc doctor had a check for registrations belonging to a compliance pack that is no longer
installed. Dropping those is right: an uninstalled pack should stop feeding its constraints to every
agent. The question is how doctor decided an entry belonged to a pack, and the answer was a flag
called always_pointer.
That flag is a rendering instruction. It tells the skill compiler to emit a pointer to the layer file rather than pasting its text inline, no matter how short the layer is. Pack layers are required to set it, because inlining a pack's control text into a generated skill is forbidden outright. So every pack layer carries the flag — and doctor read that the other way round, as though every layer carrying the flag were a pack layer.
A project is free to set it too. That is the supported way to say "this guidance is long, link to it, don't paste it into everything". Any project that did was told its own layer belonged to a pack that was not installed:
$ cat .aidlc/guidance/index.yaml
layers:
- name: house-rules
file: house-rules.md
always_pointer: true
$ aidlc doctor
pending compliance-layer-unowned (non-interactive): would drop 1 guidance
registration(s) belonging to no installed pack: house-rules — the files stay on disk
$ echo $?
1
The comment sitting directly above the filter stated the contract the filter broke: a framework layer, or one the project authored, is never in scope here.
Now follow where that leaves someone. No pack will ever claim a file the project wrote, so the check can never clear on its own. Doctor exits 1 on every run, forever, and in continuous integration that is a red build with one obvious remedy printed underneath it. Running that remedy drops the registration. The message reassures you that the files stay on disk, which is true and beside the point — the layer stops being delivered at the next update, and nothing says so. The only action that turns the light green is the one that breaks the thing.
That shape is worth naming on its own, separately from the mistaken filter. A check that cannot clear except by applying its own fix has stopped being a check. It is a prompt, and the pressure it applies runs in whatever direction its remedy happens to point.
Underneath sat a second defect, and it was the worse one. Reading the compliance config deliberately degrades a broken section to "no packs installed", so that a hand-mangled config cannot break unrelated commands. That is the right call for every consumer asking what governs this project, and exactly the wrong one for a consumer that acts on a record's absence. A pack record missing a single required key fails validation and disappears without a word — so doctor concluded the pack was gone and deregistered a layer belonging to a pack that was still installed, with its file still sitting there. Recording provenance does not fix that path, because the ownership set is read from the same degraded config. It needed something the codebase did not have: a way to tell "no section" from "a section I could not read".
How it could be solved
The tempting fix is to make the inference smarter. Keep reading always_pointer, but add a second
condition — the file name looks like a pack's, or it sits in a list of known pack layers, or the layer
body carries a pack's disclaimer sentence. Each of those is a better guess than the original. None of
them stops being a guess, and the thing being guessed at is knowable: exactly one piece of code ever
knows which pack a registration came from, and that is the installer that wrote it.
So the choice was between inferring provenance more cleverly and recording it. Recording it means the installer stamps the pack id on every entry it creates, and the check filters on that key instead. An entry with no key is the project's or the framework's, and out of scope. Two keys where there was one, and they stop meaning the same thing: one says how to render the layer, the other says who put it there.
That leaves the awkward half, which is every project that installed a pack before the key existed. Their entries carry no provenance and never will unless something writes it. The obvious move is a migration that stamps whatever it can work out — and "whatever it can work out" is where the original defect lives, so the migration had to be narrower than that. It stamps an entry only when an installed pack's own record names that file. That is proof, not inference. Everything else it leaves exactly as it found it, key for key.
Which means accepting a gap, deliberately. A registration left behind by an old install whose config record was already deleted has no provenance and nothing that can establish any. Neither migration will ever touch it. That stale pointer stays.
The reason to accept it is that the two errors do not cost the same. Wrongly dropping a registration stops delivering guidance an agent was instructed to always read, and nothing announces it — you find out when an agent does something the guidance existed to prevent. Wrongly keeping one leaves a stale pointer in a file a person can open, in a check that names it. One failure is silent and its damage compounds; the other is visible and costs a line edit. When the evidence runs out, take the reversible error.
The same asymmetry settled the config question. The unreadable-section path could have been fixed by making the config reader strict — throw on a malformed section instead of degrading to empty. That would have closed the hole and broken the property the degradation exists for: one project's mangled config must not take down commands that have nothing to do with compliance. So the reader keeps degrading, and a separate function answers the narrower question of whether the section is absent, readable, or damaged. Only the two migrations that act on a record's absence consult it, and they decline rather than proceed on a config nobody could parse.
One suggestion from the original report was turned down. It proposed dropping the check below the tier that fails the build, on the grounds that a leftover registration is cosmetic. Before the fix that was arguable, because most of what the check caught was not a pack registration at all. After it, every entry it catches genuinely claims a pack that is not installed — which is precisely the state where an uninstalled pack's constraints keep reaching every agent. Fixing the predicate removes the reason to soften the verdict. It keeps its tier.
How AIDLC solves it
aidlc doctor no longer deregisters guidance layers the project wrote itself. The
compliance-layer-unowned migration used to identify compliance-pack registrations by
filtering .aidlc/guidance/index.yaml on always_pointer === true, which inverts the real
invariant. always_pointer is a rendering instruction — the skill compiler checks it before
the inline-size comparison to force pointer delivery whatever a layer's length — and pack
layers are required to set it, so pack implies the flag. The check assumed the reverse. Any
layer a project registered with that flag, which is the supported way to ask for a long
layer to be linked rather than pasted into every skill, was read as a registration
belonging to an uninstalled pack.
The consequence was not cosmetic and it ran one way. The check reported the entry on every
run and could never clear, because no pack will ever claim a file the project authored, so
aidlc doctor exited 1 forever. The only action that silenced it, aidlc doctor --yes,
dropped the registration. The layer file stayed on disk, which is what the old message
reassured the reader about, but the registration is what makes a layer reach an agent: the
loader builds its layer list from the index, so the next aidlc update emitted skills with
the pointer gone and the layer silently stopped being delivered. The pressure ran toward the
damaging action.
Provenance is now recorded rather than inferred. installPack and the re-register path both
stamp pack: <id> on entries they create, and a single unownedRegistrations() helper —
called by both the check and the apply, so the two halves cannot report one set and act on
another — selects only entries carrying that key whose file no installed record owns. An
entry with no pack key is the project's or the framework's and is out of scope. A new
safe-tier compliance-pack-provenance migration backfills existing installs, stamping only
what an installed record proves and leaving every other entry byte-for-byte as it found it.
Where provenance cannot be proven the entry defaults to project-authored, because the two
errors cost very different amounts: wrongly dropping a registration stops delivering
guidance an agent was instructed to always read and nothing announces it, while wrongly
keeping one leaves a stale pointer that is visible in the index and cheap to remove.
A second defect in the same seam shipped fixed alongside it, and it was the worse of the
two. readComplianceConfig deliberately degrades an unparseable compliance: section to
"no packs installed" so a hand-mangled config cannot break unrelated commands. That is right
for every consumer asking what governs the project and dangerous for one that acts on a
record's absence. A pack record with, say, no version key fails to narrow and is dropped
silently, so doctor --yes deregistered a live pack's layer while the pack was still
installed and its file still on disk. Recording provenance does not close that path, because
the ownership set comes from the same degraded read. A new complianceSectionState()
distinguishes absent from readable from broken, and both provenance-dependent migrations
decline to run on a broken section rather than concluding a pack was uninstalled. This was
reproduced against the shipped v1.24.1 build, not argued from the source.
Two smaller things travelled with the fix. The pending description now names the pack each
candidate entry claims and states that the layer stops reaching agents, instead of only
reassuring that files stay on disk. And the drop check keeps its confirm tier and its
exit code, deliberately against the suggestion in the original report: once provenance is
explicit, a leftover pack pointer is not cosmetic, it keeps feeding an uninstalled pack's
constraints to every agent, which is the state the registration drop exists to prevent.
One criterion was retired mid-implementation. AC-12 claimed that ordering the backfill ahead of the drop check lets one run stamp provenance and then act on it. Writing its test proved that impossible: the backfill stamps entries an installed record owns, the drop check selects entries no installed record owns, so the two predicates are complementary and no ordering produces a deregistration. Amendment 1 retired it in favour of AC-20, which asserts the invariant directly, and AC-21, which records what AC-12 was reaching for and getting wrong — a stale pointer from a pre-provenance install whose record is already gone is genuinely undetectable, and that is an accepted false negative under the asymmetric-cost rule rather than something registry order can rescue. The comment in the migration registry that told the false story now says the opposite.