# AGENTS.md Tidy: Control Surface Schema and Rules Mirroring

**Date**: 2026-03-15
**Status**: Design approved, pending implementation plan

## Problem

Context window bloat from duplicated governance content across control surfaces:

- AGENTS.md and CLAUDE.md both carry identical 49-line skill catalogs (~2.5K tokens each)
- CLAUDE.md duplicates Gate Covenant, Governance Workflow, and OBPI Acceptance Protocol from AGENTS.md
- Total overhead: ~5K tokens loaded into every Claude Code conversation
- Claude Code does NOT auto-load AGENTS.md — only CLAUDE.md and `.claude/rules/*.md` are loaded
- Governance rules in AGENTS.md alone get skipped by Claude (observed, not theoretical)

## Goals

1. **Reduce context window bloat** — ~80% token reduction in always-loaded content
2. **Maintain strict agent guardrails** — governance rules must reach Claude reliably
3. **AGENTS.md-first posture** — AGENTS.md stays authoritative for cross-agent operations (Claude, Copilot, Codex)
4. **gzkit owns the schema** — vendor formats are projections, not sources of truth

## Architecture

### Three-layer control surface model

```
Layer 1: Canonical Artifacts (gzkit-owned, validated)
  .gzkit/skills/*/SKILL.md          — skill definitions
  .github/instructions/*.md         — rule/instruction definitions
  .gzkit/hooks/*.py                 — hook definitions (future)
  .gzkit/schemas/*.schema.json      — validation schemas

Layer 2: Vendor Mirrors (generated by gz agent sync, never edited)
  .claude/skills/    — skill mirror for Claude Code
  .claude/rules/     — rules mirror for Claude Code
  .claude/hooks/     — hook mirror for Claude Code
  .github/skills/    — skill mirror for Copilot
  .agents/skills/    — skill mirror for Codex

Layer 3: Control Surface Documents (generated from templates + Layer 1)
  AGENTS.md          — universal agent contract
  CLAUDE.md          — Claude Code entry point
  .github/copilot-instructions.md  — Copilot entry point
```

### Vendor projection map

```
Canonical (gzkit-owned)          ->  Vendor Mirror (generated)
-----------------------------    -----------------------------
.gzkit/skills/*/SKILL.md        ->  .claude/skills/*/SKILL.md
                                ->  .github/skills/*/SKILL.md
                                ->  .agents/skills/*/SKILL.md

.github/instructions/*.md       ->  .claude/rules/*.md
                                ->  (Copilot reads directly)
                                ->  .agents/rules/*.md (future)

.gzkit/hooks/*.py               ->  .claude/hooks/*.py
                                ->  (Copilot: .github/hooks/)
```

## Design Decisions

### 1. Rules mirroring: `.github/instructions/` -> `.claude/rules/`

**What**: `gz agent sync control-surfaces` gains a new mirror step that converts Copilot instruction files into Claude rules files.

**Frontmatter conversion**:

| Copilot (`applyTo`) | Claude (`paths`) |
|---------------------|------------------|
| `applyTo: "**/*"` | No frontmatter (unconditional, always loaded) |
| `applyTo: "tests/**"` | `paths: ["tests/**"]` |
| `applyTo: "docs/**,src/gzkit/**"` | `paths: ["docs/**", "src/gzkit/**"]` |

**Glob syntax translation**: `sync_claude_rules()` must handle more than comma splitting:

1. Split `applyTo` by commas: `"docs/**,src/**"` -> `["docs/**", "src/**"]`
2. Flag unsupported features: VS Code's glob engine (used by Copilot) supports `{}` brace expansion and `!` exclusion negation. Claude Code's `paths` matcher may not. If an `applyTo` pattern contains `{`, `}`, or `!`, the sync logs a warning. Canonical instructions should avoid these until cross-vendor glob compatibility is verified.
3. Trim whitespace from each pattern after splitting.

**Agent exclusion filter**: If an instruction file has `excludeAgent: coding-agent` (or `excludeAgent: all`), `sync_claude_rules()` skips it entirely — Claude Code acts as a coding agent, so the rule has no business loading into its context.

**File naming**: Drop `.instructions` infix. `governance_core.instructions.md` -> `governance_core.md`.

**Sync behavior**: Full replace on each sync. `.claude/rules/` is 100% generated. Stale files are deleted. Directory is created if missing on first run (matching existing `sync_copilot_instructions()` pattern).

**File filter**: Only files matching `*.instructions.md` are mirrored. `README.md` and other non-instruction files in `.github/instructions/` are excluded.

**Existing instruction inventory** (14 files):

| File | applyTo | Claude loading |
|------|---------|----------------|
| `governance_core` | `**/*` | unconditional |
| `arb` | `**/*` | unconditional |
| `pythonic` | `**/*.py` | path-scoped |
| `models` | `src/**/*.py` | path-scoped |
| `tests` | `tests/**` | path-scoped |
| `adr_audit` | `docs/design/adr/**` | path-scoped |
| `gate5_runbook_code_covenant` | `docs/**,src/gzkit/**` | path-scoped |
| `gh_cli` | `.github/**,docs/design/adr/**` | path-scoped |
| `cross-platform` | `src/**/*.py,tests/**/*.py,...` | path-scoped |
| `warehouse` | `src/airlineops/warehouse/**` | path-scoped |
| `sql_hygiene` | `src/airlineops/warehouse/**` | path-scoped |
| `cli` | `src/airlineops/cli/**,...` | path-scoped |
| `chores` | `src/opsdev/**,...` | path-scoped |
| `calendars` | `config/calendars.json,...` | path-scoped |

### 2. Slim CLAUDE.md template (~40 lines)

**Removed** (now in `.claude/rules/` via instruction mirrors):
- Gate Covenant table
- Governance Workflow steps
- OBPI Acceptance Protocol
- Full 49-line skill catalog
- Architecture section (one-liner, low value)
- Key Invariants section (one-liner pointer, low value)

**Retained**:
- Project identity and purpose
- Build commands (high-frequency reference)
- Coding conventions (one line)
- Canonical contract pointer to AGENTS.md
- "Rules loaded from `.claude/rules/`" reference
- "Skills discovered from `.claude/skills/`" reference
- Local content injection from `agents.local.md`

**Template** (`src/gzkit/templates/claude.md`):

```markdown
# CLAUDE.md

This file provides guidance to Claude Code when working with {project_name}.

## Project Overview

{project_purpose}

## Canonical Contract

`AGENTS.md` is the authoritative governance contract.

If this file and `AGENTS.md` diverge, follow `AGENTS.md` and run `gz agent sync control-surfaces`.

## Tech Stack

{tech_stack}

## Build Commands

{build_commands}

## Coding Conventions

{coding_conventions}

## Governance Rules

Loaded contextually from `.claude/rules/` (mirrored from `.github/instructions/`).

Run `gz agent sync control-surfaces` to regenerate.

## Skills

Discovered automatically from `.claude/skills/`. See `AGENTS.md` for the full catalog.

## Control Surfaces

This file is generated by `gz agent sync control-surfaces` from governance canon.

Project-specific additions come from `agents.local.md`.

---

{local_content}
```

### 3. Compact AGENTS.md skill catalog

**Before**: 49-line flat alphabetical list with full description + path per skill.

**After**: ~20-line categorized name-only list grouped by function.

**Category source**: `category` field added to each SKILL.md frontmatter. This is a gzkit-only extension — both Claude and Copilot silently ignore unknown frontmatter fields. Consistent with existing gzkit extensions (`lifecycle_state`, `owner`, `last_reviewed`).

**Categories**:

| Category | Skills |
|----------|--------|
| ADR Lifecycle | `gz-plan`, `gz-adr-create`, `gz-adr-promote`, `gz-adr-status`, `gz-adr-eval`, `gz-closeout`, `gz-attest` |
| ADR Operations | `gz-adr-check`, `gz-adr-sync`, `gz-adr-recon`, `gz-adr-map`, `gz-adr-autolink`, `gz-adr-verification`, `gz-adr-emit-receipt`, `gz-adr-manager` |
| ADR Audit & Closeout | `gz-adr-audit`, `gz-adr-closeout-ceremony`, `gz-audit` |
| OBPI Pipeline | `gz-specify`, `gz-obpi-brief`, `gz-obpi-pipeline`, `gz-obpi-sync`, `gz-obpi-reconcile`, `gz-obpi-audit`, `gz-plan-audit` |
| Governance Infrastructure | `gz-init`, `gz-constitute`, `gz-prd`, `gz-interview`, `gz-gates`, `gz-validate`, `gz-implement`, `gz-state`, `gz-status` |
| Agent & Repository Operations | `gz-agent-sync`, `gz-session-handoff`, `gz-register-adrs`, `gz-migrate-semver`, `gz-check-config-paths`, `gz-tidy`, `git-sync` |
| Code Quality | `lint`, `format`, `test`, `gz-typecheck`, `gz-check`, `gz-arb`, `gz-cli-audit` |
| Cross-Repository | `airlineops-parity-scan` |

**Rendering**: `render_skills_catalog()` in `sync.py` is modified to accept a `categorized=True` parameter. When `True`, groups skills by `category` field and emits compact name-only format. When `False` (or for backward compatibility), emits the current flat format. The AGENTS.md template uses `{skills_catalog}` — the variable name stays the same, but the renderer produces categorized output by default. `categorized=True` is the default for all control surface documents (AGENTS.md, CLAUDE.md, copilot-instructions.md). The flat renderer remains in `sync.py` for testing/auditing.

**Uncategorized fallback**: Skills missing a `category` field (or with null/empty value) are grouped under an `Uncategorized` header at the bottom of the catalog. This prevents skills from silently disappearing from the catalog while making the omission an obvious quality defect visible in AGENTS.md during audits.

**Categories are a closed enum** validated by `skill.schema.json`. Allowed values: `adr-lifecycle`, `adr-operations`, `adr-audit`, `obpi-pipeline`, `governance-infrastructure`, `agent-operations`, `code-quality`, `cross-repository`.

### 4. gzkit Control Surface Schema

**Principle**: gzkit defines one canonical frontmatter schema per artifact type. Vendor formats are projections. Validation happens at the canonical level only.

**Schema files** (JSON Schema, in `.gzkit/schemas/`):

#### `skill.schema.json`

Recognized fields for `.gzkit/skills/*/SKILL.md` frontmatter:

| Field | Type | Required | gzkit Extension | Notes |
|-------|------|----------|-----------------|-------|
| `name` | string | no | no | Lowercase, hyphens, max 64 chars; defaults to directory name |
| `description` | string | recommended | no | What the skill does and when to use it |
| `category` | string | recommended | **yes** | Grouping for catalog rendering |
| `lifecycle_state` | string | no | **yes** | `active`, `deprecated`, `draft` |
| `owner` | string | no | **yes** | Who maintains this skill |
| `last_reviewed` | string (date) | no | **yes** | ISO date of last review |
| `user-invocable` | boolean | no | no | Show in `/` menu (default: true) |
| `disable-model-invocation` | boolean | no | no | Prevent auto-loading (default: false) |
| `argument-hint` | string | no | no | Autocomplete hint |
| `allowed-tools` | string | no | no | Claude-specific: tools without permission |
| `model` | string | no | no | Claude-specific: model override |
| `context` | string | no | no | Claude-specific: `fork` for subagent |
| `agent` | string | no | no | Claude-specific: subagent type |

#### `rule.schema.json`

Recognized fields for `.github/instructions/*.md` frontmatter:

| Field | Type | Required | gzkit Extension | Notes |
|-------|------|----------|-----------------|-------|
| `name` | string | no | **yes** | Display name for the rule |
| `description` | string | no | **yes** | What the rule enforces |
| `category` | string | no | **yes** | Grouping (future catalog use) |
| `applyTo` | string | **yes** | no | Copilot glob patterns (comma-separated) |
| `excludeAgent` | string | no | no | Copilot-specific: `code-review` or `coding-agent` |

**Validation command**: `gz validate --surfaces` checks all canonical artifacts against these schemas.

**What's validated**:
- Required fields present
- Field types correct
- `category` values from allowed set (if constrained)
- `applyTo` patterns are valid globs
- `lifecycle_state` from allowed enum
- Skill directory name matches `name` field (if provided)

## Implementation Scope

### Files to modify

| File | Change |
|------|--------|
| `src/gzkit/sync.py` | Add `sync_claude_rules()` (called from `sync_all()` after `sync_agents_md()` and before `sync_skill_mirrors()`), modify `collect_skills_catalog()` to read `category` field, modify `render_skills_catalog()` to support categorized output |
| `src/gzkit/templates/claude.md` | Slim to ~40 lines |
| `src/gzkit/templates/agents.md` | No template variable change needed — `{skills_catalog}` stays, renderer now produces categorized output |
| `.gzkit/schemas/skill.schema.json` | New file — skill frontmatter schema |
| `.gzkit/schemas/rule.schema.json` | New file — rule frontmatter schema |
| `src/gzkit/cli.py` | Wire `gz validate --surfaces` to schema validation |
| `.gzkit/manifest.json` | Add `"claude_rules": ".claude/rules"` and `"instructions": ".github/instructions"` to `control_surfaces` |
| 49x `.gzkit/skills/*/SKILL.md` | Add `category` frontmatter field |

### Files NOT modified (generated, will be regenerated)

- `AGENTS.md`
- `CLAUDE.md`
- `.claude/skills/` (existing mirror)
- `.claude/rules/` (new mirror)

## Token Budget Analysis

| Surface | Before | After | Savings |
|---------|--------|-------|---------|
| CLAUDE.md (always loaded) | ~155 lines | ~40 lines | ~115 lines |
| Skill catalog in CLAUDE.md | 49 lines | 0 (removed) | 49 lines |
| Skill catalog in AGENTS.md | 49 lines | ~20 lines | 29 lines |
| Gate Covenant in CLAUDE.md | 12 lines | 0 (in rules) | 12 lines |
| Governance Workflow in CLAUDE.md | 10 lines | 0 (in rules) | 10 lines |
| OBPI Acceptance in CLAUDE.md | 14 lines | 0 (in rules) | 14 lines |
| **Total always-loaded** | **~290 lines** | **~60 lines** | **~230 lines (~80%)** |

Governance rules now load contextually via `.claude/rules/` — only when Claude touches matching files.

**Deduplication note**: The savings come from eliminating *duplication*, not adding net-new content. Currently Claude loads the Gate Covenant and OBPI workflow twice: once from CLAUDE.md and once from `governance_core` (which loads unconditionally via `applyTo: "**/*"`). After this change, those rules exist only in `.claude/rules/governance_core.md` (unconditional). The `governance_core` rule staying unconditional is working as intended — it's the single source, not a duplicate.

## Scope Exclusions

- **Codex rules mirroring** (`.agents/rules/*.md`): Out of scope for this implementation. Tracked as future work in the vendor projection map.
- **Hook schema**: `.gzkit/hooks/*.py` schema is TBD and not part of this implementation.

## Risk Mitigation

| Risk | Mitigation |
|------|-----------|
| Claude skips governance after CLAUDE.md slims down | `governance_core.instructions.md` has `applyTo: "**/*"` — mirrors as unconditional rule, always loaded |
| Stale rules in `.claude/rules/` | Sync does full replace + stale file deletion |
| `category` field breaks vendor tools | Tested: both Claude and Copilot silently ignore unknown frontmatter |
| Schema validation too strict | Start permissive (recommended, not required fields); tighten over time |
| Instruction files with no/malformed frontmatter | `sync_claude_rules()` skips files without valid `applyTo` frontmatter and logs a warning. `README.md` excluded by file filter. |
| Claude `paths` glob semantics differ from Copilot `applyTo` | Both use standard glob syntax (`**`, `*`). Verify during implementation with a smoke test. `gz validate --surfaces` should include a mirrored-rules parse check. |

## References

- Claude Code skills docs: skills are discovered from `.claude/skills/` filesystem, not from CLAUDE.md
- Claude Code rules docs: `.claude/rules/*.md` files auto-load like CLAUDE.md, with optional `paths` frontmatter for scoping
- Copilot instructions docs: `.github/instructions/*.md` with `applyTo` frontmatter
- gzkit memory: gzkit is the headless CMS for governance; schema-driven everything
