From c07e40dd023350a04870dc225f4439dc0b9f30af Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 04:58:36 +0000 Subject: [PATCH 1/3] Update repository-governance extension to v3.0.0 --- extensions/catalog.json | 4 +- extensions/repository-governance/CHANGELOG.md | 19 + extensions/repository-governance/README.md | 42 +- .../speckit.repository-governance.generate.md | 56 +++ .../speckit.repository-governance.refresh.md | 64 --- .../repository-governance/extension.yml | 28 +- ...e.py => generate_repository_governance.py} | 431 +++++++----------- .../repository-governance-template.md | 51 ++- 8 files changed, 315 insertions(+), 380 deletions(-) create mode 100644 extensions/repository-governance/commands/speckit.repository-governance.generate.md delete mode 100644 extensions/repository-governance/commands/speckit.repository-governance.refresh.md rename extensions/repository-governance/scripts/{refresh_repository_governance.py => generate_repository_governance.py} (71%) diff --git a/extensions/catalog.json b/extensions/catalog.json index e80493f8f4..f1a1d92d4c 100644 --- a/extensions/catalog.json +++ b/extensions/catalog.json @@ -144,8 +144,8 @@ "repository-governance": { "name": "Repository Governance", "id": "repository-governance", - "version": "2.0.6", - "description": "Generate Repository Governance Framework files from Spec Kit metadata", + "version": "3.0.0", + "description": "Generate project-governance projections from Spec Kit metadata", "author": "bigben", "repository": "https://github.com/bigsmartben/spec-kit-agent-governance", "license": "MIT", diff --git a/extensions/repository-governance/CHANGELOG.md b/extensions/repository-governance/CHANGELOG.md index 5c10cbce10..d8c503cb7f 100644 --- a/extensions/repository-governance/CHANGELOG.md +++ b/extensions/repository-governance/CHANGELOG.md @@ -2,6 +2,25 @@ ## Unreleased +### Changed + +- Merge Toolchain SSOT coverage into Engineering SSOT to remove overlapping vertical ownership. +- Remove the cache/managed-section model; generation now overwrites the active agent platform target from the current repository scan. +- Make generated projections subordinate to vertical SSOT documents and source-backed repository facts on substantive conflicts. +- Preserve `SKILL.md` trigger metadata in the scenario capability index. +- Restrict custom `context_file` projection targets to safe agent/rules/instructions context paths. +- Tighten generated write-boundary instructions around cache-free active-target generation, `CONTEXT_FILES` legacy cleanup, and protected-write validation gates. +- Rename the public command and packaged script from `refresh` to `generate`. + +## [3.0.0] - 2026-06-25 + +### Changed + +- Reposition generated output as project-governance projections for Spec Kit supported agent platform targets while keeping the `repository-governance` extension identity and runtime filenames. +- Replace generated `SPECKIT GOVERNANCE` managed markers with `PROJECT GOVERNANCE` markers and treat legacy Speckit markers as migration cleanup targets. +- Normalize legacy initialization cache wording during refresh so old cache sections cannot reintroduce `SPECKIT GOVERNANCE` markers or weaker protected-write rules. +- Resolve missing or unknown integration metadata through the explicit `generic` entry in `CONTEXT_FILES`. + ## [2.0.6] - 2026-06-24 ### Fixed diff --git a/extensions/repository-governance/README.md b/extensions/repository-governance/README.md index 7bf769c5d7..0c750f1656 100644 --- a/extensions/repository-governance/README.md +++ b/extensions/repository-governance/README.md @@ -1,16 +1,16 @@ # Spec Kit Repository Governance -Generate the active Repository Governance Framework SSOT section. +Generate project-governance projections for the active Spec Kit agent platform target. ## Output -- Active target file from Spec Kit integration metadata. -- Managed `SPECKIT GOVERNANCE` section. +- Active agent platform target from safe `context_file` override or Spec Kit integration metadata. +- Generated `PROJECT GOVERNANCE` projection file. ## Scope -- Generate missing target governance file. -- Update existing target governance file. +- Generate the resolved active agent platform target when missing. +- Update existing active target project-governance projections. - Distill detected repository areas into action rules. - Capture repository facts as vertical SSOT evidence. - Project agent platform adapter rules from Spec Kit integration metadata. @@ -18,15 +18,15 @@ Generate the active Repository Governance Framework SSOT section. - Analyze repository areas to depth 2 only. - Include hidden and cache directories in repository area governance. - Enforce one primary responsibility per directory. -- Preserve user-authored content outside managed markers. -- Preserve managed markers verbatim. -- Keep `.specify/memory/repository-governance.md` internal. -- Review only the active target file. +- Overwrite the active agent platform target on generation. +- Generate repository evidence from the current repository state on every run. +- Review only the active agent platform target. +- Remove legacy managed sections only from non-active context files enumerated by `CONTEXT_FILES`. ## Install ```bash -specify extension add repository-governance --from https://github.com/bigsmartben/spec-kit-agent-governance/archive/refs/tags/v2.0.6.zip +specify extension add repository-governance --from https://github.com/bigsmartben/spec-kit-agent-governance/archive/refs/tags/v3.0.0.zip ``` Local development: @@ -38,13 +38,13 @@ specify extension add --dev /path/to/spec-kit-agent-governance ## Run ```text -/speckit.repository-governance.refresh +/speckit.repository-governance.generate ``` Helper: ```bash -uv run python .specify/extensions/repository-governance/scripts/refresh_repository_governance.py +uv run python .specify/extensions/repository-governance/scripts/generate_repository_governance.py ``` ## Build @@ -56,32 +56,34 @@ uv run python tools/build_repository_governance_zip.py ## Files - `extension.yml` -- `commands/speckit.repository-governance.refresh.md` -- `scripts/refresh_repository_governance.py` +- `commands/speckit.repository-governance.generate.md` +- `scripts/generate_repository_governance.py` - `templates/repository-governance-template.md` -## SSOT Coverage +## Vertical SSOT Coverage - 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. +- Engineering SSOT evidence from CI workflows, release/version files, command/template governance contracts, manifests, lockfiles, task runners, extension assets, build config, runtime config, Docker, and compose files. - 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, 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. + +## Evidence Coverage + - 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 - Repository Capability: abstract repository-local skill specs and MCP evidence into scenario-level capabilities. -- Spec Kit Agent Adapter: map the active integration to the context target and supported discovery behavior. -- Platform Projection: emit only rules the active agent context file can safely apply. +- Spec Kit Agent Adapter: map the active integration to the active agent platform target and supported discovery behavior. +- Platform Projection: emit only rules the active agent platform target can safely apply. Repository-local `SKILL.md` files are indexed by declared name, description, trigger, and source path. MCP config files are reported as candidates and evidence only; active servers, resources, and tools must be enumerated from the agent platform runtime before use. ## Verify ```bash -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 python -m py_compile scripts/generate_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.generate.md b/extensions/repository-governance/commands/speckit.repository-governance.generate.md new file mode 100644 index 0000000000..4c55897e99 --- /dev/null +++ b/extensions/repository-governance/commands/speckit.repository-governance.generate.md @@ -0,0 +1,56 @@ +--- +description: "Generate the active project-governance projection" +--- + +# Project Governance Projection Generation + +## Input + +$ARGUMENTS + +## Output + +- Active agent platform target. +- Generated `PROJECT GOVERNANCE` projection file. + +## Procedure + +1. Require `.specify/`. +2. Resolve the active agent platform target: + - `.specify/init-options.json` `context_file` when it is a safe relative agent/rules/instructions context path + - `.specify/integration.json` `default_integration` or `integration` + - default context target from `CONTEXT_FILES` +3. Generate or overwrite the active agent platform target. +4. Distill detected repository areas into action rules. + - depth: 2 + - include hidden and cache directories +5. Capture repository facts from the current repository state as vertical SSOT evidence. + - Architecture evidence + - Engineering evidence + - Code Style evidence + - Directory Structure evidence + - Agent Harness evidence + - README, project docs, repository policy, and Spec Kit metadata + - extension assets, command/template governance contracts, manifests, lockfiles, task runners, build config, and runtime config + - feature specs, API contracts, source paths, and test paths + - development commands from package scripts or Python/uv test conventions +6. Resolve the Spec Kit Agent Adapter for the active integration. + - active agent platform target + - repository-local skill discovery behavior + - MCP runtime discovery behavior + - repository MCP config candidates as evidence only +7. Project the scenario capability index. + - repository-local skill capabilities from `SKILL.md` name, description, trigger, and source path + - MCP-backed external tool capability with runtime enumeration before use +8. Run: + + ```bash + uv run python .specify/extensions/repository-governance/scripts/generate_repository_governance.py + ``` + +## Report + +- active agent platform target +- generated or updated +- review target +- captured evidence from the current repository state diff --git a/extensions/repository-governance/commands/speckit.repository-governance.refresh.md b/extensions/repository-governance/commands/speckit.repository-governance.refresh.md deleted file mode 100644 index 3494af9549..0000000000 --- a/extensions/repository-governance/commands/speckit.repository-governance.refresh.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -description: "Generate or update the active repository governance file" ---- - -# Repository Governance Generate/Update - -## Input - -$ARGUMENTS - -## Output - -- Active repository governance file. -- Managed `SPECKIT GOVERNANCE` section. -- `.specify/memory/repository-governance.md`: internal cache. - -## Procedure - -1. Require `.specify/`. -2. Resolve target: - - `.specify/init-options.json` `context_file` - - `.specify/integration.json` `default_integration` or `integration` - - `AGENTS.md` -3. Create internal cache when missing. -4. Generate target file when missing. -5. Update only the managed section when target exists. -6. Use existing managed section as refresh source. -7. Distill detected repository areas into action rules. - - depth: 2 - - include hidden and cache directories -8. Capture repository facts as vertical SSOT evidence. - - Architecture evidence - - Engineering evidence - - Code Style evidence - - 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 - - MCP runtime discovery behavior - - repository MCP config candidates as evidence only -10. Project the scenario capability index. - - repository-local skill capabilities from `SKILL.md` name, description, trigger, and source path - - MCP-backed external tool capability with runtime enumeration before use -11. Preserve content outside managed markers. -12. Preserve managed markers verbatim. -13. Run: - - ```bash - uv run python .specify/extensions/repository-governance/scripts/refresh_repository_governance.py - ``` - -## Report - -- target governance file -- generated or updated -- review target -- internal cache status -- captured evidence when cache is created diff --git a/extensions/repository-governance/extension.yml b/extensions/repository-governance/extension.yml index 50e18645b1..b93cc329c4 100644 --- a/extensions/repository-governance/extension.yml +++ b/extensions/repository-governance/extension.yml @@ -3,8 +3,8 @@ schema_version: "1.0" extension: id: repository-governance name: "Repository Governance" - version: "2.0.6" - description: "Generate Repository Governance Framework files from Spec Kit metadata" + version: "3.0.0" + description: "Generate project-governance projections from Spec Kit metadata" author: "bigben" repository: "https://github.com/bigsmartben/spec-kit-agent-governance" homepage: "https://github.com/bigsmartben/spec-kit-agent-governance" @@ -18,26 +18,26 @@ requires: provides: commands: - - name: "speckit.repository-governance.refresh" - file: "commands/speckit.repository-governance.refresh.md" - description: "Generate or update the active repository governance file" + - name: "speckit.repository-governance.generate" + file: "commands/speckit.repository-governance.generate.md" + description: "Generate the active project-governance projection" hooks: after_constitution: - command: "speckit.repository-governance.refresh" + command: "speckit.repository-governance.generate" optional: true - prompt: "Refresh repository governance after constitution changes?" - description: "Generates the active repository governance file" + prompt: "Generate project-governance projection after constitution changes?" + description: "Generates the active agent platform target" after_plan: - command: "speckit.repository-governance.refresh" + command: "speckit.repository-governance.generate" optional: true - prompt: "Refresh repository governance after planning?" - description: "Keeps the active repository governance file aligned" + prompt: "Generate project-governance projection after planning?" + description: "Keeps the active agent platform target aligned" after_tasks: - command: "speckit.repository-governance.refresh" + command: "speckit.repository-governance.generate" optional: true - prompt: "Refresh repository governance after task generation?" - description: "Keeps the active repository governance file aligned" + prompt: "Generate project-governance projection after task generation?" + description: "Keeps the active agent platform target aligned" tags: - "governance" diff --git a/extensions/repository-governance/scripts/refresh_repository_governance.py b/extensions/repository-governance/scripts/generate_repository_governance.py similarity index 71% rename from extensions/repository-governance/scripts/refresh_repository_governance.py rename to extensions/repository-governance/scripts/generate_repository_governance.py index c9f819a923..710735f594 100755 --- a/extensions/repository-governance/scripts/refresh_repository_governance.py +++ b/extensions/repository-governance/scripts/generate_repository_governance.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Generate or refresh Spec Kit repository governance for the current project.""" +"""Generate Spec Kit project-governance projections.""" from __future__ import annotations @@ -10,13 +10,27 @@ from typing import Any -MEMORY_PATH = Path(".specify/memory/repository-governance.md") -TEMPLATE_PATH = Path(".specify/extensions/repository-governance/templates/repository-governance-template.md") INTEGRATION_JSON = Path(".specify/integration.json") INIT_OPTIONS_JSON = Path(".specify/init-options.json") -MARKER_START = "" -MARKER_END = "" +MARKER_START = "" +MARKER_END = "" +LEGACY_MARKER_START = "" +LEGACY_MARKER_END = "" +GENERIC_INTEGRATION = "generic" +CONTEXT_FILE_SUFFIXES = {".md", ".mdc", ".txt"} +CONTEXT_FILE_NAME_PATTERN = re.compile(r"(agent|agents|rule|rules|instruction|instructions|governance)", re.IGNORECASE) +PROTECTED_CUSTOM_CONTEXT_ROOTS = { + ".github", + ".git", + "scripts", + "src", + "tests", + "test", + "spec", + "specs", +} +MCP_CONFIG_NAMES = {".mcp.json", "mcp.json", "mcp.yml", "mcp.yaml", "mcp.config.json"} CONTEXT_FILES = { "agy": "AGENTS.md", @@ -101,7 +115,7 @@ EXTENSION_ASSET_FILES = ["extension.yml", ".extensionignore"] EXTENSION_ASSET_DIRS = ["commands", "templates"] EXTENSION_CONTRACT_FILES = [ - "commands/speckit.repository-governance.refresh.md", + "commands/speckit.repository-governance.generate.md", "templates/repository-governance-template.md", "docs/extension-governance.md", ] @@ -176,88 +190,19 @@ def main() -> int: state = read_json(root / INTEGRATION_JSON) init_options = read_json(root / INIT_OPTIONS_JSON) - created_memory = ensure_memory(root) target = resolve_target(root, state, init_options) - projection = render_projection(root, target, state, created_memory) - evidence_summary = repository_evidence_summary(root, state, init_options) if created_memory else "" + projection = render_projection(root, target, state, init_options) + evidence_summary = repository_evidence_summary(root, state, init_options) action = write_projection(target, projection) - remove_stale_sections(root, target, init_options, state) + remove_stale_sections(root, target, init_options) - print(f"Target governance file: {rel(root, target)}") - print(f"Governance file: {action}") + print(f"Target agent platform file: {rel(root, target)}") + print(f"Project-governance projection: {action}") print(f"Review target: {rel(root, target)}") - print(f"Internal initialization cache: {MEMORY_PATH.as_posix()} ({'created' if created_memory else 'existing'})") - if created_memory: - print(f"Repository evidence: {evidence_summary}") + print(f"Repository evidence: {evidence_summary}") return 0 -def ensure_memory(root: Path) -> bool: - memory = root / MEMORY_PATH - if memory.exists(): - return False - template = root / TEMPLATE_PATH - if not template.exists(): - raise SystemExit(f"Error: template not found: {TEMPLATE_PATH.as_posix()}") - memory.parent.mkdir(parents=True, exist_ok=True) - memory.write_text(render_initial_memory(root, template.read_text(encoding="utf-8-sig")), encoding="utf-8") - return True - - -def render_initial_memory(root: Path, template: str) -> str: - content = normalize_newlines(template) - state = read_json(root / INTEGRATION_JSON) - init_options = read_json(root / INIT_OPTIONS_JSON) - content = replace_sync_report(content, root, state) - evidence = "\n".join( - [ - "## Repository Evidence", - "", - *repository_evidence_lines(root, state, init_options), - "", - "## Vertical SSOT Evidence", - "", - *vertical_ssot_evidence_lines(root, state, init_options), - "", - "## Repository Areas", - "", - *repository_area_lines(root), - "", - "## Development Commands", - "", - *development_command_lines(root), - "", - ] - ) - marker = "\n## Scope\n" - if marker in content: - content = content.replace(marker, f"\n{evidence}{marker}", 1) - else: - content = content.rstrip() + "\n\n" + evidence - return content - - -def replace_sync_report(content: str, root: Path, state: dict[str, Any]) -> str: - report = "\n".join( - [ - "", - ] - ) - pattern = re.compile(r"", re.DOTALL) - if pattern.search(content): - return pattern.sub(report, content, count=1) - return content - - def repository_evidence_summary(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> str: evidence = repository_evidence_lines(root, state, init_options) detected = [line for line in evidence if "none detected" not in line and "`unknown`" not in line] @@ -286,7 +231,7 @@ def repository_evidence_lines(root: Path, state: dict[str, Any], init_options: d evidence_line("Repository-local skills", scan_skills(root)), evidence_line("MCP configs", scan_mcp_configs(root)), f"- Active integration: `{default_integration(state) or 'unknown'}`", - f"- Resolved context file: `{rel(root, resolve_target(root, state, init_options))}`", + f"- Resolved agent platform target: `{rel(root, resolve_target(root, state, init_options))}`", ] return lines @@ -297,7 +242,6 @@ def vertical_ssot_evidence_lines(root: Path, state: dict[str, Any], init_options evidence_line("Engineering evidence", engineering_evidence(root)), evidence_line("Code Style evidence", code_style_evidence(root)), evidence_line("Directory Structure evidence", directory_structure_evidence(root)), - evidence_line("Toolchain evidence", toolchain_evidence(root)), evidence_line("Agent Harness evidence", agent_harness_evidence(root, init_options, state)), ] @@ -320,7 +264,11 @@ def engineering_evidence(root: Path) -> list[str]: *existing_paths(root, ["CHANGELOG.md", "RELEASE.md", "VERSION"]), *existing_paths(root, EXTENSION_CONTRACT_FILES), *package_manifest_paths(root), + *lockfile_paths(root), *existing_paths(root, TASK_RUNNERS), + *extension_asset_paths(root), + *build_config_paths(root), + *runtime_config_paths(root), ] ) @@ -339,19 +287,6 @@ def directory_structure_evidence(root: Path) -> list[str]: return repository_area_paths(root) -def toolchain_evidence(root: Path) -> list[str]: - return unique_ordered( - [ - *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( [ @@ -507,17 +442,19 @@ def repository_area_paths(root: Path) -> list[str]: def existing_context_files(root: Path, init_options: dict[str, Any], state: dict[str, Any]) -> list[str]: - paths: set[Path] = {root / "AGENTS.md"} + paths = known_context_paths(root, init_options) + resolved = resolve_target(root, state, init_options) + paths.add(resolved) + return sorted(rel(root, path) for path in paths if path.exists()) + + +def known_context_paths(root: Path, init_options: dict[str, Any]) -> set[Path]: + paths: set[Path] = set() for value in CONTEXT_FILES.values(): target = safe_project_path(root, value) if target is not None: paths.add(target) - init_target = safe_project_path(root, init_options.get("context_file")) - if init_target is not None: - paths.add(init_target) - resolved = resolve_target(root, state, init_options) - paths.add(resolved) - return sorted(rel(root, path) for path in paths if path.exists()) + return paths def development_command_lines(root: Path) -> list[str]: @@ -561,28 +498,29 @@ def read_json(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: - data = json.loads(path.read_text(encoding="utf-8")) + data = json.loads(path.read_text(encoding="utf-8-sig")) except (json.JSONDecodeError, OSError, UnicodeDecodeError): return {} return data if isinstance(data, dict) else {} def resolve_target(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> Path: - for value in ( - init_options.get("context_file"), - CONTEXT_FILES.get(default_integration(state) or ""), - "AGENTS.md", - ): + init_target = safe_context_file_path(root, init_options.get("context_file")) + if init_target is not None: + return init_target + for value in (CONTEXT_FILES.get(default_integration(state) or ""), CONTEXT_FILES.get(GENERIC_INTEGRATION)): target = safe_project_path(root, value) if target is not None: return target - return root / "AGENTS.md" + return root / CONTEXT_FILES[GENERIC_INTEGRATION] def safe_project_path(root: Path, value: Any) -> Path | None: if not isinstance(value, str) or not value.strip(): return None raw = Path(value.strip()) + if raw.is_absolute(): + return None candidate = raw if raw.is_absolute() else root / raw try: candidate.resolve(strict=False).relative_to(root.resolve()) @@ -591,6 +529,40 @@ def safe_project_path(root: Path, value: Any) -> Path | None: return candidate +def safe_context_file_path(root: Path, value: Any) -> Path | None: + target = safe_project_path(root, value) + if target is None: + return None + if is_known_context_target(root, target): + return target + if target.suffix.lower() not in CONTEXT_FILE_SUFFIXES: + return None + if not CONTEXT_FILE_NAME_PATTERN.search(target.name): + return None + if is_protected_custom_context_target(root, target): + return None + return target + + +def is_known_context_target(root: Path, target: Path) -> bool: + target_text = rel(root, target) + return target_text in set(CONTEXT_FILES.values()) + + +def is_protected_custom_context_target(root: Path, target: Path) -> bool: + target_text = rel(root, target) + parts = Path(target_text).parts + if not parts: + return True + if parts[0] in PROTECTED_CUSTOM_CONTEXT_ROOTS: + return True + if target.name in MCP_CONFIG_NAMES: + return True + if target.name.startswith(".env"): + return True + return any(part.lower() in {"secret", "secrets", "permission", "permissions"} for part in parts) + + def default_integration(state: dict[str, Any]) -> str | None: for key in ("default_integration", "integration"): value = state.get(key) @@ -622,23 +594,25 @@ def installed_integrations(state: dict[str, Any]) -> list[str]: return result -def render_projection(root: Path, target: Path, state: dict[str, Any], created_memory: bool) -> str: - source_text, source_label = governance_source(root, target) +def render_projection(root: Path, target: Path, state: dict[str, Any], init_options: dict[str, Any]) -> str: default_key = default_integration(state) or "unknown" installed = installed_integrations(state) style = projection_style(target) lines = [ - MARKER_START, - "## Repository Governance", - "- SSOT: this managed section.", - "- Framework: Repository Governance Framework.", + "# Project Governance Projection", + "- Projection: active agent platform target file.", + "- Domain: project-governance.", + "- Framework: Project Governance Projection Framework.", + "- Extension identity: repository-governance.", f"- Target: {rel(root, target)}", f"- Active integration: {default_key}", - f"- Refresh source: {source_label}", - f"- Cache: {MEMORY_PATH.as_posix()} ({'created' if created_memory else 'present'})", + "- Projection source: current repository scan.", "", "## Scope", - "- Repository Governance Framework", + "- Project-governance projection for the active agent platform target", + "- Review and report only the active agent platform target", + "- Legacy managed-section cleanup limited to non-active context files enumerated by `CONTEXT_FILES`", + "- Spec Kit extension routing and target resolution", "- top-level SSOT registry and routing", "- vertical SSOT discovery and read order", "- missing SSOT handling from repository evidence", @@ -646,7 +620,7 @@ def render_projection(root: Path, target: Path, state: dict[str, Any], created_m "- architecture methodology: owned by Architecture SSOT", "", "## Vertical SSOT Registry", - *section_or_default(source_text, ["## Vertical SSOT Registry"], vertical_ssot_registry_default()), + *vertical_ssot_registry_default(), "", "## Context", f"- Installed integrations: {', '.join(installed) if installed else 'none'}", @@ -661,57 +635,40 @@ def render_projection(root: Path, target: Path, state: dict[str, Any], created_m *capability_index_lines(root), "", "## Repository Evidence", - *section_or_default(source_text, ["## Repository Evidence"], repository_evidence_default()), + *repository_evidence_lines(root, state, init_options), "", "## Vertical SSOT Evidence", - *section_or_default(source_text, ["## Vertical SSOT Evidence"], vertical_ssot_evidence_default()), + *vertical_ssot_evidence_lines(root, state, init_options), "", "## Repository Areas", - *section_or_default(source_text, ["## Repository Areas"], repository_areas_default()), + *repository_area_lines(root), "", "## Directory Governance", - *section_or_default(source_text, ["## Directory Governance"], directory_governance_default()), + *directory_governance_default(), "", "## Development Commands", - *section_or_default(source_text, ["## Development Commands"], development_commands_default()), + *development_command_lines(root), "", "## Missing SSOT Handling", - *section_or_default(source_text, ["## Missing SSOT Handling"], missing_ssot_handling_default()), + *missing_ssot_handling_default(), "", "## Authority", - "1. Current user instruction", - "2. Safety and permission constraints", - "3. Active `SPECKIT GOVERNANCE` section", - "4. Vertical SSOT documents", - "5. Current repository code and configuration facts", - "6. Tests and CI results", - "7. Historical documents", - "8. Agent inference", + *authority_default(), "", "## Repository Workflow", - "- Classify task type before changing files.", - "- Route task to relevant vertical SSOT entries.", - "- Read: Repository Evidence", - "- Run: Development Commands", - "- Scope: active task only", - "- Preserve: user-authored edits", - "- Protected files: implementation, CI, MCP config, secrets, permissions, tool settings", - "- Protected-file writes: explicit user request only", - "- External writes: authorized target and action only", - "- Handoff: changed files, commands, validation, risks", + *repository_workflow_default(), "", "## Write Boundaries", - *section_or_default(source_text, ["## Write Boundaries"], write_boundary_default(style)), + *write_boundary_default(style), "", "## MCP And External Tools", - *section_or_default(source_text, ["## MCP And External Tools", "## MCP And External Tool Policy", "## MCP Policy"], mcp_default(style)), + *mcp_default(style), "", "## Skills", - *section_or_default(source_text, ["## Skills", "## Skill Usage Policy", "## Skill Contract"], skill_default(style)), + *skill_default(style), "", "## Handoff", - *section_or_default(source_text, ["## Handoff", "## Required Handoff Report", "## Validation"], handoff_default(style)), - MARKER_END, + *handoff_default(style), "", ] return "\n".join(lines) @@ -719,8 +676,7 @@ def render_projection(root: Path, target: Path, state: dict[str, Any], created_m def write_projection(target: Path, projection: str) -> str: existed = target.exists() - existing = target.read_text(encoding="utf-8-sig") if target.exists() else "" - updated = upsert_section(existing, projection) + updated = projection if target.suffix == ".mdc": updated = ensure_mdc_frontmatter(updated) target.parent.mkdir(parents=True, exist_ok=True) @@ -728,30 +684,8 @@ def write_projection(target: Path, projection: str) -> str: return "updated" if existed else "generated" -def upsert_section(content: str, projection: str) -> str: - start = content.find(MARKER_START) - end = content.find(MARKER_END, start if start != -1 else 0) - if start != -1 and end != -1 and end > start: - end += len(MARKER_END) - if end < len(content) and content[end] == "\r": - end += 1 - if end < len(content) and content[end] == "\n": - end += 1 - return content[:start] + projection + content[end:] - if content and not content.endswith("\n"): - content += "\n" - return content + ("\n" if content else "") + projection - - -def remove_stale_sections(root: Path, active: Path, init_options: dict[str, Any], state: dict[str, Any]) -> None: - paths = {root / "AGENTS.md"} - for value in CONTEXT_FILES.values(): - target = safe_project_path(root, value) - if target is not None: - paths.add(target) - init_target = safe_project_path(root, init_options.get("context_file")) - if init_target is not None: - paths.add(init_target) +def remove_stale_sections(root: Path, active: Path, init_options: dict[str, Any]) -> None: + paths = known_context_paths(root, init_options) for path in paths: if same_path(path, active): continue @@ -762,16 +696,10 @@ def remove_section(path: Path) -> None: if not path.exists(): return content = path.read_text(encoding="utf-8-sig") - start = content.find(MARKER_START) - end = content.find(MARKER_END, start if start != -1 else 0) - if start == -1 or end == -1 or end <= start: + managed_range = find_managed_range(content) + if managed_range is None: return - removal_end = end + len(MARKER_END) - if removal_end < len(content) and content[removal_end] == "\r": - removal_end += 1 - if removal_end < len(content) and content[removal_end] == "\n": - removal_end += 1 - removal_start = start + removal_start, removal_end = managed_range if removal_start > 1 and content[removal_start - 1] == "\n" and content[removal_start - 2] == "\n": removal_start -= 1 updated = normalize_newlines(content[:removal_start] + content[removal_end:]) @@ -781,44 +709,6 @@ def remove_section(path: Path) -> None: path.write_text(updated, encoding="utf-8") -def governance_source(root: Path, target: Path) -> tuple[str, str]: - managed = extract_managed_section(target) - if managed: - return managed, "active generated section" - memory = root / MEMORY_PATH - try: - return normalize_newlines(memory.read_text(encoding="utf-8-sig")), "initialization cache" - except (OSError, UnicodeDecodeError): - return "", "built-in defaults" - - -def section_or_default(source_text: str, headings: list[str], default: list[str]) -> list[str]: - for heading in headings: - section = extract_section_from_text(source_text, heading) - if section: - return section - return default - - -def repository_evidence_default() -> list[str]: - return ["- none captured"] - - -def vertical_ssot_evidence_default() -> list[str]: - return [ - "- Architecture evidence: none detected", - "- Engineering evidence: none detected", - "- Code Style evidence: none detected", - "- Directory Structure evidence: none detected", - "- Toolchain evidence: none detected", - "- Agent Harness evidence: none detected", - ] - - -def repository_areas_default() -> list[str]: - return ["- none detected"] - - def directory_governance_default() -> list[str]: return [ "- Responsibility: one primary purpose per directory.", @@ -836,10 +726,9 @@ def development_commands_default() -> list[str]: def vertical_ssot_registry_default() -> list[str]: return [ "- Architecture SSOT: owns architecture boundaries, interfaces, dependencies, runtime constraints, deployment assumptions, and scenario-level architecture decisions.", - "- Engineering SSOT: owns branch, version, release, CI/CD, and collaboration process.", + "- Engineering SSOT: owns branch, version, release, CI/CD, collaboration process, standard tools, command entrypoints, configuration templates, and execution constraints.", "- Code Style SSOT: owns naming, formatting, comments, error handling, logging, tests, and quality standards.", "- Directory Structure SSOT: owns directory layout, file placement, module organization, and configuration locations.", - "- Toolchain SSOT: owns standard tools, command entrypoints, configuration templates, and execution constraints.", "- Agent Harness SSOT: owns agent task boundaries, tool usage, permissions, audit, validation, and failure handling.", ] @@ -853,38 +742,51 @@ def missing_ssot_handling_default() -> list[str]: ] -def extract_section(path: Path, heading: str) -> list[str]: - try: - return extract_section_from_text(path.read_text(encoding="utf-8-sig"), heading) - except (OSError, UnicodeDecodeError): - return [] +def authority_default() -> list[str]: + return [ + "1. Current user instruction", + "2. Safety and permission constraints", + "3. Vertical SSOT documents", + "4. Current repository code and configuration facts", + "5. Active `PROJECT GOVERNANCE` projection", + "6. Tests and CI results", + "7. Historical documents", + "8. Agent inference", + "- Active projection is generated routing guidance and is subordinate to explicit vertical SSOT documents or source-backed repository facts on substantive conflicts.", + ] -def extract_managed_section(path: Path) -> str: - try: - content = normalize_newlines(path.read_text(encoding="utf-8-sig")) - except (OSError, UnicodeDecodeError): - return "" - start = content.find(MARKER_START) - end = content.find(MARKER_END, start if start != -1 else 0) - if start == -1 or end == -1 or end <= start: - return "" - return content[start + len(MARKER_START) : end] +def repository_workflow_default() -> list[str]: + return [ + "- Classify task type before changing files.", + "- Route task to relevant vertical SSOT entries.", + "- Read: Repository Evidence", + "- Run: Development Commands", + "- Scope: active task only", + "- Preserve: user-authored edits.", + "- Protected files: implementation paths, CI configuration, MCP configuration, secrets, permissions, tool settings, and arbitrary repository paths outside the resolved write surface.", + "- Protected-file writes: explicit user request, named matching contract or regression test, and passing validation commands.", + "- External writes: authorized target and action only.", + "- Handoff: changed files, commands, validation, risks.", + ] -def extract_section_from_text(text: str, heading: str) -> list[str]: - lines = normalize_newlines(text).splitlines() - capture = False - result: list[str] = [] - for line in lines: - if line.strip() == heading: - capture = True +def find_managed_range(content: str) -> tuple[int, int] | None: + for start_marker, end_marker in ( + (MARKER_START, MARKER_END), + (LEGACY_MARKER_START, LEGACY_MARKER_END), + ): + start = content.find(start_marker) + end = content.find(end_marker, start if start != -1 else 0) + if start == -1 or end == -1 or end <= start: continue - if capture and line.startswith("## "): - break - if capture and line.strip(): - result.append(line) - return result + removal_end = end + len(end_marker) + if removal_end < len(content) and content[removal_end] == "\r": + removal_end += 1 + if removal_end < len(content) and content[removal_end] == "\n": + removal_end += 1 + return start, removal_end + return None def projection_style(path: Path) -> str: @@ -908,24 +810,31 @@ def write_boundary_default(style: str) -> list[str]: if style == "rule": return [ "- Stay inside the active task scope.", - "- Preserve user-authored edits.", - "- Preserve managed markers verbatim: `` and ``.", + "- The active agent platform target is generated output and may be overwritten.", + "- Legacy managed-section cleanup is limited to non-active context files enumerated by `CONTEXT_FILES`.", + "- Protected-file writes require explicit user request, a named matching contract or regression test, and passing validation commands.", ] return [ - "- Keep edits inside the active task scope and preserve user changes.", - "- Preserve managed markers verbatim: `` and ``.", + "- Keep edits inside the active task scope.", + "- The active agent platform target is generated output and may be overwritten.", + "- Legacy managed-section cleanup is limited to non-active context files enumerated by `CONTEXT_FILES`.", + "- Protected-file writes require explicit user request, a named matching contract or regression test, and passing validation commands.", ] def mcp_default(style: str) -> list[str]: - return ["- Read-only unless the user authorizes mutation.", "- External writes: target, action, expected effect."] + return [ + "- Read-only unless the user authorizes mutation.", + "- Mutation: explicit user intent with target, action, and expected effect.", + "- External writes: target, action, expected effect.", + ] def skill_default(style: str) -> list[str]: return [ "- Use active skill `SKILL.md`.", "- Write scope: declared skill paths only.", - "- Repository-local skill specs should declare purpose, trigger, allowed read paths, allowed write paths, forbidden paths, outputs, and validation command.", + "- Repository-local skill specs should declare name, description or trigger, allowed read paths, allowed write paths, forbidden paths, outputs, and validation command.", ] @@ -959,6 +868,7 @@ def skill_capability_lines(root: Path) -> list[str]: [ f"- Repository capability: {name}", f" - Scenario: {description}", + *([f" - Trigger: {fields['trigger']}"] if fields.get("trigger") else []), f" - Source: `{path_text}`.", " - Runtime action: read matching skill before planning or editing.", ] @@ -982,7 +892,7 @@ def skill_frontmatter(path: Path) -> dict[str, str]: continue key = key.strip() value = value.strip().strip("'\"") - if key in {"name", "description"} and value: + if key in {"name", "description", "trigger"} and value: fields[key] = value return fields @@ -1010,7 +920,7 @@ def agent_adapter_lines(root: Path, target: Path, integration: str) -> list[str] lines = [ "- Repository Capability layer: abstract repository-local abilities and evidence independent of agent runtime.", "- Agent Adapter layer: translate repository capabilities into platform-specific discovery and activation rules.", - "- Platform Projection layer: render the active context target without claiming unavailable platform support.", + "- Platform Projection layer: render the active agent platform target without claiming unavailable platform support.", f"- Active integration: {integration}", f"- Context target: {rel(root, target)}", ] @@ -1032,11 +942,10 @@ def agent_adapter_lines(root: Path, target: Path, integration: str) -> list[str] def scan_mcp_configs(root: Path) -> list[str]: - names = {".mcp.json", "mcp.json", "mcp.yml", "mcp.yaml", "mcp.config.json"} return sorted( rel(root, path) for path in root.rglob("*") - if path.is_file() and not ignored(path) and path.name in names + if path.is_file() and not ignored(path) and path.name in MCP_CONFIG_NAMES ) diff --git a/extensions/repository-governance/templates/repository-governance-template.md b/extensions/repository-governance/templates/repository-governance-template.md index 197966c823..0a85a01990 100644 --- a/extensions/repository-governance/templates/repository-governance-template.md +++ b/extensions/repository-governance/templates/repository-governance-template.md @@ -1,4 +1,4 @@ -# Repository Governance Framework Cache +# Project Governance Projection Template ` and `` -- Protected files: implementation, CI, MCP config, secrets, permissions, tool settings -- Protected-file writes: explicit user request only +- Active agent platform target: generated output, overwritten on generation. +- Legacy managed-section cleanup: non-active context files enumerated by `CONTEXT_FILES`. +- Protected files: implementation paths, CI configuration, MCP configuration, secrets, permissions, tool settings, and arbitrary repository paths outside the resolved write surface +- Protected-file writes: explicit user request, named matching contract or regression test, and passing validation commands ## Agent Platform Adapter - Repository Capability: abstract repository-local skill and MCP evidence into scenario capabilities. -- Spec Kit Agent Adapter: map integration metadata to the active agent context target and supported discovery behavior. -- Platform Projection: emit only the rules the active agent context file can safely apply. +- Spec Kit Agent Adapter: map integration metadata to the active agent platform target and supported discovery behavior. +- Platform Projection: emit only the rules the active agent platform target can safely apply. ## Capability Index @@ -91,7 +104,7 @@ Sync Impact Report ## MCP Policy - Default: read-only -- Mutation: explicit user intent +- Mutation: explicit user intent with target, action, and expected effect. - Runtime discovery: enumerate available servers, resources, and tools before use. - Config candidates: evidence only, not proof of active tools. - External writes: target, action, result From 33c878078876c89b1ba76b9602697e1e3e05e850 Mon Sep 17 00:00:00 2001 From: bigsmartben <30429295+bigsmartben@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:04:28 +0800 Subject: [PATCH 2/3] Update repository governance smoke expectations --- .github/workflows/community-smoke.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/community-smoke.yml b/.github/workflows/community-smoke.yml index a50296c44f..33884d6ac7 100644 --- a/.github/workflows/community-smoke.yml +++ b/.github/workflows/community-smoke.yml @@ -119,7 +119,7 @@ jobs: test -f .claude/skills/speckit-intake-visual-design/SKILL.md test -f .claude/skills/speckit-intake-prd/SKILL.md test -f .claude/skills/speckit-intake-test-cases/SKILL.md - test -f .claude/skills/speckit-repository-governance-refresh/SKILL.md + test -f .claude/skills/speckit-repository-governance-generate/SKILL.md test -f .claude/skills/speckit-specify/SKILL.md test -f .claude/skills/speckit-clarify/SKILL.md test -f .claude/skills/speckit-checklist/SKILL.md @@ -161,7 +161,7 @@ jobs: test "$(find .claude/skills -maxdepth 1 -type d -name 'speckit-intake-*' | wc -l)" -eq 0 ;; repository-governance) - test ! -d .claude/skills/speckit-repository-governance-refresh + test ! -d .claude/skills/speckit-repository-governance-generate ;; esac done @@ -197,8 +197,8 @@ jobs: grep -q "visual design intake" .claude/skills/speckit-intake-visual-design/SKILL.md ;; repository-governance) - test -f .claude/skills/speckit-repository-governance-refresh/SKILL.md - grep -q "Repository Governance Generate/Update" .claude/skills/speckit-repository-governance-refresh/SKILL.md + test -f .claude/skills/speckit-repository-governance-generate/SKILL.md + grep -q "Project Governance Projection Generation" .claude/skills/speckit-repository-governance-generate/SKILL.md ;; esac done From dd8fd05d8f8bda82d62955ae497ff939d8e2c510 Mon Sep 17 00:00:00 2001 From: bigsmartben <30429295+bigsmartben@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:46:35 +0800 Subject: [PATCH 3/3] Align tests with repository governance generate command --- tests/integrations/community_defaults.py | 2 +- tests/test_presets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integrations/community_defaults.py b/tests/integrations/community_defaults.py index 2053727b1b..5b36b97f24 100644 --- a/tests/integrations/community_defaults.py +++ b/tests/integrations/community_defaults.py @@ -36,7 +36,7 @@ "speckit.preview.mid-html", "speckit.preview.high-md", "speckit.preview.high-html", - "speckit.repository-governance.refresh", + "speckit.repository-governance.generate", ) DEFAULT_PRESET_ID = "workflow-preset" DEFAULT_PRESET_COMMANDS = ( diff --git a/tests/test_presets.py b/tests/test_presets.py index 037981742f..da1825b72f 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -4657,7 +4657,7 @@ def test_community_smoke_checks_wheel_assets_and_extension_dev_reinstall(self): assert f'test -f ".claude/skills/$preview_skill/SKILL.md"' in verify_run assert 'grep -q "evidence-backed" ".claude/skills/$preview_skill/SKILL.md"' in verify_run assert ( - "test ! -d .claude/skills/speckit-repository-governance-refresh" + "test ! -d .claude/skills/speckit-repository-governance-generate" in verify_run ) assert ( @@ -4686,7 +4686,7 @@ def test_community_smoke_checks_wheel_assets_and_extension_dev_reinstall(self): in verify_run ) assert ( - 'test -f .claude/skills/speckit-repository-governance-refresh/SKILL.md' + 'test -f .claude/skills/speckit-repository-governance-generate/SKILL.md' in verify_run )