---
id: skill-surface-sync
paths:
  - ".claude/**"
  - ".gzkit/skills/**"
  - ".gzkit/rules/**"
  - ".github/skills/**"
  - ".github/instructions/**"
description: Version-disciplined editing and sync for skill and rule surfaces
---

<!-- rule-version: 0.9.0 -->

# Skill & Surface Sync (gzkit)

> **Rule version:** `0.9.0` — renamed prohibited headings; lifted Rationale to expansion doc (OBPI-0.0.54-04 shape conformance pass).

## Non-negotiable rules

1. **Edit `.gzkit/` first.** The canonical source for skills is `.gzkit/skills/` and for rules is `.gzkit/rules/`. Always make changes there.
2. **Bump the version on every edit.** Increment the version marker before saving. The marker differs by surface:
   - **Skills** carry `skill-version:` in YAML frontmatter (validated by the skill schema).
   - **Rules** carry a body-level `<!-- rule-version: X.Y.Z -->` HTML comment immediately after the frontmatter, plus a visible `> **Rule version:** \`X.Y.Z\`` block quote with a one-sentence rationale. The rule frontmatter schema (`RuleFrontmatter` in `src/gzkit/rules.py`) is `extra="forbid"` and rejects a `skill-version:` key on rule files — that is intentional. The "skill-version" name on a non-skill artifact was a doctrine smell; the body-level marker resolves it.
3. **Run sync after every edit.** `uv run gz agent sync control-surfaces` propagates canonical state to ALL derived surfaces. One invocation covers BOTH the wheel-shipping byte-parity copy at `src/gzkit/<surface>/` AND vendor mirrors at `.[vendor]/<surface>/`. No separate copy step is needed (OBPI-0.0.32-08).
4. **Never edit vendor mirrors directly.** `.claude/skills/`, `.claude/rules/`, `.github/skills/`, and `.github/instructions/` are generated outputs.
5. **Never edit `src/gzkit/<surface>/` directly.** `src/gzkit/skills/`, `src/gzkit/rules/`, `src/gzkit/personas/`, and `src/gzkit/templates/` are derived outputs regenerated by sync. Only the sync mechanism (`sync_pkg_surfaces`) writes to them.
6. **`skill-version` bumps require `last_reviewed` bumps in the same edit (skills only).** Any commit that increments `skill-version:` in a skill's frontmatter MUST also set `last_reviewed:` to today's date (YYYY-MM-DD). Rationale: if the change is substantive enough to bump the version, the author has just re-examined the surface — the review-date field exists to record exactly that act. The 90-day staleness audit (`DEFAULT_MAX_REVIEW_AGE_DAYS` in `src/gzkit/skills_audit.py`) is the downstream consumer; decoupling the two fields produces fresh code with stale review metadata, which silently disables the staleness signal. Rules do not carry `last_reviewed:` (no schema field) — this clause applies to skills only.

## Surface layout

| Canonical (edit here) | Pkg copy (wheel-shipping) | Vendor mirrors (generated, do not edit) | Version marker |
|-----------------------|---------------------------|-----------------------------------------|----------------|
| `.gzkit/skills/`      | `src/gzkit/skills/`       | `.claude/skills/`, `.github/skills/`    | Frontmatter `skill-version:` |
| `.gzkit/rules/`       | `src/gzkit/rules/`        | `.claude/rules/`, `.github/instructions/` | Body `<!-- rule-version: X.Y.Z -->` + visible block quote |
| `.gzkit/personas/`    | `src/gzkit/personas/`     | `.claude/personas/`, `.agents/personas/`, `.github/personas/` | (no version marker) |
| `.gzkit/templates/`   | `src/gzkit/templates/`    | (none — consumed at scaffold-time only) | (no version marker) |
| `.gzkit/chores/<slug>/` | `src/gzkit/chores/<slug>/` (canonical class only) | (none) | (no version marker) |

`gz agent sync control-surfaces` propagates ALL rows in one invocation: first pkg copies, then vendor mirrors.

## Procedure

1. Edit the canonical file under `.gzkit/skills/` or `.gzkit/rules/`
2. Bump the version marker for the surface:
   - Skill: increment `skill-version:` in frontmatter
   - Rule: increment both the `<!-- rule-version: X.Y.Z -->` HTML comment and the visible `> **Rule version:** \`X.Y.Z\`` block quote
3. **Skills only:** update `last_reviewed:` to today's date (YYYY-MM-DD) in the same edit as the `skill-version` bump (non-negotiable rule #6)
4. Run `uv run gz agent sync control-surfaces`
5. Verify sync output shows no stale or divergent mirrors
6. If sync reports stale mirror-only paths, follow the recovery in `/gz-agent-sync` skill documentation

## Version discipline

| Change type | Bump | Example |
|-------------|------|---------|
| GovZero framework major release | Major | 6.0.0 -> 7.0.0 |
| Governance rule or procedure change | Minor | 6.0.0 -> 6.1.0 |
| Tooling, template, or wording fix | Patch | 6.0.0 -> 6.0.1 |

The same bump table applies to both skills (frontmatter version) and rules (body-level version). The marker location differs; the semver semantics do not.

## Conflict resolution

When sync detects a version mismatch between canonical and a mirror, resolve via:

| Signal | What it tells you | How to check |
|--------|-------------------|--------------|
| Version marker (semver) | Intentional edit sequence | Skill: parse frontmatter `skill-version`. Rule: parse body `<!-- rule-version: ... -->` |
| Git commit hash / timestamp | Physical edit recency | `git log -1 --format=%H -- <path>` |

**Resolution rules:**

- **Mirror version > canonical version:** An agent edited the mirror directly. Promote the mirror content to canonical, then sync. The higher version wins.
- **Mirror version == canonical version, content differs:** An agent edited the mirror without bumping the version. Use git commit timestamp to determine recency; flag for human review.
- **Canonical version > mirror version:** Normal state — sync propagates canonical to mirrors.

Version is the primary signal (intentional semantic ordering). Commit hash is the tiebreaker (physical recency when versions match but content diverges).

## Do Not

- Do not edit a skill without bumping its `skill-version` frontmatter
- Do not edit a rule without bumping its body-level `<!-- rule-version: ... -->` marker (and the visible block quote)
- Do not add `skill-version:` to rule frontmatter — the schema rejects it (GHI #307); use the body-level marker instead
- Do not edit `.claude/rules/` directly — sync overwrites it from `.gzkit/rules/`
- Do not edit `.claude/skills/` directly — edit `.gzkit/skills/` and sync
- Do not manually copy skill files between surfaces — use the sync command
- Do not skip sync because "both files look the same" — sync also updates manifests, registrations, and vendor-specific rendering

## Bootstrap semantics (`gz init`)

On first init in an adopter project, `gz init` populates
`.gzkit/skills/<slug>/SKILL.md` by copying canonical content from the wheel's
package surface (`importlib.resources.files("gzkit.skills")`). The package
surface is the *one-time bootstrap source*: after init, the adopter's
`.gzkit/skills/` is **the project canonical source-of-truth** for that
project, and the "Edit `.gzkit/` first" rule binds from that point forward.

Repair mode (re-running `gz init`) is idempotent: it adds new canonical
slugs delivered by the installed gzkit version without overwriting
operator-edited files (`skip_existing=True`). Use `--force` to wipe and
re-copy every canonical SKILL.md from the wheel.

`scaffold_core_skills` filters skills whose canonical SKILL.md declares
`lifecycle_state: retired` — retired slugs are not re-introduced on `gz init`
(hard cutover invariant, enforced by `tests/commands/test_skills.py::TestSkillCommands::test_init_scaffolds_adr_create_and_removes_adr_manager`).

As of OBPI-0.0.32-04, `gz init` also populates `.gzkit/rules/<slug>.md` by
copying canonical rule content from the wheel's package surface
(`importlib.resources.files("gzkit.rules")`). The same editing invariant
applies: after init, `.gzkit/rules/` is **the project canonical
source-of-truth** for rules in that adopter project. Rules scaffolding runs
after `sync_all` in the fresh init path so the initial control-surface sync
uses the instruction-sync path; subsequent `gz agent sync control-surfaces`
invocations render canonical rules to `.github/instructions/`. The
`AGENTS.md` file in the rules package is excluded from scaffolding (it is a
package-internal agent contract, not an operator-facing rule).

`gz init --update` (OBPI-0.0.32-05, not yet landed) will provide
version-aware refresh semantics for the adopter's `.gzkit/<surface>/` from
the wheel, with three-state IDENTICAL/STALE/EDITED detection so operator
edits are preserved across gzkit upgrades.

## Retirement policy (delete-on-retire, binding)

When a skill is superseded or consolidated, its directory is **deleted**
from every surface root — `.gzkit/skills/`, `src/gzkit/skills/`,
`.claude/skills/`, `.github/skills/`, and `.agents/skills/`. Do not
preserve a `lifecycle_state: retired` SKILL.md stub.

**Why delete:** the `package_only` classifier carve-out exists for
*non-md package-machinery* (e.g. `__init__.py`, `__pycache__/**`).
Routing retired SKILL.md tombstones through `package_only` is classifier
scope creep that inflates the shipped wheel with `archived_into`
frontmatter stubs and muddies the canonical/package_only distinction.
Operator judgment (recorded under GHI #464 / ADR-0.0.32 closeout):
*"tombstones not worth keeping if they are stubs."*

**Redirect UX is acceptably "skill not found."** A retired-name
invocation returns the missing-skill response rather than a tombstone
pointer to the successor. The discoverability cost is small enough that
indefinite wheel inflation is not justified.

**Defensive backstop preserved.** The retired-frontmatter branch in
`_classify_skill_file` (lines ~45–52 of `src/gzkit/skills/__init__.py`)
remains as a defensive net: if a tombstone leaks back in violation of
this doctrine, it is still classified `package_only` so
`gz validate --distribution` does not flag it. The doctrine is the
authored rule; the classifier branch is the runtime fallback.

**Promotion of new-skill scaffolding inherits this rule.** Any future
skill consolidation lands as a delete-on-retire pass in the same
commit/OBPI that introduces the successor, not as a follow-up cleanup
chore.

## Canonical surface class-classifier

This section codifies the three-class content model for every canonical surface (OBPI-0.0.32-13 for chores; extended to all surfaces under OBPI-0.0.32-15). Every file under any canonical surface (`src/gzkit/{chores,rules,skills,personas,templates}/`) falls into exactly one class:

| Class | Definition | Byte-parity | Sync | Validator |
|---|---|---|---|---|
| **canonical** | Operator-authored content present at `.gzkit/<surface>/` — the source of truth | Required | `.gzkit/ → src/gzkit/` | Included in wheel; present in baseline manifest |
| **package_only** | Python modules and supporting files with no `.gzkit/` counterpart (e.g. `__init__.py`, `_scaffolder.py`) | Exempt | NEVER propagate to `.gzkit/` side | Exempt from `ON_DISK_NOT_INCLUDED` |
| **runtime_state** | Per-run logs, receipts, proofs — written by agents at runtime | Exempt | NEVER sync in either direction | Exempt from both error classes |

### Default rules by surface

| Surface | canonical | package_only | runtime_state |
|---------|-----------|--------------|---------------|
| **chores** | `CHORE.md`, `AGENTS.md`, `*.md` outside `proofs/`, `acceptance.json`, `registry.json`, `.py` with `.gzkit/` counterpart | `__init__.py`, `__pycache__/**`, `.py` with no `.gzkit/` counterpart | `CHORE-LOG.md`, `proofs/**`, `.gitkeep` |
| **rules** | `*.md`, `*.json` with `.gzkit/rules/` counterpart, `.py` with `.gzkit/rules/` counterpart | `__init__.py`, `__pycache__/**`, `_scaffolder.py`, `.py` or `.json` with no `.gzkit/rules/` counterpart | (none) |
| **skills** | `SKILL.md` (all) | `__init__.py`, `__pycache__/**` | (none) |
| **personas** | `*.md` (all) | `__init__.py`, `__pycache__/**` | (none) |
| **templates** | `*.md` (all) | `__init__.py`, `__pycache__/**` | (none) |

**Python helpers (all signature-compatible with `_classify_chore_file`):**

| Surface | Helper | Module | Tests |
|---------|--------|--------|-------|
| chores | `_classify_chore_file(path, *, project_root=None)` | `gzkit.chores` | `tests/test_chores.py::TestChoresLayoutDualSurface` |
| rules | `_classify_rule_file(path, *, project_root=None)` | `gzkit.rules` | `tests/test_rules.py::TestClassifyRuleFile` |
| skills | `_classify_skill_file(path, *, project_root=None)` | `gzkit.skills` | `tests/test_skills.py::TestClassifySkillFile` |
| personas | `_classify_persona_file(path, *, project_root=None)` | `gzkit.personas` | `tests/test_personas.py::TestClassifyPersonaFile` |
| templates | `_classify_template_file(path, *, project_root=None)` | `gzkit.templates` | `tests/test_templates.py::TestClassifyTemplateFile` |

The `gz validate --distribution` validator consults these classifiers to exempt `package_only` files from `ON_DISK_NOT_INCLUDED` errors. The `sync_pkg_surfaces` function consults them to skip non-canonical files when propagating `.gzkit/ → src/gzkit/`.

**Long-term note (chores):** The chores classifier is a **temporary accommodation** for runtime-state co-location with canonical instructions. When `ADR-pool.canonical-vs-runtime-separation` promotes, runtime-state moves to a separate location and the chores classifier shrinks to a single class (canonical only).

See [Skill-Surface-Sync Rationale](../../docs/governance/skill-surface-sync-rationale.md) for the version-marker split rationale and conflict-resolution design.
