Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
repos:
- repo: local
hooks:
- id: sync-version-check
name: sync-version-check
entry: python3 scripts/sync_version.py --check
language: system
pass_filenames: false
always_run: true
- id: pytest-unit
name: pytest-unit
entry: python3 -m pytest tests/unit -q
language: system
pass_filenames: false
always_run: true
15 changes: 14 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Follow the repository policy in `AGENTS.md`.
- Release branches merge to `master` and back to `develop`.
- Tags named `v*` are created only from `master`.

## Local Checks
## Local Checks & Pre-Commit

Run the relevant checks before opening a pull request:

Expand All @@ -25,6 +25,19 @@ python3 skills/adr-toolkit/scripts/adr.py validate --dir docs/decisions --json
python3 skills/adr-toolkit/scripts/adr.py index --dir docs/decisions --json
```

You can also install the local pre-commit hook to run these checks automatically before committing:

```bash
pip install pre-commit
pre-commit install
```

### Plugin Adapters & Manifest Governance

If you add or modify a harness adapter (e.g. under `adapters/` or `.claude-plugin/`):
- Every `plugin.json` or `gemini-extension.json` must be registered in `MANIFEST_SPECS` and `DESCRIPTION_MANIFEST_SPECS` in `scripts/sync_version.py`.
- `python3 scripts/sync_version.py --check` will fail in CI if an untracked or out-of-sync manifest file is added.

For changes that may affect accepted architectural decisions, also run:

```bash
Expand Down
3 changes: 2 additions & 1 deletion adapters/antigravity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Antigravity plugins are a `plugin.json` marker file plus optional sibling
directories (`skills/`, `agents/`, `rules/`), per
`antigravity.google/docs/cli/plugins/`. This manifest needs only `name`.
`antigravity.google/docs/cli/plugins/`. This manifest includes `name`,
`version`, `description`, and `$schema`.
**Manually verified against Antigravity's `agy` CLI 1.1.13** (`agy
--version`): validate, install, and discovery all work — see "Verification
status" below.
Expand Down
1 change: 1 addition & 0 deletions adapters/antigravity/plugin.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
"name": "adr-toolkit",
"version": "0.2.1",
"description": "Initialize, record, and check Architecture Decision Records by inspecting the repository and existing decisions before asking questions."
}
15 changes: 4 additions & 11 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,15 @@ Lightweight human-readable summary of meaningful repository changes.

## Unreleased

- Fixed Windows CP1252 console encoding failure (`UnicodeEncodeError: 'charmap' codec can't encode character '\u2713'`) in `scripts/verify_examples.py` and `tests/integration/test_examples.py` by replacing non-ASCII symbols with ASCII tags (`[ok]`, `[error]`), reconfiguring stdout/stderr UTF-8 streams, and setting `PYTHONIOENCODING=utf-8` in subprocess calls.
- Added untracked manifest discovery (`discover_untracked_manifests`) in `scripts/sync_version.py` to automatically prevent untracked plugin/extension manifests from being added in PRs without version/description tracking.
- Added `.pre-commit-config.yaml` for local contributor pre-commit checks and updated `CONTRIBUTING.md` with manifest governance guidelines.
- Enhanced Antigravity CLI (`agy`) plugin manifest (`adapters/antigravity/plugin.json`) with `version` tracking integrated into `scripts/sync_version.py`, expanded unit test assertions in `test_antigravity_adapter.py` (including symlink layout simulation) and `test_readme.py`, and updated `README.md` documentation.
- Added Conventional Commits PR title validation job (`pr-title-check`) to GitHub Actions workflow (`.github/workflows/test.yml`)
to enforce standard title format (`feat:`, `fix:`, `docs:`, etc.) for pull requests.
- Updated `.github/PULL_REQUEST_TEMPLATE.md` with Conventional Commits title format guide and an explicit Examples Impact checklist
requiring example updates for `feat:` and `fix:` changes while skipping non-feature PRs.

## v0.2.1 (2026-08-31)

- Redesigned and expanded `examples/` into representative, structured usage guides
(`basic-usage.md`, `check-constraints.md`, `graph-visualization.md`, and
`multilingual-adr.md`) with standardized Scenario, Input, What Happens, and Output sections.
- Added a full Korean documentation suite under [`examples/ko/`](file:///Users/yangseunghyeon/orca/workspaces/ADR-toolkit/seasnake/examples/ko/README.md)
(including `basic-usage.md`, `check-constraints.md`, `graph-visualization.md`, and `multilingual-adr.md`).
- Created `scripts/verify_examples.py` and `tests/integration/test_examples.py` to
automatically verify that all documented example commands execute cleanly and to auto-update
example output snippets when core `adr.py` logic or schemas change.

- Added a `harness-parity` CI job that installs the real Codex CLI and
Gemini CLI and drives their own plugin/extension commands (marketplace
add, install, list) against this repo, then runs `preflight`/`init`/
Expand Down
21 changes: 9 additions & 12 deletions handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@

## Current task (2026-09-01)

Examples redesign, Korean documentation, automated verification pipeline, `v0.2.1` release, and PR Conventional Commits automation.

### Implemented this session:

- **Examples Redesign (`examples/`)**: Created 4 structured, representative use-case guides (`basic-usage.md`, `check-constraints.md`, `graph-visualization.md`, `multilingual-adr.md`) using standard Scenario → Input → What Happens → Output format.
- **Korean Documentation Suite (`examples/ko/`)**: Added full Korean translation suite (`basic-usage.md`, `check-constraints.md`, `graph-visualization.md`, `multilingual-adr.md`, `README.md`).
- **Automated Verification Pipeline**:
- `scripts/verify_examples.py`: `--check` (executes example workflows in isolated temp repo) & `--update` (auto-updates JSON output snippets when CLI outputs change).
- `tests/integration/test_examples.py`: Integration test ensuring 100% executable example parity in `pytest`.
- **v0.2.1 Release**: Bumped version to `0.2.1`, synced manifests (`SKILL.md`, `.claude-plugin/plugin.json`, `adapters/gemini-cli/gemini-extension.json`), tagged `v0.2.1` on `master`, merged via Git Flow, and pushed to `origin`.
- **PR Title Linter & PR Template**: Added `pr-title-check` CI job to `.github/workflows/test.yml` enforcing Conventional Commits format (`feat:`, `fix:`, `docs:`, etc.) and updated `.github/PULL_REQUEST_TEMPLATE.md` with explicit Examples Impact checklist for `feat:`/`fix:` changes.
- **Lifecycle Report**: Recorded automation strategy in `automated_examples_lifecycle_report.md` artifact.
**AGY (`agy`) Plugin Integration & Adapter Enhancements.**
Working on branch `feature/agy-plugin-implements-2`:

- Enhanced Antigravity CLI (`agy`) plugin manifest (`adapters/antigravity/plugin.json`) with `version` field.
- Registered `adapters/antigravity/plugin.json` version tracking in `scripts/sync_version.py` (`MANIFEST_SPECS`).
- Added `discover_untracked_manifests()` in `scripts/sync_version.py` to automatically catch and block any untracked plugin/extension manifest added in PRs.
- Created `.pre-commit-config.yaml` for pre-commit verification and updated `CONTRIBUTING.md` with manifest governance rules.
- Updated unit test assertions in `tests/unit/test_antigravity_adapter.py` (including symlink layout simulation), `tests/unit/test_sync_version.py`, and `tests/unit/test_readme.py`.
- Updated `adapters/antigravity/README.md` and `README.md` documentation.

## Touched files

Expand Down
23 changes: 23 additions & 0 deletions scripts/sync_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
MANIFEST_SPECS = [
(REPO_ROOT / ".claude-plugin" / "plugin.json", ["version"]),
(REPO_ROOT / "adapters" / "gemini-cli" / "gemini-extension.json", ["version"]),
(REPO_ROOT / "adapters" / "antigravity" / "plugin.json", ["version"]),
]

# SKILL.md's frontmatter `description:` is the single canonical source; every
Expand Down Expand Up @@ -148,6 +149,28 @@ def require_known_paths() -> None:
names = ", ".join(f"{_display_path(p)} ({key})" for p, key in keyless)
raise SystemExit(f"tracked manifest(s) lost a tracked key: {names}")

untracked = discover_untracked_manifests()
if untracked:
names = ", ".join(_display_path(p) for p in sorted(untracked, key=str))
raise SystemExit(f"untracked plugin/extension manifest(s) found: {names}")


def discover_untracked_manifests() -> list:
"""Discover any untracked plugin or extension manifest files in the repo.

Prevents external contributors from adding a new plugin manifest file without
registering it in MANIFEST_SPECS or DESCRIPTION_MANIFEST_SPECS.
"""
all_specs = MANIFEST_SPECS + DESCRIPTION_MANIFEST_SPECS
tracked = {p for p, _ in all_specs}
candidates = []
for glob_pat in ("adapters/**/plugin.json", "adapters/**/*.json", ".claude-plugin/*.json"):
for path in REPO_ROOT.glob(glob_pat):
if path.name in ("plugin.json", "gemini-extension.json", "antigravity-plugin.json") and path.is_file():
if path not in tracked:
candidates.append(path)
return candidates


def _display_path(path: Path) -> str:
try:
Expand Down
9 changes: 7 additions & 2 deletions scripts/verify_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,18 @@ def main(argv=None) -> int:
parser.add_argument("--update", action="store_true", help="Auto-update examples if needed")
args = parser.parse_args(argv)

if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace")

print("Verifying examples execution against adr.py...")
try:
verify_all_flows()
print("✓ All example workflows executed successfully and verified clean.")
print("[ok] All example workflows executed successfully and verified clean.")
return 0
except AssertionError as err:
print(f"❌ Verification failed: {err}", file=sys.stderr)
print(f"[error] Verification failed: {err}", file=sys.stderr)
return 1


Expand Down
6 changes: 6 additions & 0 deletions tests/integration/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Integration test verifying that examples/*.md workflows remain executable and up-to-date with adr.py logic.
"""
import os
import subprocess
import sys
from pathlib import Path
Expand All @@ -10,10 +11,15 @@

def test_examples_execution_and_schema_parity():
"""Verify that all example workflows execute cleanly without error."""
env = dict(os.environ, PYTHONIOENCODING="utf-8")
res = subprocess.run(
[sys.executable, str(VERIFY_SCRIPT), "--check"],
cwd=REPO_ROOT,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=env,
)
assert res.returncode == 0, f"Example verification script failed:\nstdout: {res.stdout}\nstderr: {res.stderr}"

30 changes: 30 additions & 0 deletions tests/unit/test_antigravity_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
def test_manifest_is_valid_json_with_required_fields():
data = json.loads(MANIFEST.read_text(encoding="utf-8"))
assert data["name"] == "adr-toolkit"
assert "version" in data
assert "description" in data


def test_manifest_name_matches_antigravity_naming_rule():
Expand All @@ -23,3 +25,31 @@ def test_manifest_name_matches_antigravity_naming_rule():
def test_manifest_schema_field_points_at_antigravity_schema():
data = json.loads(MANIFEST.read_text(encoding="utf-8"))
assert data["$schema"] == "https://antigravity.google/schemas/v1/plugin.json"


def test_antigravity_adapter_directory_layout_and_symlink_structure(tmp_path):
# Simulate Antigravity plugin installation layout:
# adapters/antigravity/plugin.json + skills/adr-toolkit symlink
repo_root = Path(__file__).resolve().parents[2]
adapter_dir = tmp_path / "adapters" / "antigravity"
adapter_dir.mkdir(parents=True)

manifest_copy = adapter_dir / "plugin.json"
manifest_copy.write_text(MANIFEST.read_text(encoding="utf-8"), encoding="utf-8")

skills_dir = adapter_dir / "skills"
skills_dir.mkdir()
target_skill = repo_root / "skills" / "adr-toolkit"
symlink_path = skills_dir / "adr-toolkit"

try:
symlink_path.symlink_to(target_skill, target_is_directory=True)
except OSError:
pytest.skip("Symlink creation not supported on this platform/user permission")

assert manifest_copy.is_file()
assert (symlink_path / "SKILL.md").is_file()
manifest_data = json.loads(manifest_copy.read_text(encoding="utf-8"))
assert manifest_data["name"] == "adr-toolkit"


7 changes: 7 additions & 0 deletions tests/unit/test_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,10 @@ def test_readme_scopes_check_confidence():
assert "CHECK does not certify the entire architecture" in text
for label in ["VERIFIED", "VIOLATED", "UNVERIFIABLE", "NOT_APPLICABLE"]:
assert label in text


def test_readme_documents_harness_adapters_including_antigravity():
text = README.read_text(encoding="utf-8")
assert "[Antigravity CLI](adapters/antigravity/)" in text
assert "adapters/antigravity/README.md" in text

6 changes: 6 additions & 0 deletions tests/unit/test_sync_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,9 @@ def test_real_manifests_have_a_description_matching_skill_md():
for key in key_path:
target = target[key]
assert target == canonical, f"{path} description has drifted from SKILL.md"


def test_discover_untracked_manifests_finds_no_untracked_files_in_clean_repo():
untracked = _sync_version.discover_untracked_manifests()
assert untracked == [], f"untracked plugin manifests found: {untracked}"

Loading