Phase 0 Research — Doctrine Governance Fidelity

Source: pre-planning adversarial squad (planner-priti, architect-alphonso, paula-patterns, debugger-debbie), profile-loaded, read-only against upstream/main tip aac0635b5. All findings are convergent unless noted.

Lane B (#2156 + #2166) — the org_dirs omission is a duplicated pattern, not one site

AgentProfileRepository accepts built_in_dir / org_dirs / project_dir (src/doctrine/agent_profiles/repository.py:222). Only one construction site passes org_dirs. Census (org-awareness):

SiteLayer passedorg_dirs?Disposition
doctrine/service.py:141 (DoctrineService.agent_profiles).kittify/doctrine/agent_profiles✅ canonicalreference seam
specify_cli/invocation/registry.py:24 (ProfileRegistry).kittify/profilesFR-004 (IC-03)
specify_cli/tool_surface/profiles/projection.py:78.kittify/agent_profiles#2166 / FR-006 (IC-04)
charter/context.py:1602 (_DEFAULT_AGENT_PROFILE_REPO)built-in only (cache)FR-005 (IC-03) — governance-context leg
runtime/next/runtime_bridge.py:2369.kittify/doctrine (wrong subdir)out of scope (note in friction trace)
cli/commands/agent/tasks.py:281,3209built-in onlyintentional (language resolution) — exclude (C-003)
cli/commands/profiles_cmd.py:85.kittify/profiles + hand-rolled org overlaypartialduplication smell; collapse onto IC-02 if low-risk

<pack>/agent_profiles/. IC-02 wraps it once; IC-03/IC-04 consume it.

(invocation) vs .kittify/doctrine/agent_profiles (doctrine). Add org_dirs; do NOT reroute ProfileRegistry through DoctrineService (would change which project profiles dispatch sees). → C-002.

profile, charter/specify catalog = 19, dispatch catalog = 18 → org profile unroutable. On-disk: .kittify/config.yaml doctrine.org.packs[].local_path, profiles at <pack>/agent_profiles/*.agent.yaml. → NFR-002 requires live proof.

  • Canonical seam exists: resolve_org_roots(repo_root) (drg/org_pack_config.py:263)
  • Two distinct project layers (alphonso + paula, binding): .kittify/profiles
  • Live divergence (debbie, dynamic): with an explicitly activated net-new org
  • #2166 confirmed = the projection leg (projection.py:78), same root cause, P1.

Lane A (#2153) — single read-but-dropped field, distinct from #1416

documentation_policy (if docs:) but emits a hardcoded string — value dropped.

the sole read-but-dropped one → not a class; one-line fix. Resisted promoting to a "field-interpolation framework" (over-consolidation bias flagged + rejected).

directives.yaml emitted here; governance context reads doctrine, not compiled prose. documentation_policy is also rendered verbatim into user-project-profile.md:799 (so not globally lost — only dropped in the directive).

(key-drift), never compiler.py. #2153 is distinct & unaddressed.

  • src/charter/compiler.py:937-939 interpolates risk_boundaries; :942-944 reads
  • Field census (paula): 13 interview fields interpolate; documentation_policy is
  • Single sink: directive output flows only to charter.md Project Directives — no
  • Prior art #1416 (CLOSED, PR #1419): touched only charter/synthesizer/*
  • Live repro (debbie): sentinels → SENTINEL_RISK present, SENTINEL_DOCS absent.

Lane C (#2082) — hidden depth: adjudicator is test-local, must be promoted

ReplaceableBuiltinsPolicy, POLICY_RELPATH, load_replaceable_builtins (4 symbols).

find_overridden_builtin_urns, UnsanctionedOverride do not exist in production — they live inside tests/architectural/test_builtin_override_policy.py (~line 79). → FR-009 (IC-06) must PROMOTE them before wiring.

loads the merged 3-layer DRG and repo_root; slot diagnostics into the org-packs-present branch after _collect_doctrine_collisions, guarded by the existing no-packs short-circuit → no new DRG plumbing (C-006).

(test_no_dead_symbols.py:714-717); module in _CATEGORY_7_GRANDFATHERED_ORPHANS (test_no_dead_modules.py:369); _baselines.yaml:70 category_7_grandfathered_orphans: 7 → 6 after wiring. No src/ runtime caller today (grep clean; merge.py hits are doc-comments; model_task_routing OverridePolicy is unrelated).

Project-tier overrides stay ungoverned (trusted operator tier) → FR-012.

  • src/doctrine/drg/override_policy.py __all__ = ReplaceableBuiltin,
  • Critical (alphonso + debbie + priti): find_unsanctioned_overrides,
  • Consumer seam already half-built: doctor doctrine (_doctrine_collect) already
  • Allowlists (debbie, exact): all 4 symbols in _CATEGORY_C_BUILTIN_OVERRIDE_POLICY
  • Not dead code — a dormant governance gate (test-consumed). Wire, never delete.

Brownfield checks (post-planning)

#2059 (doctor.py god-module) referenced-by-checklist only — FR-011 delivers one #2049 burn-down item; IC-07 prefers extraction over insertion (partial #2059 credit), no full de-godding. No other open issue is domain-matched.

dispatch see different catalogs) — IC-02 consolidates to one resolver + FR-008 gate.

context.py module cache + agent/tasks.py` built-in-only sites are intentional.

  • Foldable issues: #2166 folded (Lane B leg 3). #2049 (ratchet-allowlist shrink) &
  • Split-brain / dual-authority: the org_dirs omission IS a split-brain (specify vs
  • Deprecation check: none of the touched surfaces are slated for removal; `charter/
  • Sizing: undersized ~2–3× vs the naive "3 surgical fixes" framing → 8 ICs, 3 lanes.

Architectural-alignment squad (charter-as-runtime-entry-point) — UNANIMOUS, live-proven

Lenses: architect-alphonso, doctrine-daphne, debugger-debbie (profile-loaded). Verdict: the original Lane B plan BYPASSED charter as the entry point → revised.

filters the merged set by PackContext.activated_agent_profiles (three-state: None→all / explicit set→only those / frozenset()→none). It sits two layers above resolve_org_roots. Canonical gated chain: build_activation_aware_doctrine_service(repo_root) (doctrine_service_factory.py:38) → inner DoctrineService(org_roots=resolve_org_roots(...)) (service.py:29, activation-BLIND) → charter.resolver.DoctrineService(inner, PackContext.from_config(repo_root)).agent_profiles.

would surface declared-but-de-activated org profiles to dispatch/projection.

  • The gate lives in charter/resolver.py:121-130DoctrineService.agent_profiles
  • Original IC-02 ("thin wrapper over resolve_org_roots") was below the gate
  • debbie's live 4-surface × 4-state matrix (org probe, 18-builtin universe):
Config stateactivation-aware S1raw DoctrineService S2raw repo+org_dirs S3dispatch S4
key absent (default)19191918 (broken)
explicit list INCLUDES1191918
explicit list EXCLUDEShidden19 ❌19 ❌18
CLI-activated17191918

Row 3 = the bypass: raw paths show a profile the charter EXPLICITLY de-activated.

"install→visible" holds with no activation for the common case); first charter activate materialises a ~16-entry list, turning the gate on thereafter.

raw==filtered passes a bypassing impl → added the negative regime (row 3).

the bypass → revised to assert the activation-aware seam is used (C-008).

sole notion of "active" is charter activation. Correct design = keep each consumer's project repo, merge the org-provenance activated subset onto it (C-008). Precedent: charter/context.py:1300-1333 already gates charter context --include agent-profile:<id> (FR-016/#1636) — Lane B raw would have created the opposite split-brain.

<pack>/agent_profiles/ (resolve_org_roots); the activation subsystem reads <pack>/doctrine/<plural>/org/ (_layer_roots.py:24-26 doctrine-dir gate + pack_manager._scan_layer_dirs:566-567). charter activate agent-profile <id> FAILS "Unknown agent-profile ID" against a runtime-flat pack. Operator decision 2026-06-27: <pack>/agent_profiles/ (flat) is canonical; fix the activation subsystem to match runtime. No existing tracker issue — to be filed under #1799.

built-in id could shadow it at dispatch with no activation/sanction check; routing through the activation filter closes the activation half; override-sanction stays a doctor doctrine diagnostic (Lane C) — noted for a later mission.

  • Gate is opt-in/off-by-default: None (key absent) → all admitted (so #2156
  • Planned NFR-002 proof was FAKEABLE — activated-only assertion (rows 2/4) where
  • Planned FR-008 gate was INVERTED — "assert site passes org_dirs" certifies
  • C-002 reconciliation: C-002 governs the PROJECT overlay only; the ORG overlay's
  • Layout split-brain (debbie finding 4 → folded as FR-013/IC-09): runtime reads
  • Override-shadow (daphne, medium, OUT of Lane B): a raw org profile reusing a

Post-tasks anti-laziness squad (reviewer-renata, debugger-debbie, python-pedro) — remediated

All cited file:line claims verified accurate (debbie — no substantive drift; WP01 RED proven live). Binding remediations folded into the WP prompts:

  • [HIGH, renata+pedro] WP02→WP04 contract gap: AgentProfile carries no provenance; source_layer/source_path live on the repository. WP02 return contract changed to list[ResolvedOrgProfile] (provenance+source_path), recovered from the activation-aware inner repo — otherwise WP04's #2166 source_layer="org" is unsatisfiable (repo fallback → "builtin").
  • [HIGH, pedro] WP06 blast radius: hard cutover to flat layout would RED the non-owned tests/charter/test_pack_manager_catalog.py (≥6 nested fixtures). Resolution: layout-tolerant resolver (flat preferred, nested fallback) as the DEFAULT.
  • [MED-HIGH, renata] WP03 fakeable context: assert context != "" passes on a built-in fallback → assert an org-doctrine sentinel; drive the dispatch --profile path (_default_agent_profile_repository :1593), not the already-gated charter context --include (:363).
  • [MED, renata] WP05 gate teeth: binding assertion = absence of raw org_dirs/resolve_org_roots at named surfaces; "references seam" is advisory; teeth fixture imports-seam-yet-bypasses.
  • [MED, renata] WP09 self-attestation: T027 requires real CI integration-tests-core-misc evidence, not an Activity-Log note.
  • WP08 ownership false-alarm RESOLVED (debbie+pedro): route findings through the existing org_drg["errors"] channel in owned _doctrine_collect.py; healthy already keys off it — no _doctrine_health.py edit. Added: extract _adjudicate_org_overrides helper (complexity ≤15) + isinstance narrowing.

Open questions for /spec-kitty.tasks

1. Final module placement for the IC-02 resolve_activated_org_profiles resolver (avoid import cycle; reuse build_activation_aware_doctrine_service). 2. Whether to collapse the profiles_cmd.py hand-rolled org overlay onto IC-02 (low-risk?) or leave it. 3. Exact JSON schema key for doctor doctrine override findings (editorial — confirm with #2082 requester/daphne). 4. IC-09 layout fix: hard cutover to flat <pack>/<plural>/ vs layout-tolerant resolver (accept both, prefer flat) for backward-compat across all org-pack kinds.