Charter Activation vs DRG Reachability
Origin. Written during the landing pass for PR #3007 (mission
doctrine-silence-guards-01KYFV7Q), in response to an operator question:
"I would expect directives, and paradigms especially, to be activated directly
from the charter — any element of the doctrine/DRG can be referenced there to be
activated. Which path is actually being followed?"
The short answer: the operator's expectation is the correct design intent, it is half-implemented, and the half that is missing is the half agents actually read.
Issue #3009 frames the problem as nine activated artefacts being DRG orphans. That framing is true but too narrow — and acting only on it would produce a fix that does not fix the symptom. This document records why.
1. What was measured
All figures re-derived on the PR #3007 tip rebased onto e97fc6ab9, this
repository, 2026-07-28.
.kittify/config.yaml activation counts:
| key | count |
|---|---|
activated_tactics |
110 |
activated_directives |
25 |
activated_procedures |
18 |
activated_styleguides |
12 |
activated_toolguides |
12 |
activated_paradigms |
8 |
| total | 185 |
What the action-context boundary renders — the surface an agent consumes at a step boundary, invoked with the correct grain:
$ spec-kitty charter context --action implement --mission-type software-dev --json
Governance:
- Template set: software-dev-default
- Paradigms: (none)
- Tools: git, mypy, pytest, ruff, spec-kitty
Directive IDs:
- DIR-001 … DIR-013
Tactic IDs:
- (none)
references_count : 0
directives (action-scoped) : 0
185 activated artefacts; zero of them appear. The thirteen DIR-0NN entries
are not the activated doctrine directives (DIRECTIVE_001, DIRECTIVE_035, …) —
they are bullet lines scraped from the charter prose, with titles like
**Cross-platform:** Linux, macOS, Windows 10+. The 25 activated doctrine
directives are absent, as are all 110 tactics and all 8 paradigms.
This is not the typeless-grain defect of #883: the run above passes
--mission-type software-dev explicitly and the result is identical to the
typeless run.
2. Why — three parallel activation vocabularies
Activation is not one mechanism. It is three, with different storage, different writers, and different readers.
| # | Vocabulary | Stored in | Written by | Read by |
|---|---|---|---|---|
| V1 | activated_<kind> |
.kittify/config.yaml |
charter activate / deactivate |
charter.resolver (as a filter), charter.compiler._resolve_config_activated_roots (as compile roots) |
| V2 | selected_<kind> |
.kittify/charter/interview/answers.yaml |
the charter interview | charter.compiler charter-body render (Selected Doctrine) |
| V3 | selected_<kind> |
.kittify/charter/charter.yaml |
charter compile | assorted consumers |
In this repository today:
- V1 holds 185 artefacts.
- V2 holds 110 tactics and 25 directives but
selected_paradigms: []. - V3 holds
selected_*: []for every kind — all eight lists empty.
The compact view that charter context --action renders takes its paradigm line
from the V2 interview answers (compiler.py:1282,
', '.join(interview.selected_paradigms) or '(none)') — which is empty — and its
tactic list from a tactic_ids argument that the action-context caller passes as
() (context.py:2814-2817, render_compact_view(..., tactic_ids=tactic_ids or ())).
So the (none) values are literally correct for the vocabulary being consulted.
The activated set is simply not the vocabulary that surface reads.
3. What activation does do today
Activation is not wholly inert, and it is important not to overstate this. Two paths honour V1 correctly:
- On-demand fetch works.
spec-kitty charter context --include tactic:acceptance-test-firstreturns the full artefact — purpose, steps, references. An agent that knows an id can always reach it. - Compile-time root seeding works.
charter.compiler._resolve_config_activated_rootsseeds tactics, styleguides, toolguides, procedures and agent profiles as direct roots, explicitly "so an artefact activated directly inconfig.activated_*resolves in the compiled set even when no selected directive's transitive closure reaches it" (compiler.py:70-77). That docstring is the design intent the operator described, stated in the code.
What is missing is the third path: the action-scoped context boundary does not consult V1 at all. Activation influences what is compiled and what can be fetched, but not what is offered at the moment an agent starts an action.
4. Why this reframes #3009
#3009's remedy 2 says each of the nine orphans needs "either an inbound edge from whatever should reach it, or an explicit 'reachable by direct activation only, by design' note."
Both branches are currently unsound as stated:
- The edge branch fixes traversal, not the symptom. Adding an inbound
requiresedge makes an artefact reachable by cascade and by DRG traversal from whatever it is attached to. It does not make it appear incharter context --action, because that surface renders from V2/V3, not from a DRG walk over V1. Wiring all nine and re-running the command yields the same(none). - The direct-activation branch presumes a capability that is absent.
"Reachable by direct activation only" is only an acceptable disposition if
direct activation actually surfaces the artefact somewhere an agent looks. Today
it surfaces only under
--include, which requires already knowing the id.
This is the mission's own thesis one level up the stack. doctrine-silence-guards
was built on the finding that "resolves" was operationalised as id-lookup rather
than reachability. Charter activation currently operationalises "activated" as
membership in a filter list rather than presence in the context an agent
receives — the same substitution, at the layer above the one the mission fixed.
5. Recommendation
Ordered by leverage, not by cost.
R1 — Make activation an entry vector into action context (the real fix)
The action-context builder should seed its directive/tactic/paradigm lists from
the V1 activated set, intersected with the action grain, rather than from the
V2 interview answers. Concretely: the directive_ids / tactic_ids the
action-context caller currently passes as () should be populated from
resolve_config_activated_roots, which already exists and already does the
resolution correctly.
This makes the operator's stated expectation true: anything referenced in the charter is activated, and activation means an agent sees it. It also makes the orphan question far less load-bearing, because direct activation becomes a first-class entry vector rather than a dead end.
Caveat to size before doing it: 110 activated tactics is far more than an
action boundary should emit verbatim. R1 needs a grain filter (by action, by
mission type) or a two-tier render (ids at the boundary, bodies on --include).
The compact view already has that shape — it emits ids, not bodies — so the
cost is mostly in choosing the grain, not in the plumbing.
R2 — Collapse the three vocabularies to one
V2 and V3 are both selected_* and both currently empty or partial, while V1 is
the one the CLI writes. Three vocabularies for one concept is the condition that
produced this defect; it will produce another. V1 (config.activated_*) is the
right survivor — it is what charter activate writes and what the compiler
already treats as authoritative roots. V2 should become an input to V1 at
interview time, not a parallel store consulted at render time.
This is an ADR-level change, not a fold. It should be decided before B2,
which migrates 400+ inline references: into authored edges and will otherwise
encode the ambiguity into the edge set.
R3 — Adopt #3009 remedy 1 now (cheap, and correct regardless of R1/R2)
Replace _EXPECTED_ORPHAN_COUNT: int with a named frozenset of intentional
orphans and assert set equality. A golden count cannot distinguish "32
known-acceptable orphans" from "32 unexamined orphans"; a named set makes each
one carry a justification and makes a new one name itself in the failure. This is
independent of the activation question and should land immediately.
R4 — Triage the nine, with the disposition vocabulary fixed
With R1 pending, the honest dispositions are:
- remove — the artefact should not ship;
- wire — it has a genuine structural relationship that is simply unauthored;
- direct-activation-only — legitimate, conditional on R1 landing; until then record it as a known gap rather than as an accepted design.
Operator rulings taken 2026-07-28:
| artefact | disposition |
|---|---|
toolguide:rtk-search-tooling |
remove — RTK will not be pushed to the userbase; it is difficult to set up correctly and can significantly affect test execution |
| the other eight | wire — oversights, not design |
R5 — Measure reachability, not incidence
#3009's third point stands and is the most important thing to do before B2. The pinned orphan metric counts incidence (nodes touching no edge, currently 30). The operationally meaningful numbers are worse and unpinned:
| measure | count |
|---|---|
| incident to no edge (currently pinned) | 30 |
| zero inbound edges — unreachable by any traversal | 80 |
unreachable from any action node |
144 of 311 (46%) |
Fifty nodes have outbound edges only: non-orphan by the pinned metric, unreachable in fact. B2 moves all three numbers at once; entering it with the wrong metric pinned is the trap.
6. What this means for PR #3007
Nothing in section 5 is a defect introduced by #3007 — every item predates it. The mission's own WP09 explicitly logged the incidence-vs-traversability gap and declined to change it mid-mission, which was the right call.
R3 and R4 are folded into the PR because they are small, bounded, and use the structures the mission just built. R1 and R2 are not — they change what an agent receives at every action boundary in every consumer project, which is a mission with its own spec and its own blast-radius analysis, not a landing fold.
They are, however, the difference between a doctrine layer that is declared and one that is in force.