Quickstart: Canonical State Authority & Single Metadata Writer
Feature: 051-canonical-state-authority-single-metadata-writer
What Changed
Before (scattered writes, Activity Log dependency)
# Acceptance read Activity Log body text to check lane state
entries = activity_entries(wp.body)
if entries[-1]["lane"] != "done":
fail("WP not done")
# meta.json written directly by 18 different code paths
meta_path.write_text(json.dumps(meta, indent=2, sort_keys=True) + "\n")
# ... with inconsistent formatting across sites
After (canonical state, single writer)
# Acceptance reads canonical status snapshot
from specify_cli.status.reducer import materialize
snapshot = materialize(feature_dir)
if snapshot[wp_id].lane != "done":
fail("WP not done")
# meta.json written through one API
from specify_cli.feature_metadata import record_acceptance
record_acceptance(feature_dir, accepted_by="claude", mode="standard", ...)
Using the Metadata API
Reading metadata
from specify_cli.feature_metadata import load_meta
meta = load_meta(feature_dir) # Returns dict or None
Writing metadata (mutation helpers)
from specify_cli.feature_metadata import (
record_acceptance,
record_merge,
finalize_merge,
set_vcs_lock,
set_documentation_state,
set_target_branch,
)
# Record acceptance (both standard and orchestrator use this)
record_acceptance(
feature_dir,
accepted_by="claude",
mode="standard", # or "orchestrator"
from_commit="abc123",
accept_commit="def456",
)
# Record merge
record_merge(
feature_dir,
merged_by="claude",
merged_into="2.x",
strategy="merge",
push=True,
)
# Set VCS lock
set_vcs_lock(feature_dir, vcs_type="git", locked_at="2026-03-18T12:00:00+00:00")
Validation
from specify_cli.feature_metadata import validate_meta
errors = validate_meta(meta)
if errors:
for e in errors:
print(f"Validation error: {e}")
Key Principles
1. Canonical state is the only truth: status.events.jsonl for lanes, meta.json for metadata 2. Compatibility views are derived: frontmatter lane, Activity Log, tasks.md status block — readable but not authoritative 3. One writer for meta.json: All mutations go through feature_metadata.py 4. Atomic writes: temp file + os.replace() — no partial corruption 5. Stable formatting: json.dumps(meta, indent=2, ensure_ascii=False, sort_keys=True) + "\n"