---
id: OBPI-0.17.0-02-rules-mirroring
parent: ADR-0.17.0-agentsmd-tidy-control-surface-schema-and-rules-mir
item: 2
lane: heavy
status: attested_completed
---

# OBPI-0.17.0-02-rules-mirroring: Rules Mirroring

## ADR Item

- **Source ADR:** `docs/design/adr/pre-release/ADR-0.17.0-agentsmd-tidy-control-surface-schema-and-rules-mir/ADR-0.17.0-agentsmd-tidy-control-surface-schema-and-rules-mir.md`
- **Checklist Item:** #2 - "Rules Mirroring"

**Status:** Completed

## Objective

Implement a canonical-source rules pipeline where `.gzkit/rules/*.md` files (with validated Pydantic frontmatter) are the single source of truth, and vendor-specific mirrors (`.claude/rules/*.md`, `.github/instructions/*.instructions.md`) are generated by `gz agent sync control-surfaces` with format transformation, stale-file cleanup, and drift detection.

## Lane

**heavy** - Inherited from parent ADR-0.17.0-agentsmd-tidy-control-surface-schema-and-rules-mir (heavy).

> Heavy is reserved for command/API/schema/runtime-contract changes. Rules mirroring changes the external contract surface that agents consume.

## Allowed Paths

- `src/gzkit/rules.py` - CanonicalRule model, load/render/sync functions
- `src/gzkit/instruction_audit.py` - Drift detection (reachability, orphans, content divergence)
- `src/gzkit/sync.py` - Integration of rules rendering into sync_all()
- `tests/test_rules.py` - CanonicalRule, load_rule, render, validate tests
- `tests/test_agent_sync.py` - Render rule tests (Claude, Copilot, rules-to-dir)
- `tests/test_sync.py` - sync_claude_rules integration tests
- `tests/test_instruction_audit.py` - Drift detection tests
- `.gzkit/rules/*.md` - Canonical rule definitions (read-only reference)

## Denied Paths

- `docs/design/**` - ADR changes out of scope (except this brief)
- New dependencies
- CI files, lockfiles
- `.gzkit/skills/` - Skill definitions (separate OBPI scope)

## Requirements (FAIL-CLOSED)

1. REQUIREMENT: `.gzkit/rules/` MUST be the single source of truth for all governance rules.
2. REQUIREMENT: `CanonicalRule` and `RuleFrontmatter` MUST be Pydantic BaseModel with `ConfigDict(frozen=True, extra="forbid")`.
3. REQUIREMENT: `render_rule_for_claude()` MUST produce Claude-format output: global rules omit frontmatter, scoped rules get `paths:` YAML.
4. REQUIREMENT: `render_rule_for_copilot()` MUST produce Copilot-format output with `applyTo:` comma-separated globs.
5. REQUIREMENT: `render_rules_to_dir()` MUST clean up stale generated files (bearing "Generated by gz agent sync" marker).
6. NEVER: Vendor mirror files may not be edited manually — only generated by sync.
7. ALWAYS: All 11 canonical rules must produce mirrors in both `.claude/rules/` and `.github/instructions/`.

> STOP-on-BLOCKERS: if `.gzkit/rules/` is empty or `src/gzkit/rules.py` is missing, print a BLOCKERS list and halt.

## Discovery Checklist

**Governance (read once, cache):**

- [x] `AGENTS.md` - agent operating contract (three-layer control surface model)
- [x] Parent ADR - vendor projection map

**Context:**

- [x] Parent ADR: ADR-0.17.0 three-layer control surface model
- [x] Related OBPIs: OBPI-01 (Skill Catalog), OBPI-04 (JSON Schemas)

**Prerequisites (check existence, STOP if missing):**

- [x] `.gzkit/rules/` contains 11 canonical rule files
- [x] `src/gzkit/rules.py` exists with CanonicalRule model

**Existing Code (understand current state):**

- [x] `src/gzkit/rules.py` lines 415-552: CanonicalRule, load_rule, render_*, render_rules_to_dir
- [x] `tests/test_rules.py`: 15+ test classes covering rules pipeline
- [x] `tests/test_agent_sync.py`: TestRenderRuleForClaude, TestRenderRuleForCopilot, TestRenderRulesToDir
- [x] `tests/test_sync.py` lines 853-959: 7 integration tests for sync_claude_rules

## Quality Gates

### Gate 1: ADR

- [x] Intent and scope recorded in this OBPI brief
- [x] Parent ADR checklist item quoted

### Gate 2: TDD

- [x] Tests pass: `uv run gz test`
- [x] Validation commands recorded in evidence with real outputs

### Code Quality

- [x] Lint clean: `uv run gz lint`
- [x] Type check clean: `uv run gz typecheck`

### Gate 3: Docs (Heavy only)

- [x] AGENTS.md control surface section documents three-layer model

### Gate 4: BDD (Heavy only)

- [x] N/A — no behave features for rules mirroring

### Gate 5: Human (Heavy only)

- [x] Human attestation recorded

## Verification

```bash
uv run gz lint
uv run gz typecheck
uv run gz test

# Specific verification: all canonical rules mirrored
uv run python -c "
from pathlib import Path
canonical = sorted(f.name for f in Path('.gzkit/rules').glob('*.md'))
claude = sorted(f.name for f in Path('.claude/rules').glob('*.md'))
copilot = sorted(f.stem.replace('_','-').replace('.instructions','') for f in Path('.github/instructions').glob('*.instructions.md'))
print(f'Canonical: {len(canonical)} rules')
print(f'Claude mirror: {len(claude)} rules')
print(f'Copilot mirror: {len(copilot)} rules')
assert canonical == claude, f'Claude mismatch: {set(canonical) ^ set(claude)}'
assert sorted(canonical) == sorted(f+\".md\" for f in copilot) or len(canonical) == len(copilot), f'Copilot count mismatch'
print('PASS: All rules mirrored')
"

# Verify CanonicalRule is Pydantic
uv run python -c "
from gzkit.rules import CanonicalRule, RuleFrontmatter
from pydantic import BaseModel
assert issubclass(CanonicalRule, BaseModel), 'CanonicalRule must be Pydantic'
assert issubclass(RuleFrontmatter, BaseModel), 'RuleFrontmatter must be Pydantic'
assert CanonicalRule.model_config.get('frozen') is True
assert CanonicalRule.model_config.get('extra') == 'forbid'
print('PASS: Pydantic models validated')
"
```

## Acceptance Criteria

- [x] REQ-0.17.0-02-01: `.gzkit/rules/` is the canonical source with Pydantic-validated frontmatter (id, paths[], description)
- [x] REQ-0.17.0-02-02: `render_rule_for_claude()` produces correct Claude format (global omits FM, scoped has paths:)
- [x] REQ-0.17.0-02-03: `render_rule_for_copilot()` produces correct Copilot format (applyTo: comma-separated)
- [x] REQ-0.17.0-02-04: `render_rules_to_dir()` writes mirrors and cleans stale generated files
- [x] REQ-0.17.0-02-05: All 11 canonical rules produce mirrors in both vendor directories
- [x] REQ-0.17.0-02-06: `instruction_audit.py` detects drift (missing rules, orphans, content divergence)
- [x] REQ-0.17.0-02-07: [doc] Tests cover load, render, sync, and drift detection paths

## Completion Checklist

- [x] **Gate 1 (ADR):** Intent recorded in brief
- [x] **Gate 2 (TDD):** Tests pass, coverage maintained
- [x] **Code Quality:** Lint, format, type checks clean
- [x] **Value Narrative:** Problem-before vs capability-now is documented
- [x] **Key Proof:** One concrete usage example is included
- [x] **OBPI Acceptance:** Evidence recorded below

> For ceremony steps and lane-inheritance attestation rules, see `AGENTS.md` section `OBPI Acceptance Protocol`.

## Evidence

### Gate 1 (ADR)

- [x] Intent and scope recorded

### Gate 2 (TDD)

```text
$ uv run gz test
Ran 685 tests in 18.436s — OK

$ uv run -m unittest tests.test_rules tests.test_agent_sync -v
Ran 45 tests in 0.113s — OK
```

### Code Quality

```text
$ uv run gz lint
All checks passed! Lint passed.

$ uv run gz typecheck
All checks passed! Type check passed.
```

### Gate 3 (Docs)

```text
AGENTS.md lines 175-177: Control surface section documents three-layer model.
Verification: 11 canonical → 11 Claude mirror + 11 Copilot mirror — PASS
```

### Gate 4 (BDD)

```text
N/A — no behave features for rules mirroring structure
```

### Gate 5 (Human)

```text
Human attestation: "attest completed" — 2026-03-19
```

### Value Narrative

Before this OBPI, governance rules existed as ad-hoc files in vendor-specific directories with no canonical source of truth or validation. Rules could drift between Claude and Copilot mirrors undetected. Now, `.gzkit/rules/` serves as the validated canonical source with Pydantic frontmatter, and `gz agent sync control-surfaces` renders vendor-specific mirrors with format transformation, stale cleanup, and drift detection via `instruction_audit.py`.

### Key Proof

```text
$ uv run python -c "from gzkit.rules import load_rules, render_rule_for_claude; ..."
Loaded 11 canonical rules
  adr-audit: paths=['docs/design/adr/**']
  arb: paths=['**/*']
  chores: paths=['src/gzkit/**', 'config/**', '.github/skills/**']
Claude render (adr-audit):
  <!-- Generated by gz agent sync — do not edit -->
  ---
  paths:
    - "docs/design/adr/**"
  ---
  # ADR Audit (gzkit)

$ uv run python -c "from pathlib import Path; ..."
Canonical: 11 rules, Claude mirror: 11 rules, Copilot mirror: 11 rules
PASS: All rules mirrored
```

### Implementation Summary

- Files created/modified: `src/gzkit/rules.py` (CanonicalRule, RuleFrontmatter, load_rule, load_rules, render_rule_for_claude, render_rule_for_copilot, render_rules_to_dir)
- Tests added: `tests/test_rules.py` (TestCanonicalRule, TestLoadRule, TestLoadRules), `tests/test_agent_sync.py` (TestRenderRuleForClaude, TestRenderRuleForCopilot, TestRenderRulesToDir), `tests/test_sync.py` (7 sync_claude_rules tests), `tests/test_instruction_audit.py` (drift detection tests)
- Date completed: 2026-03-19
- Attestation status: Human attested
- Defects noted: None

## Tracked Defects

_No defects tracked._

## Human Attestation

- Attestor: `human:Jeff`
- Attestation: attest completed
- Date: 2026-03-19

---

**Brief Status:** Completed

**Date Completed:** 2026-03-19

**Evidence Hash:** -
