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
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
2 changes: 1 addition & 1 deletion src/specsmith/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
32 changes: 20 additions & 12 deletions src/specsmith/auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}",
)
)

Expand Down Expand Up @@ -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*"))
Expand Down Expand Up @@ -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()
Expand Down
76 changes: 76 additions & 0 deletions src/specsmith/upgrader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
Expand All @@ -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)")
Loading