diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b8154..8f3289f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.2] - 2026-04-02 + +### Fixed +- **Upgrade auto-fixes AGENTS.md references**: when `upgrade` renames governance files (lowercase→uppercase), it now rewrites path references in AGENTS.md, CLAUDE.md, GEMINI.md, SKILL.md, and all agent config files automatically. +- **Alternate path detection**: auditor and upgrader now find LEDGER.md at `docs/LEDGER.md` and architecture docs in subdirectories (e.g. `docs/architecture/`). No more false "missing" reports or duplicate stub creation. +- **Case-insensitive architecture check**: `docs/ARCHITECTURE.md` recommended check now works regardless of filename casing. +- **CI-gated dev releases**: dev-release workflow now runs full test suite (ruff check+format, mypy, pytest) before PyPI publish. + ## [0.2.1] - 2026-04-02 ### Added @@ -186,7 +194,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **G9**: Session start file list now marks services.md as conditional ("if it exists"). - **G10**: Open TODOs format specified as `- [ ]` / `- [x]` checkbox syntax. -[Unreleased]: https://github.com/BitConcepts/specsmith/compare/v0.2.1...HEAD +[Unreleased]: https://github.com/BitConcepts/specsmith/compare/v0.2.2...HEAD +[0.2.2]: https://github.com/BitConcepts/specsmith/compare/v0.2.1...v0.2.2 [0.2.1]: https://github.com/BitConcepts/specsmith/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/BitConcepts/specsmith/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/BitConcepts/specsmith/compare/v0.1.2...v0.1.3 diff --git a/README.md b/README.md index d917e20..11dccbb 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![CI](https://github.com/BitConcepts/specsmith/actions/workflows/ci.yml/badge.svg)](https://github.com/BitConcepts/specsmith/actions/workflows/ci.yml) [![Docs](https://readthedocs.org/projects/specsmith/badge/?version=stable)](https://specsmith.readthedocs.io/en/stable/) -[![PyPI Stable](https://img.shields.io/pypi/v/specsmith?label=stable&style=flat&color=blue)](https://pypi.org/project/specsmith/) -[![PyPI Dev](https://img.shields.io/pypi/v/specsmith?include_prereleases&label=dev&style=flat&color=orange)](https://pypi.org/project/specsmith/#history) +[![PyPI Stable](https://img.shields.io/pypi/v/specsmith?label=stable&style=flat&color=blue&cacheSeconds=60)](https://pypi.org/project/specsmith/) +[![PyPI Dev](https://img.shields.io/pypi/v/specsmith?include_prereleases&label=dev&style=flat&color=orange&cacheSeconds=60)](https://pypi.org/project/specsmith/#history) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) diff --git a/pyproject.toml b/pyproject.toml index 395185f..141364e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "specsmith" -version = "0.2.1" +version = "0.2.2" description = "Forge governed project scaffolds from the Agentic AI Development Workflow Specification." readme = "README.md" license = {text = "MIT"} diff --git a/src/specsmith/__init__.py b/src/specsmith/__init__.py index 71804e1..f8ef7b2 100644 --- a/src/specsmith/__init__.py +++ b/src/specsmith/__init__.py @@ -8,4 +8,4 @@ try: __version__: str = _pkg_version("specsmith") except PackageNotFoundError: # running from source without install - __version__ = "0.2.1" + __version__ = "0.2.2" diff --git a/src/specsmith/auditor.py b/src/specsmith/auditor.py index 2791356..794dbb6 100644 --- a/src/specsmith/auditor.py +++ b/src/specsmith/auditor.py @@ -79,11 +79,15 @@ def check_governance_files(root: Path) -> list[AuditResult]: for f in REQUIRED_FILES: path = root / f + found = path.exists() + # LEDGER.md: also check docs/LEDGER.md (some imported projects place it there) + if not found and f == "LEDGER.md": + found = (root / "docs" / "LEDGER.md").exists() results.append( AuditResult( name=f"file-exists:{f}", - passed=path.exists(), - message=f"Required file {f} {'exists' if path.exists() else 'MISSING'}", + passed=found, + message=f"Required file {f} {'exists' if found else 'MISSING'}", ) ) @@ -123,8 +127,8 @@ def check_governance_files(root: Path) -> list[AuditResult]: for f in RECOMMENDED_FILES: path = root / f found = path.exists() - # For architecture.md, also search subdirectories (e.g. docs/architecture/*.md) - if not found and "architecture" in f: + # For ARCHITECTURE.md, also search subdirectories (e.g. docs/architecture/*.md) + if not found and "architecture" in f.lower(): found = ( bool( list((root / "docs").glob("**/architecture*")) @@ -232,16 +236,20 @@ def check_ledger_health(root: Path) -> list[AuditResult]: """Check ledger quality and staleness.""" results: list[AuditResult] = [] ledger_path = root / "LEDGER.md" - if not ledger_path.exists(): - results.append( - AuditResult( - name="ledger-exists", - passed=False, - message="LEDGER.md not found", + # Also check docs/LEDGER.md (some imported projects place it there) + alt = root / "docs" / "LEDGER.md" + if alt.exists(): + ledger_path = alt + else: + results.append( + AuditResult( + name="ledger-exists", + passed=False, + message="LEDGER.md not found", + ) ) - ) - return results + return results text = ledger_path.read_text(encoding="utf-8") lines = text.splitlines() diff --git a/src/specsmith/upgrader.py b/src/specsmith/upgrader.py index a8225d1..2e395ca 100644 --- a/src/specsmith/upgrader.py +++ b/src/specsmith/upgrader.py @@ -246,6 +246,41 @@ def _sync_full( out.write_text(tmpl.render(**ctx), encoding="utf-8") synced.append(f"{output_rel} (created)") + # 6. Essential docs — create stubs only if truly missing (check alternate paths) + has_ledger = (root / "LEDGER.md").exists() or (root / "docs" / "LEDGER.md").exists() + if not has_ledger: + (root / "LEDGER.md").write_text("# Ledger\n\nNo entries yet.\n", encoding="utf-8") + synced.append("LEDGER.md (created)") + + has_arch = (root / "docs" / "ARCHITECTURE.md").exists() + if not has_arch and (root / "docs").is_dir(): + # Check subdirectories (e.g. docs/architecture/CPSC-RE-ARCHITECTURE.md) + has_arch = bool( + list((root / "docs").glob("**/architecture*")) + + list((root / "docs").glob("**/ARCHITECTURE*")) + ) + if not has_arch: + try: + from specsmith.architect import generate_architecture + + generate_architecture(root) + synced.append("docs/ARCHITECTURE.md (generated from scan)") + except Exception: # noqa: BLE001 + arch_path = root / "docs" / "ARCHITECTURE.md" + arch_path.parent.mkdir(parents=True, exist_ok=True) + arch_path.write_text( + f"# Architecture \u2014 {config.name}\n\n[Run `specsmith architect` to populate]\n", + encoding="utf-8", + ) + synced.append("docs/ARCHITECTURE.md (stub created)") + specsmith_dir = root / ".specsmith" + credit_budget = specsmith_dir / "credit-budget.json" + if not credit_budget.exists(): + from specsmith.credits import CreditBudget, save_budget + + save_budget(root, CreditBudget()) + synced.append(".specsmith/credit-budget.json (created)") + return synced @@ -255,9 +290,13 @@ def _migrate_legacy_filenames(root: Path, result: UpgradeResult) -> None: Handles both case-sensitive (Linux) and case-insensitive (Windows/macOS) filesystems. On case-insensitive FS, uses a two-step rename via a temporary name to avoid conflicts. + + Also updates references in AGENTS.md so the hub links stay valid. """ import shutil + renamed: list[tuple[str, str]] = [] + for old_rel, new_rel in _LEGACY_RENAMES: old_path = root / old_rel new_path = root / new_rel @@ -276,4 +315,41 @@ def _migrate_legacy_filenames(root: Path, result: UpgradeResult) -> None: shutil.move(str(old_path), str(new_path)) else: continue # Both exist as truly separate files — skip + renamed.append((old_rel, new_rel)) result.updated_files.append(f"{old_rel} → {new_rel}") + + # Update references in user-owned docs that point to renamed files + if renamed: + _update_references(root, renamed, result) + + +def _update_references( + root: Path, + renames: list[tuple[str, str]], + result: UpgradeResult, +) -> None: + """Rewrite old paths to new paths in AGENTS.md and other hub files. + + Only performs safe string replacement of exact path references. + """ + docs_to_patch = [ + "AGENTS.md", + "CLAUDE.md", + "GEMINI.md", + ".warp/skills/SKILL.md", + ".cursor/rules/governance.mdc", + ".windsurfrules", + ".aider.conf.yml", + ] + + for doc_name in docs_to_patch: + doc_path = root / doc_name + if not doc_path.exists(): + continue + content = doc_path.read_text(encoding="utf-8") + original = content + for old_rel, new_rel in renames: + content = content.replace(old_rel, new_rel) + if content != original: + doc_path.write_text(content, encoding="utf-8") + result.updated_files.append(f"{doc_name} (references updated)")