diff --git a/extensions/catalog.json b/extensions/catalog.json index 02486c0658..e2ff82430d 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -144,8 +144,8 @@ "repository-governance": { "name": "Repository Governance", "id": "repository-governance", - "version": "2.0.2", - "description": "Generate or update the active Repository Governance Framework file with vertical SSOT routing and evidence", + "version": "2.0.5", + "description": "Generate Repository Governance Framework files from Spec Kit metadata", "author": "bigben", "repository": "https://github.com/bigsmartben/spec-kit-agent-governance", "license": "MIT", diff --git a/extensions/repository-governance/.extensionignore b/extensions/repository-governance/.extensionignore index 43edb8e715..73a6e73711 100644 --- a/extensions/repository-governance/.extensionignore +++ b/extensions/repository-governance/.extensionignore @@ -8,6 +8,7 @@ __pycache__/ *.pyc .pytest_cache/ tests/ +tools/ .DS_Store .venv/ dist/ diff --git a/extensions/repository-governance/CHANGELOG.md b/extensions/repository-governance/CHANGELOG.md index e8c9eb4a2d..71bfd921f2 100644 --- a/extensions/repository-governance/CHANGELOG.md +++ b/extensions/repository-governance/CHANGELOG.md @@ -2,6 +2,20 @@ ## Unreleased +## [2.0.5] - 2026-06-24 + +### Fixed + +- Update the release smoke install command to use Spec Kit's current `--integration codex` option. + +## [2.0.4] - 2026-06-24 + +### Changed + +- Lock local GitHub Actions verification commands with `uv run --locked` and keep the extension install smoke test on the GitHub runner. +- Broaden repository fact detection for extension assets, Spec Kit metadata, project policy files, feature specs, build/runtime config, and Python/uv test commands. +- Replace the local `zip` shell command with a cross-platform Python package builder shared by docs and the release workflow. + ### Fixed - Keep spec-kit integration PR sync aligned with the runtime extension package boundary and current spec-kit test node ids. diff --git a/extensions/repository-governance/README.md b/extensions/repository-governance/README.md index d9c260082d..e3bfecd0ee 100644 --- a/extensions/repository-governance/README.md +++ b/extensions/repository-governance/README.md @@ -25,17 +25,8 @@ Generate the active Repository Governance Framework SSOT section. ## Install -New projects install `repository-governance` by default with `specify init`. -Use the catalog command to restore or reinstall the bundled extension: - -```bash -specify extension add repository-governance -``` - -Install a specific released archive when you need to pin an exact external version: - ```bash -specify extension add repository-governance --from https://github.com/bigsmartben/spec-kit-agent-governance/archive/refs/tags/v2.0.2.zip +specify extension add repository-governance --from https://github.com/bigsmartben/spec-kit-agent-governance/archive/refs/tags/v2.0.5.zip ``` Local development: @@ -59,9 +50,7 @@ uv run python .specify/extensions/repository-governance/scripts/refresh_reposito ## Build ```bash -rm -f dist/repository-governance.zip -mkdir -p dist -zip -qr dist/repository-governance.zip extension.yml commands scripts templates -x '*/__pycache__/*' '*.pyc' +uv run python tools/build_repository_governance_zip.py ``` ## Files @@ -73,12 +62,14 @@ zip -qr dist/repository-governance.zip extension.yml commands scripts templates ## SSOT Coverage -- Architecture SSOT evidence from source roots, route files, API contracts, and deployment directories. -- Engineering SSOT evidence from CI workflows, release/version files, manifests, and task runners. +- Architecture SSOT evidence from source roots, extension source assets, route files, API contracts, and deployment directories. +- Engineering SSOT evidence from CI workflows, release/version files, command/template governance contracts, manifests, and task runners. - Code Style SSOT evidence from formatter, lint, type-check, and test configuration. - Directory Structure SSOT evidence from repository areas scanned to depth 2. -- Toolchain SSOT evidence from manifests, lockfiles, Docker, compose, and task runner files. -- Agent Harness SSOT evidence from active agent context files, repository-local skills, and MCP config candidates. +- Toolchain SSOT evidence from manifests, lockfiles, extension assets, build config, runtime config, Docker, compose, and task runner files. +- Agent Harness SSOT evidence from active agent context files, Spec Kit metadata, repository-local skills, and MCP config candidates. +- Repository fact evidence from README files, project docs, repository policy files, feature specs, source/test paths, and runtime/build configuration. +- Development command evidence from package scripts or Python/uv test conventions. ## Agent Adapter @@ -91,6 +82,6 @@ Repository-local `SKILL.md` files are indexed by declared name, description, tri ## Verify ```bash -uv run python -m py_compile scripts/refresh_repository_governance.py -uv run pytest -q tests/test_extensions.py::TestBundledCommunityExtensionLocator tests/integrations/test_cli.py::TestGitExtensionAutoInstall::test_community_extensions_and_workflow_preset_auto_installed +uv run --locked python -m py_compile scripts/refresh_repository_governance.py tools/build_repository_governance_zip.py tests/test_governance_domains.py +uv run --locked pytest -q ``` diff --git a/extensions/repository-governance/commands/speckit.repository-governance.refresh.md b/extensions/repository-governance/commands/speckit.repository-governance.refresh.md index ce07b247c8..3494af9549 100644 --- a/extensions/repository-governance/commands/speckit.repository-governance.refresh.md +++ b/extensions/repository-governance/commands/speckit.repository-governance.refresh.md @@ -35,6 +35,10 @@ $ARGUMENTS - Directory Structure evidence - Toolchain evidence - Agent Harness evidence + - README, project docs, repository policy, and Spec Kit metadata + - extension assets and command/template governance contracts + - feature specs, API contracts, build config, runtime config, source paths, and test paths + - development commands from package scripts or Python/uv test conventions 9. Resolve the Spec Kit Agent Adapter for the active integration. - context target - repository-local skill discovery behavior diff --git a/extensions/repository-governance/extension.yml b/extensions/repository-governance/extension.yml index f736701acd..fe1a8ed9ff 100644 --- a/extensions/repository-governance/extension.yml +++ b/extensions/repository-governance/extension.yml @@ -3,7 +3,7 @@ schema_version: "1.0" extension: id: repository-governance name: "Repository Governance" - version: "2.0.2" + version: "2.0.5" description: "Generate Repository Governance Framework files from Spec Kit metadata" author: "bigben" repository: "https://github.com/bigsmartben/spec-kit-agent-governance" diff --git a/extensions/repository-governance/scripts/refresh_repository_governance.py b/extensions/repository-governance/scripts/refresh_repository_governance.py index b80faa9e41..c9f819a923 100755 --- a/extensions/repository-governance/scripts/refresh_repository_governance.py +++ b/extensions/repository-governance/scripts/refresh_repository_governance.py @@ -51,6 +51,122 @@ "windsurf": ".windsurf/rules/specify-rules.md", } +README_FILES = ["README.md", "README.markdown", "README.txt"] +PROJECT_DOC_FILES = [ + "CONTRIBUTING.md", + "SECURITY.md", + "SUPPORT.md", + "ARCHITECTURE.md", + "ROADMAP.md", +] +PROJECT_DOC_DIRS = ["docs", "adr", "adrs", "specs"] +PACKAGE_MANIFESTS = [ + "package.json", + "pyproject.toml", + "Cargo.toml", + "go.mod", + "Gemfile", + "pom.xml", + "build.gradle", + "build.gradle.kts", + "composer.json", + "requirements.txt", + "setup.py", + "setup.cfg", +] +LOCKFILES = [ + "package-lock.json", + "pnpm-lock.yaml", + "yarn.lock", + "bun.lockb", + "uv.lock", + "poetry.lock", + "Pipfile.lock", + "Cargo.lock", + "go.sum", + "Gemfile.lock", + "composer.lock", +] +TASK_RUNNERS = ["Makefile", "Taskfile.yml", "Taskfile.yaml", "justfile", "Brewfile"] +SOURCE_DIRS = ["src", "app", "lib", "services", "packages", "apps", "cmd", "internal", "scripts", "commands", "templates"] +TEST_DIRS = ["test", "tests", "spec", "specs", "e2e"] +REPOSITORY_POLICY_FILES = [ + "LICENSE", + "CODEOWNERS", + ".github/CODEOWNERS", + "CONTRIBUTING.md", + "SECURITY.md", + "SUPPORT.md", +] +EXTENSION_ASSET_FILES = ["extension.yml", ".extensionignore"] +EXTENSION_ASSET_DIRS = ["commands", "templates"] +EXTENSION_CONTRACT_FILES = [ + "commands/speckit.repository-governance.refresh.md", + "templates/repository-governance-template.md", + "docs/extension-governance.md", +] +API_CONTRACT_FILES = [ + "openapi.yaml", + "openapi.yml", + "openapi.json", + "api.yaml", + "api.yml", + "schema.graphql", + "buf.yaml", +] +ARCHITECTURE_DIRS = ["infra", "deploy", "deployment", "k8s", "helm", "terraform"] +BUILD_CONFIG_FILES = [ + "tsconfig.json", + "jsconfig.json", + "vite.config.js", + "vite.config.ts", + "next.config.js", + "next.config.mjs", + "webpack.config.js", + "rollup.config.js", + "turbo.json", + "nx.json", + "pnpm-workspace.yaml", + "lerna.json", +] +CODE_STYLE_FILES = [ + ".editorconfig", + ".prettierrc", + ".prettierrc.json", + ".prettierrc.yml", + "prettier.config.js", + "prettier.config.cjs", + "eslint.config.js", + "eslint.config.mjs", + ".eslintrc", + ".eslintrc.json", + "biome.json", + "ruff.toml", + ".ruff.toml", + "pyproject.toml", + "mypy.ini", + "pytest.ini", + "tox.ini", + "vitest.config.js", + "vitest.config.ts", + "jest.config.js", + "playwright.config.ts", +] +RUNTIME_CONFIG_FILES = [ + ".env.example", + ".env.sample", + "Dockerfile", + "docker-compose.yml", + "docker-compose.yaml", + "compose.yml", + "compose.yaml", + "Procfile", + "vercel.json", + "netlify.toml", + "fly.toml", +] +SPEC_KIT_METADATA = [".specify/integration.json", ".specify/init-options.json", ".specify/extensions.yml"] + def main() -> int: root = Path.cwd() @@ -150,19 +266,21 @@ def repository_evidence_summary(root: Path, state: dict[str, Any], init_options: def repository_evidence_lines(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> list[str]: lines = [ - evidence_line("README", existing_paths(root, ["README.md", "README.markdown", "README.txt"])), - evidence_line( - "Package manifest", - existing_paths(root, ["package.json", "pyproject.toml", "Cargo.toml", "go.mod", "Gemfile", "pom.xml", "build.gradle", "build.gradle.kts"]), - ), - evidence_line( - "Lockfiles", - existing_paths(root, ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "uv.lock", "poetry.lock", "Cargo.lock", "go.sum", "Gemfile.lock"]), - ), - evidence_line("Task runners", existing_paths(root, ["Makefile", "Taskfile.yml", "Taskfile.yaml", "justfile"])), + evidence_line("README", existing_paths(root, README_FILES)), + evidence_line("Project docs", unique_ordered([*existing_paths(root, PROJECT_DOC_FILES), *existing_dirs(root, PROJECT_DOC_DIRS)])), + evidence_line("Repository policy", existing_paths(root, REPOSITORY_POLICY_FILES)), + evidence_line("Spec Kit metadata", existing_paths(root, SPEC_KIT_METADATA)), + evidence_line("Extension assets", extension_asset_paths(root)), + evidence_line("Package manifest", package_manifest_paths(root)), + evidence_line("Lockfiles", lockfile_paths(root)), + evidence_line("Task runners", existing_paths(root, TASK_RUNNERS)), evidence_line("CI workflows", directory_files(root, ".github/workflows")), - evidence_line("Source paths", existing_dirs(root, ["src", "app", "lib", "scripts", "commands", "templates"])), - evidence_line("Test paths", existing_dirs(root, ["test", "tests", "spec", "specs"])), + evidence_line("Source paths", source_paths(root)), + evidence_line("Test paths", test_paths(root)), + evidence_line("Feature specs", scan_feature_specs(root)), + evidence_line("API contracts", api_contract_paths(root)), + evidence_line("Build config", build_config_paths(root)), + evidence_line("Runtime config", runtime_config_paths(root)), evidence_line("Repository areas", repository_area_paths(root)), evidence_line("Existing agent context files", existing_context_files(root, init_options, state)), evidence_line("Repository-local skills", scan_skills(root)), @@ -187,10 +305,10 @@ def vertical_ssot_evidence_lines(root: Path, state: dict[str, Any], init_options def architecture_evidence(root: Path) -> list[str]: return unique_ordered( [ - *existing_dirs(root, ["src", "app", "lib", "services", "packages"]), + *source_paths(root), *route_files(root), - *existing_paths(root, ["openapi.yaml", "openapi.yml", "openapi.json", "api.yaml", "api.yml", "schema.graphql"]), - *existing_dirs(root, ["infra", "deploy", "k8s", "helm"]), + *api_contract_paths(root), + *existing_dirs(root, ARCHITECTURE_DIRS), ] ) @@ -199,8 +317,10 @@ def engineering_evidence(root: Path) -> list[str]: return unique_ordered( [ *directory_files(root, ".github/workflows"), - *existing_paths(root, ["CHANGELOG.md", "RELEASE.md", "VERSION", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"]), - *existing_paths(root, ["Makefile", "Taskfile.yml", "Taskfile.yaml", "justfile"]), + *existing_paths(root, ["CHANGELOG.md", "RELEASE.md", "VERSION"]), + *existing_paths(root, EXTENSION_CONTRACT_FILES), + *package_manifest_paths(root), + *existing_paths(root, TASK_RUNNERS), ] ) @@ -208,26 +328,9 @@ def engineering_evidence(root: Path) -> list[str]: def code_style_evidence(root: Path) -> list[str]: return unique_ordered( [ - *existing_paths( - root, - [ - ".editorconfig", - ".prettierrc", - ".prettierrc.json", - ".prettierrc.yml", - "prettier.config.js", - "eslint.config.js", - ".eslintrc", - ".eslintrc.json", - "ruff.toml", - ".ruff.toml", - "pyproject.toml", - "mypy.ini", - "pytest.ini", - "tox.ini", - ], - ), - *existing_dirs(root, ["test", "tests", "spec", "specs"]), + *existing_paths(root, CODE_STYLE_FILES), + *existing_top_level_globs(root, ["*.prettierrc.*", "eslint.config.*", "jest.config.*", "playwright.config.*", "vitest.config.*"]), + *test_paths(root), ] ) @@ -239,33 +342,25 @@ def directory_structure_evidence(root: Path) -> list[str]: def toolchain_evidence(root: Path) -> list[str]: return unique_ordered( [ - *existing_paths( - root, - [ - "package.json", - "pyproject.toml", - "Cargo.toml", - "go.mod", - "Gemfile", - "pom.xml", - "build.gradle", - "build.gradle.kts", - "Dockerfile", - "docker-compose.yml", - "docker-compose.yaml", - "Makefile", - "Taskfile.yml", - "Taskfile.yaml", - "justfile", - ], - ), - *existing_paths(root, ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "uv.lock", "poetry.lock", "Cargo.lock", "go.sum", "Gemfile.lock"]), + *package_manifest_paths(root), + *lockfile_paths(root), + *existing_paths(root, TASK_RUNNERS), + *extension_asset_paths(root), + *build_config_paths(root), + *runtime_config_paths(root), ] ) def agent_harness_evidence(root: Path, init_options: dict[str, Any], state: dict[str, Any]) -> list[str]: - return unique_ordered([*existing_context_files(root, init_options, state), *scan_skills(root), *scan_mcp_configs(root)]) + return unique_ordered( + [ + *existing_context_files(root, init_options, state), + *existing_paths(root, SPEC_KIT_METADATA), + *scan_skills(root), + *scan_mcp_configs(root), + ] + ) def evidence_line(label: str, values: list[str]) -> str: @@ -295,6 +390,67 @@ def existing_dirs(root: Path, names: list[str]) -> list[str]: return [f"{name}/" for name in names if (root / name).is_dir()] +def existing_top_level_globs(root: Path, patterns: list[str]) -> list[str]: + matches: list[str] = [] + for pattern in patterns: + for path in sorted(root.glob(pattern)): + if path.is_file(): + matches.append(rel(root, path)) + return unique_ordered(matches) + + +def package_manifest_paths(root: Path) -> list[str]: + return unique_ordered( + [ + *existing_paths(root, PACKAGE_MANIFESTS), + *existing_top_level_globs(root, ["requirements*.txt"]), + ] + ) + + +def lockfile_paths(root: Path) -> list[str]: + return existing_paths(root, LOCKFILES) + + +def source_paths(root: Path) -> list[str]: + return existing_dirs(root, SOURCE_DIRS) + + +def test_paths(root: Path) -> list[str]: + return existing_dirs(root, TEST_DIRS) + + +def api_contract_paths(root: Path) -> list[str]: + return unique_ordered( + [ + *existing_paths(root, API_CONTRACT_FILES), + *existing_top_level_globs(root, ["*.proto", "*.graphql"]), + ] + ) + + +def build_config_paths(root: Path) -> list[str]: + return unique_ordered( + [ + *existing_paths(root, BUILD_CONFIG_FILES), + *existing_top_level_globs(root, ["tsconfig*.json", "vite.config.*", "next.config.*", "webpack.config.*"]), + ] + ) + + +def extension_asset_paths(root: Path) -> list[str]: + return unique_ordered([*existing_paths(root, EXTENSION_ASSET_FILES), *existing_dirs(root, EXTENSION_ASSET_DIRS)]) + + +def runtime_config_paths(root: Path) -> list[str]: + return unique_ordered( + [ + *existing_paths(root, RUNTIME_CONFIG_FILES), + *existing_dirs(root, ARCHITECTURE_DIRS), + ] + ) + + def directory_files(root: Path, directory: str) -> list[str]: base = root / directory if not base.is_dir(): @@ -369,6 +525,10 @@ def development_command_lines(root: Path) -> list[str]: if commands: commands.append("- manifest commands over ad hoc equivalents") return commands + commands = python_project_command_lines(root) + if commands: + commands.append("- project commands over ad hoc equivalents") + return commands return ["- none detected"] @@ -388,6 +548,15 @@ def package_script_lines(root: Path) -> list[str]: return result +def python_project_command_lines(root: Path) -> list[str]: + if not (root / "pyproject.toml").is_file(): + return [] + if not (test_paths(root) or existing_paths(root, ["pytest.ini", "tox.ini"])): + return [] + prefix = "uv run --locked" if (root / "uv.lock").is_file() else "uv run" + return [f"- `{prefix} pytest -q` -> pytest suite"] + + def read_json(path: Path) -> dict[str, Any]: if not path.exists(): return {} @@ -764,15 +933,15 @@ def handoff_default(style: str) -> list[str]: return ["- changed files", "- commands run", "- validation result", "- unresolved risks"] -def scan_feature_specs(root: Path) -> str: +def scan_feature_specs(root: Path) -> list[str]: specs = root / "specs" if not specs.is_dir(): - return "none" - entries = [] + return [] + entries: list[str] = [] for feature in sorted(path for path in specs.iterdir() if path.is_dir()): statuses = [f"{name}:{'present' if (feature / name).exists() else 'missing'}" for name in ("spec.md", "plan.md", "tasks.md")] entries.append(f"{rel(root, feature)} ({', '.join(statuses)})") - return ", ".join(entries) if entries else "none" + return entries def scan_skills(root: Path) -> list[str]: