Skip to content

Skill & Surface Sync — Rationale

Expansion doc for .gzkit/rules/skill-surface-sync.md § Rationale (ADR-0.0.54-04 lift). Prose preserved verbatim; see rule file for binding bullets.

Rationale

.claude/ surfaces are the path of least resistance — that is what the agent reads at runtime. Editing there is the common drift mode; the next sync silently overwrites it. Rules cannot fully override the editing instinct, so the mechanical backstop is version + commit hash comparison: even when an agent edits the wrong surface, the version and git history make the conflict detectable and resolvable.

The split between frontmatter skill-version (skills) and body-level <!-- rule-version: ... --> (rules) keeps each artifact's version marker close to the surface that consumes it. The skill schema validates frontmatter strictly, so skill-version belongs there. The rule schema is deliberately minimal (id/paths/description) so that it can grow without breaking every rule file at once — the body-level marker preserves the version-bump invariant without requiring a schema migration every time the metadata vocabulary changes. See chores.md for the canonical body-marker form.

Bootstrap semantics (gz init)

(Lifted from .gzkit/rules/skill-surface-sync.md under the 2026-08-02 diet pass; the rule keeps the binding core and a pointer here.)

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)

(Rationale lifted from the rule's § Retirement policy, 2026-08-02; the delete-on-retire invariant and the same-commit inheritance rule remain binding in the rule file.)

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.

Class-classifier reference

(Reference tables lifted from the rule's § Canonical surface class-classifier, 2026-08-02; the three-class model and its validator/sync consumers remain in the rule file.)

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

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).