From 958bc063888632cfa55c4c386db09d2fa6cd0d54 Mon Sep 17 00:00:00 2001 From: bigsmartben <30429295+bigsmartben@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:13:11 +0800 Subject: [PATCH 1/2] Update bundled repository governance snapshot Assisted-by: OpenAI Codex (GPT-5, autonomous) --- CHANGELOG.md | 1 + extensions/repository-governance/CHANGELOG.md | 4 + extensions/repository-governance/README.md | 51 +- .../speckit.repository-governance.generate.md | 47 +- .../scripts/generate_repository_governance.py | 496 +++++++++++------- .../repository-governance-template.md | 80 +-- 6 files changed, 401 insertions(+), 278 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fbf0ac8cc..7eae2e9386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Update Architecture Workflow extension to v1.2.2 with full-workflow commands and readiness validators. - Update Spec Kit Preview extension to v1.2.0 with structured IR-backed mid-fidelity previews. - Update Intake extension to v0.1.3 with HTML SSOT validation and bounded visual inference gates. +- Update bundled Repository Governance extension with SSOT index projections, Zed target mapping, and bounded evidence scanning. - Update bundled Repository Governance extension to v2.0.2, including the `/speckit.repository-governance.refresh` command, `.specify/memory/repository-governance.md` internal cache, and default bundled installation behavior. - Update bundled Workflow Preset documentation for v1.3.2 Final Code Review task generation and structured code review receipts. diff --git a/extensions/repository-governance/CHANGELOG.md b/extensions/repository-governance/CHANGELOG.md index e51c8cc2d6..40d58b3fc9 100644 --- a/extensions/repository-governance/CHANGELOG.md +++ b/extensions/repository-governance/CHANGELOG.md @@ -12,6 +12,10 @@ - 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`. +- Separate generator operations from generated target-file guidance, require clarification for missing vertical SSOT, add explicit Zed target mapping, and bound repository-local skill projection. +- Collapse generated target content into SSOT index entries and remove expanded repository evidence, repository area, skill capability, and development command lists from active targets. +- Keep SSOT index source refs complete while narrowing Agent Harness SSOT refs to explicit governance entrypoints and treating ordinary `SKILL.md` files as evidence only. +- Bound route evidence scanning to cached project files and text candidates, narrow Directory Structure fallback wording, and treat extension metadata as Engineering SSOT only for this extension source repository. ## [3.0.0] - 2026-06-25 diff --git a/extensions/repository-governance/README.md b/extensions/repository-governance/README.md index 667642caa3..d7c2261463 100644 --- a/extensions/repository-governance/README.md +++ b/extensions/repository-governance/README.md @@ -6,29 +6,32 @@ Generate project-governance projections for the active Spec Kit agent platform t - Active agent platform target from safe `context_file` override or Spec Kit integration metadata. - Generated `PROJECT GOVERNANCE` projection file. +- Target file content is a runtime project-governance entrypoint for coding agents, not a generator operations manual. ## Scope - 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 and routing input. -- Structure generated instructions with Copilot-like repository-wide, path-scope, and agent-harness layers. +- Scan repository areas as bounded evidence for missing-SSOT fallback. +- Capture repository facts as bounded evidence for gap handling and CLI reporting. +- Structure generated instructions as repository-wide, path-scope, and agent-harness guidance. - Project agent platform adapter rules from Spec Kit integration metadata. -- Build a scenario capability index for repository-local skills and MCP-backed external tool evidence. -- Analyze repository areas to depth 2 only. -- Include hidden and cache directories in repository area governance. -- Enforce one primary responsibility per directory. +- Index only explicit SSOT content entrypoints in the generated SSOT index. +- Report repository-local skills and MCP config files as evidence candidates only unless an explicit Agent Harness SSOT source names them. +- Analyze repository areas to depth 2 only for evidence and CLI summaries. +- Use Directory Structure fallback only when the Directory Structure SSOT is missing and task scope is explicit. - Overwrite the active agent platform target on generation. - Do not generate Copilot `.github/instructions/*.instructions.md` companion files. - 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`. +- Missing vertical SSOT is reported as a clarification need for governed changes; generated output must not invent repository policy from descriptive repository evidence. +- Do not project full repository fact inventories, repository area lists, skill capability lists, or development command lists into the active target. ## Install ```bash -specify extension add repository-governance --from https://github.com/bigsmartben/spec-kit-agent-governance/archive/refs/tags/v3.0.1.zip +specify extension add repository-governance --from https://github.com/bigsmartben/spec-kit-agent-governance/archive/42f0cb04891a29a4c05973b7fa5e746a7e0f8dd4.zip ``` Local development: @@ -62,34 +65,38 @@ uv run python tools/build_repository_governance_zip.py - `scripts/generate_repository_governance.py` - `templates/repository-governance-template.md` -## Vertical SSOT Coverage +## SSOT Index -- 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, 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. -- Agent Harness SSOT evidence from active agent context files, Spec Kit metadata, repository-local skills, and MCP config candidates. +- Architecture SSOT index: status, source_refs, and gap code. +- Engineering SSOT index: status, source_refs, and gap code. +- Code Style SSOT index: status, source_refs, and gap code. +- Directory Structure SSOT index: status, source_refs, and gap code. +- Agent Harness SSOT index: status, source_refs, and gap code. +- Repository-level and nested project evidence is scanned through explicit path families with bounded parent depth, but only explicit SSOT content entrypoints become SSOT index source_refs. +- `extension.yml` and `.extensionignore` are Engineering SSOT refs only in this extension source repository; other projects report them as evidence. ## Instruction Layers - Repository-wide instructions summarize authority, active-target scope, write boundaries, validation commands, and handoff expectations. +- SSOT index maps Architecture, Engineering, Code Style, Directory Structure, and Agent Harness categories to source_refs and gap codes. - SSOT routing maps task types and path families to Architecture, Engineering, Code Style, Directory Structure, and Agent Harness SSOT entries. - Path and task scope rules keep generated guidance deterministic without expanding the write surface. - Agent harness instructions cover adapter behavior, repository-local skills, MCP discovery, external tools, permissions, and failure handling. -- Copilot's instruction model is a structural reference only; this extension still emits one active target file. +- The extension emits one active target file and does not generate platform companion instruction files. -## Evidence Coverage +## Evidence Discovery -- 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. +- Repository facts are scanned from README files, project docs, repository policy files, feature specs, source/test paths, nested manifests, and runtime/build configuration. +- Development command sources are scanned from package scripts or Python/uv test conventions. +- Scanned facts feed missing-SSOT handling and CLI evidence summaries; they are not projected as SSOT source_refs or full content lists into the active target unless they are explicit SSOT content entrypoints. ## 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 active agent platform target and supported discovery behavior. -- Platform Projection: emit only rules the active agent platform target can safely apply. +- Repository capability layer: source-backed repository-local skills and MCP candidates only. +- Agent adapter layer: use explicit integration support when available; otherwise use generic fallback rules. +- Platform projection layer: apply only rules supported by the active target file. -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. +Repository-local `SKILL.md` files are reported as evidence and read when they match the task; they are not Agent Harness SSOT source_refs unless an explicit Agent Harness SSOT source names them. MCP config files are reported as candidates and evidence only; they are not SSOT source_refs, and active servers, resources, and tools must be enumerated from the agent platform runtime before use. ## Verify diff --git a/extensions/repository-governance/commands/speckit.repository-governance.generate.md b/extensions/repository-governance/commands/speckit.repository-governance.generate.md index 8e201afc2c..2711146462 100644 --- a/extensions/repository-governance/commands/speckit.repository-governance.generate.md +++ b/extensions/repository-governance/commands/speckit.repository-governance.generate.md @@ -12,57 +12,24 @@ $ARGUMENTS - Active agent platform target. - Generated `PROJECT GOVERNANCE` projection file. -- Copilot-like instruction layers inside the single active target. +- Project-governance instructions for the active coding-agent platform. ## 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. Reference the Copilot custom-instructions model for projection structure, but emit only the active agent platform target. - - repository-wide instructions - - path and task scope routing - - agent harness guidance - - no `.github/instructions/*.instructions.md` companion files -5. Distill detected repository areas into action rules. - - depth: 2 - - include hidden and cache directories -6. Capture repository facts from the current repository state as vertical SSOT evidence and SSOT routing input. - - 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 -7. Resolve deterministic SSOT routing rules by task type and path family. - - Architecture SSOT for source, route, API, runtime, infra, dependency-boundary, and architecture decision work - - Engineering SSOT for build, release, CI, manifest, lockfile, command, template, package, and runtime configuration work - - Code Style SSOT for formatting, linting, typing, testing, logging, comments, naming, and error-handling work - - Directory Structure SSOT for new files, moved files, generated assets, and directory responsibility work - - Agent Harness SSOT for agent instructions, permissions, MCP, external tools, skills, validation, and failure handling -8. 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 -9. 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 -10. Run: +2. Run the deterministic projection helper: ```bash uv run python .specify/extensions/repository-governance/scripts/generate_repository_governance.py ``` +3. Review only the reported active agent platform target. +4. Treat repository evidence in the report as descriptive source-backed context, not as generated implementation work. ## Report - active agent platform target - generated or updated - review target -- captured evidence from the current repository state +- SSOT content index summary +- descriptive evidence scan summary +- unresolved risks or clarification needs diff --git a/extensions/repository-governance/scripts/generate_repository_governance.py b/extensions/repository-governance/scripts/generate_repository_governance.py index ca58dece47..5d2b421ab5 100755 --- a/extensions/repository-governance/scripts/generate_repository_governance.py +++ b/extensions/repository-governance/scripts/generate_repository_governance.py @@ -6,6 +6,7 @@ import json import re import sys +from functools import lru_cache from pathlib import Path from typing import Any @@ -63,6 +64,7 @@ "trae": ".trae/rules/project_rules.md", "vibe": "AGENTS.md", "windsurf": ".windsurf/rules/specify-rules.md", + "zed": "AGENTS.md", } README_FILES = ["README.md", "README.markdown", "README.txt"] @@ -83,6 +85,8 @@ "pom.xml", "build.gradle", "build.gradle.kts", + "settings.gradle", + "settings.gradle.kts", "composer.json", "requirements.txt", "setup.py", @@ -157,7 +161,6 @@ "biome.json", "ruff.toml", ".ruff.toml", - "pyproject.toml", "mypy.ini", "pytest.ini", "tox.ini", @@ -179,7 +182,100 @@ "netlify.toml", "fly.toml", ] +ROUTE_BASE_DIRS = ("src", "app", "lib", "services", "packages") +ROUTE_TEXT_SUFFIXES = { + ".cjs", + ".cs", + ".go", + ".java", + ".js", + ".jsx", + ".kt", + ".mjs", + ".php", + ".py", + ".rb", + ".rs", + ".scala", + ".ts", + ".tsx", +} +MAX_ROUTE_FILE_BYTES = 512_000 SPEC_KIT_METADATA = [".specify/integration.json", ".specify/init-options.json", ".specify/extensions.yml"] +EVIDENCE_SCAN_MAX_PARENT_DEPTH = 3 +SUPPORTED_ADAPTERS = {"codex", "zed"} +ARCHITECTURE_SSOT_FILES = ["ARCHITECTURE.md", "architecture.md"] +ARCHITECTURE_SSOT_GLOBS = [ + "docs/architecture*.md", + "docs/**/architecture*.md", + "adr/*.md", + "adr/**/*.md", + "adrs/*.md", + "adrs/**/*.md", +] +ENGINEERING_SSOT_FILES = [ + "CONTRIBUTING.md", + "contributing.md", + "DEVELOPMENT.md", + "development.md", + "RELEASE.md", + "release.md", + "VERSION", + "docs/engineering.md", + "docs/development.md", + "docs/release.md", + "docs/releases.md", + "docs/ci.md", + "docs/tooling.md", + "docs/commands.md", + *EXTENSION_CONTRACT_FILES, +] +EXTENSION_ENGINEERING_SSOT_FILES = ["extension.yml", ".extensionignore"] +DIRECTORY_STRUCTURE_SSOT_FILES = [ + "STRUCTURE.md", + "structure.md", + "LAYOUT.md", + "layout.md", + "docs/structure.md", + "docs/directory-structure.md", + "docs/repository-structure.md", + "docs/layout.md", +] +DIRECTORY_STRUCTURE_SSOT_GLOBS = [ + "docs/*structure*.md", + "docs/**/structure*.md", + "docs/**/directory-structure*.md", + "docs/**/repository-structure*.md", +] +AGENT_HARNESS_SSOT_FILES = [ + "AGENT_HARNESS.md", + "agent-harness.md", + "AGENT_GOVERNANCE.md", + "agent-governance.md", + "docs/agent-harness.md", + "docs/agent-governance.md", + "docs/agents.md", +] +AGENT_HARNESS_SSOT_GLOBS = [ + "docs/*agent*harness*.md", + "docs/**/*agent*harness*.md", + "docs/*agent*governance*.md", + "docs/**/*agent*governance*.md", +] +CODE_STYLE_SSOT_FILES = [ + ".editorconfig", + "STYLE.md", + "style.md", + "CODE_STYLE.md", + "code-style.md", + "docs/style.md", + "docs/code-style.md", + "docs/coding-style.md", +] +CODE_STYLE_SSOT_GLOBS = [ + "docs/*style*.md", + "docs/**/*style*.md", +] def main() -> int: @@ -192,6 +288,7 @@ def main() -> int: init_options = read_json(root / INIT_OPTIONS_JSON) target = resolve_target(root, state, init_options) projection = render_projection(root, target, state, init_options) + index_summary = ssot_index_summary(root, state, init_options) evidence_summary = repository_evidence_summary(root, state, init_options) action = write_projection(target, projection) remove_stale_sections(root, target, init_options) @@ -199,10 +296,21 @@ def main() -> int: print(f"Target agent platform file: {rel(root, target)}") print(f"Project-governance projection: {action}") print(f"Review target: {rel(root, target)}") - print(f"Repository evidence: {evidence_summary}") + print(f"SSOT index summary: {index_summary}") + print(f"Evidence scan summary: {evidence_summary}") return 0 +def ssot_index_summary(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> str: + entries = ssot_index_data(root, state, init_options) + parts = [] + for name, refs in entries: + status = "indexed" if refs else "missing" + gap = "none" if refs else ssot_gap_code(name) + parts.append(f"{name}: {status} ({len(refs)} refs, gap: {gap})") + return "; ".join(parts) + + 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] @@ -236,64 +344,46 @@ def repository_evidence_lines(root: Path, state: dict[str, Any], init_options: d return lines -def vertical_ssot_evidence_lines(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> list[str]: - return [ - evidence_line("Architecture evidence", architecture_evidence(root)), - 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("Agent Harness evidence", agent_harness_evidence(root, init_options, state)), - ] - - -def architecture_evidence(root: Path) -> list[str]: +def architecture_ssot_refs(root: Path) -> list[str]: return unique_ordered( [ - *source_paths(root), - *route_files(root), - *api_contract_paths(root), - *existing_dirs(root, ARCHITECTURE_DIRS), + *existing_paths(root, ARCHITECTURE_SSOT_FILES), + *bounded_glob_files(root, ARCHITECTURE_SSOT_GLOBS), ] ) -def engineering_evidence(root: Path) -> list[str]: +def engineering_ssot_refs(root: Path) -> list[str]: + refs = existing_paths(root, ENGINEERING_SSOT_FILES) + if is_repository_governance_extension_source(root): + refs.extend(existing_paths(root, EXTENSION_ENGINEERING_SSOT_FILES)) + return unique_ordered(refs) + + +def code_style_ssot_refs(root: Path) -> list[str]: return unique_ordered( [ - *directory_files(root, ".github/workflows"), - *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), + *existing_paths(root, CODE_STYLE_SSOT_FILES), + *bounded_glob_files(root, CODE_STYLE_SSOT_GLOBS), ] ) -def code_style_evidence(root: Path) -> list[str]: +def directory_structure_ssot_refs(root: Path) -> list[str]: return unique_ordered( [ - *existing_paths(root, CODE_STYLE_FILES), - *existing_top_level_globs(root, ["*.prettierrc.*", "eslint.config.*", "jest.config.*", "playwright.config.*", "vitest.config.*"]), - *test_paths(root), + *existing_paths(root, DIRECTORY_STRUCTURE_SSOT_FILES), + *bounded_glob_files(root, DIRECTORY_STRUCTURE_SSOT_GLOBS), ] ) -def directory_structure_evidence(root: Path) -> list[str]: - return repository_area_paths(root) - - -def agent_harness_evidence(root: Path, init_options: dict[str, Any], state: dict[str, Any]) -> list[str]: +def agent_harness_ssot_refs(root: Path, init_options: dict[str, Any], state: dict[str, Any]) -> list[str]: return unique_ordered( [ *existing_context_files(root, init_options, state), - *existing_paths(root, SPEC_KIT_METADATA), - *scan_skills(root), - *scan_mcp_configs(root), + *existing_paths(root, AGENT_HARNESS_SSOT_FILES), + *bounded_glob_files(root, AGENT_HARNESS_SSOT_GLOBS), ] ) @@ -306,6 +396,12 @@ def format_values(values: list[str]) -> str: return ", ".join(f"`{value}`" for value in values) if values else "none detected" +def format_index_refs(values: list[str]) -> str: + if not values: + return "none detected" + return ", ".join(f"`{value}`" for value in values) + + def unique_ordered(values: list[str]) -> list[str]: seen: set[str] = set() result: list[str] = [] @@ -318,11 +414,81 @@ def unique_ordered(values: list[str]) -> list[str]: def existing_paths(root: Path, names: list[str]) -> list[str]: - return [name for name in names if (root / name).is_file()] + return bounded_named_files(root, names) def existing_dirs(root: Path, names: list[str]) -> list[str]: - return [f"{name}/" for name in names if (root / name).is_dir()] + return bounded_named_dirs(root, names) + + +def bounded_named_files(root: Path, names: list[str], max_parent_depth: int = EVIDENCE_SCAN_MAX_PARENT_DEPTH) -> list[str]: + matches: list[str] = [] + files = project_files(root) + for name in names: + for path in files: + relative = path.relative_to(root) + path_text = relative.as_posix() + if (path_text == name or path.name == name) and len(relative.parts) - 1 <= max_parent_depth: + matches.append(rel(root, path)) + return unique_ordered(matches) + + +def bounded_named_dirs(root: Path, names: list[str], max_parent_depth: int = EVIDENCE_SCAN_MAX_PARENT_DEPTH) -> list[str]: + matches: list[str] = [] + dirs = project_dirs(root) + for name in names: + for path in dirs: + if path.name != name: + continue + relative = path.relative_to(root) + if len(relative.parts) - 1 <= max_parent_depth: + matches.append(f"{rel(root, path)}/") + return unique_ordered(matches) + + +def bounded_glob_files(root: Path, patterns: list[str], max_parent_depth: int = EVIDENCE_SCAN_MAX_PARENT_DEPTH) -> list[str]: + matches: list[str] = [] + for path in project_files(root): + relative = path.relative_to(root) + if len(relative.parts) - 1 > max_parent_depth: + continue + if any(relative.match(pattern) for pattern in patterns): + matches.append(rel(root, path)) + return unique_ordered(matches) + + +def project_files(root: Path) -> tuple[Path, ...]: + files, _dirs = project_walk(root) + return files + + +def project_dirs(root: Path) -> tuple[Path, ...]: + _files, dirs = project_walk(root) + return dirs + + +@lru_cache(maxsize=16) +def project_walk(root: Path) -> tuple[tuple[Path, ...], tuple[Path, ...]]: + files: list[Path] = [] + dirs: list[Path] = [] + stack = [root] + while stack: + base = stack.pop() + try: + children = sorted(base.iterdir(), key=lambda path: path.as_posix()) + except OSError: + continue + next_dirs: list[Path] = [] + for child in children: + if ignored(child): + continue + if child.is_dir(): + dirs.append(child) + next_dirs.append(child) + elif child.is_file(): + files.append(child) + stack.extend(reversed(next_dirs)) + return tuple(files), tuple(dirs) def existing_top_level_globs(root: Path, patterns: list[str]) -> list[str]: @@ -377,6 +543,10 @@ def extension_asset_paths(root: Path) -> list[str]: return unique_ordered([*existing_paths(root, EXTENSION_ASSET_FILES), *existing_dirs(root, EXTENSION_ASSET_DIRS)]) +def is_repository_governance_extension_source(root: Path) -> bool: + return all((root / path).is_file() for path in ("extension.yml", *EXTENSION_CONTRACT_FILES)) + + def runtime_config_paths(root: Path) -> list[str]: return unique_ordered( [ @@ -400,24 +570,35 @@ def route_files(root: Path) -> list[str]: r"(@app\.route|APIRouter|router\.|Route::|express\(|fastify\(|app\.(get|post|put|delete|patch)\()", re.IGNORECASE, ) - for base in (root / name for name in ("src", "app", "lib", "services", "packages")): - if not base.is_dir(): + for path in project_files(root): + if not route_file_candidate(root, path): continue - for path in sorted(base.rglob("*")): - if not path.is_file() or ignored(path): - continue - if route_name_pattern.search(path.name): - result.append(rel(root, path)) - continue - try: - text = path.read_text(encoding="utf-8", errors="ignore") - except OSError: - continue - if route_content_pattern.search(text): - result.append(rel(root, path)) + if route_name_pattern.search(path.name): + result.append(rel(root, path)) + continue + try: + text = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + continue + if route_content_pattern.search(text): + result.append(rel(root, path)) return unique_ordered(result) +def route_file_candidate(root: Path, path: Path) -> bool: + relative = path.relative_to(root) + if not relative.parts or relative.parts[0] not in ROUTE_BASE_DIRS: + return False + if len(relative.parts) - 1 > EVIDENCE_SCAN_MAX_PARENT_DEPTH: + return False + if path.suffix.lower() not in ROUTE_TEXT_SUFFIXES: + return False + try: + return path.stat().st_size <= MAX_ROUTE_FILE_BYTES + except OSError: + return False + + def repository_area_lines(root: Path) -> list[str]: lines = [] for area in repository_area_paths(root): @@ -610,47 +791,37 @@ def render_projection(root: Path, target: Path, state: dict[str, Any], init_opti "", "## Repository-Wide Instructions", f"- {style_lead(style)}", - "- Generate and review only the resolved active agent platform target.", - "- Use the Copilot instruction model for layering only; do not emit Copilot path-specific companion files.", - "- Keep instructions short, self-contained, and free of conflicting rules.", - "- Project-governance projection for the active agent platform target.", - "- Legacy managed-section cleanup limited to non-active context files enumerated by `CONTEXT_FILES`.", + "- Treat this file as the active project-governance entrypoint for coding-agent work in this repository.", + "- Keep task reasoning grounded in source-backed repository facts, matched SSOT routes, and explicit user instructions.", + "- Keep edits scoped to the active task and matched path family.", "- architecture methodology: owned by Architecture SSOT.", "", "### Context", f"- Installed integrations: {', '.join(installed) if installed else 'none'}", - f"- Skills: {', '.join(scan_skills(root)) or 'none'}", - f"- MCP configs: {', '.join(scan_mcp_configs(root)) or 'none'}", + f"- Repository-local skill evidence: {evidence_count(scan_skills(root))}", + f"- MCP config candidates: {evidence_count(scan_mcp_configs(root))}", f"- Extensions config: .specify/extensions.yml ({extensions_status(root)})", "", "### Authority", *authority_default(), "", - "## SSOT Routing", + "## SSOT Index", *vertical_ssot_registry_default(), "", - *vertical_ssot_routing_lines(root, state, init_options), + *ssot_index_lines(root, state, init_options), "", "### Missing SSOT Handling", *missing_ssot_handling_default(), "", - "## Repository Evidence", - *repository_evidence_lines(root, state, init_options), - "", "## Path And Task Scope Rules", *task_scope_rules_default(), "", - "### Repository Areas", - *repository_area_lines(root), - "", - "### Directory Governance", - *directory_governance_default(), + "### Directory Structure Fallback", + *directory_structure_fallback_default(), "", "## Agent Harness", *agent_adapter_lines(root, target, default_key), "", - *capability_index_lines(root), - "", *mcp_default(style), "", *skill_default(style), @@ -660,9 +831,6 @@ def render_projection(root: Path, target: Path, state: dict[str, Any], init_opti "## Write Boundaries", *write_boundary_default(style), "", - "## Development Commands", - *development_command_lines(root), - "", "## Handoff", *handoff_default(style), "", @@ -705,13 +873,12 @@ def remove_section(path: Path) -> None: path.write_text(updated, encoding="utf-8") -def directory_governance_default() -> list[str]: +def directory_structure_fallback_default() -> list[str]: return [ - "- Responsibility: one primary purpose per directory.", - "- Depth: 2.", - "- Coverage: include visible, hidden, generated, cache, config/env, tool, and agent directories.", - "- Mixed concerns: follow existing repo convention or split responsibility.", - "- Change impact: review linked code, tests, docs, config/env, data, assets, generated files, and tool outputs; update only when in scope and authorized.", + "- Use only when Directory Structure SSOT is missing and the task scope is explicit.", + "- Treat scanned repository areas as descriptive context, not as approved directory policy.", + "- Keep new or moved files aligned with existing nearby conventions unless the user supplies a different target.", + "- Record `NEEDS_CLARIFICATION:DIRECTORY_STRUCTURE` in handoff when placement is ambiguous.", ] @@ -740,28 +907,44 @@ def vertical_ssot_registry_default() -> list[str]: ] -def vertical_ssot_routing_lines(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> list[str]: - evidence = vertical_ssot_evidence_lines(root, state, init_options) +def ssot_index_lines(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> list[str]: + lines: list[str] = [] + for name, refs in ssot_index_data(root, state, init_options): + lines.extend(ssot_index_entry(name, refs)) + return lines + + +def ssot_index_data(root: Path, state: dict[str, Any], init_options: dict[str, Any]) -> list[tuple[str, list[str]]]: + return [ + ("Architecture", architecture_ssot_refs(root)), + ("Engineering", engineering_ssot_refs(root)), + ("Code Style", code_style_ssot_refs(root)), + ("Directory Structure", directory_structure_ssot_refs(root)), + ("Agent Harness", agent_harness_ssot_refs(root, init_options, state)), + ] + + +def ssot_index_entry(name: str, refs: list[str]) -> list[str]: + status = "indexed" if refs else "missing" + gap = "none" if refs else ssot_gap_code(name) return [ - "- Architecture SSOT route: use for source roots, route files, API contracts, runtime constraints, deployment assumptions, and architecture decisions.", - evidence[0], - "- Engineering SSOT route: use for branch, version, release, CI/CD, command entrypoints, manifests, lockfiles, build config, runtime config, and extension packaging.", - evidence[1], - "- Code Style SSOT route: use for naming, formatting, comments, error handling, logging, tests, linting, typing, and quality standards.", - evidence[2], - "- Directory Structure SSOT route: use for directory layout, file placement, module organization, configuration locations, and generated artifact placement.", - evidence[3], - "- Agent Harness SSOT route: use for agent task boundaries, tool usage, permissions, audit, validation, failure handling, skills, and MCP config candidates.", - evidence[4], + f"- {name} SSOT index:", + f" - status: {status}", + f" - source_refs: {format_index_refs(refs)}", + f" - gap: {gap}", ] +def ssot_gap_code(name: str) -> str: + return f"NEEDS_CLARIFICATION:{name.replace(' ', '_').upper()}" + + def missing_ssot_handling_default() -> list[str]: return [ - "- If a vertical SSOT is missing or incomplete, infer temporary guidance from current repository evidence.", - "- Mark inferred guidance as pending SSOT solidification.", - "- Do not present inferred guidance as an approved repository rule.", - "- Do not let inference override explicit SSOT content.", + "- If a vertical SSOT is missing or incomplete, treat repository evidence as descriptive context only.", + "- Before changing a surface governed by missing SSOT, ask for clarification or record `NEEDS_CLARIFICATION:` in handoff.", + "- Use existing code and config facts for narrow edits only when task scope and validation are explicit.", + "- Do not invent repository policy from descriptive repository evidence.", ] @@ -774,7 +957,7 @@ def authority_default() -> list[str]: "5. Active `PROJECT GOVERNANCE` projection", "6. Tests and CI results", "7. Historical documents", - "8. Agent inference", + "8. Explicit assumptions for reversible local edits", "- Active projection is generated routing guidance and is subordinate to explicit vertical SSOT documents or source-backed repository facts on substantive conflicts.", ] @@ -783,8 +966,8 @@ def repository_workflow_default() -> list[str]: return [ "- Classify task type and path family before changing files.", "- Read every SSOT route matched by Path And Task Scope Rules.", - "- Use Repository Evidence as source-backed facts, not as higher authority than explicit SSOT.", - "- Run Development Commands that match the changed surface.", + "- Use SSOT Index source_refs as entrypoints, not as replacement content for the referenced sources.", + "- Run validation commands from explicit Engineering SSOT instructions or user direction when they match the changed surface.", "- 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.", @@ -833,14 +1016,12 @@ def write_boundary_default(style: str) -> list[str]: if style == "rule": return [ "- Stay 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`.", + "- Edit agent context files only when the user explicitly asks for instruction changes.", "- 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.", - "- 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`.", + "- Edit agent context files only when the user explicitly asks for instruction changes.", "- Protected-file writes require explicit user request, a named matching contract or regression test, and passing validation commands.", ] @@ -855,9 +1036,10 @@ def mcp_default(style: str) -> list[str]: 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 name, description or trigger, allowed read paths, allowed write paths, forbidden paths, outputs, and validation command.", + "- Repository-local skills are evidence only unless an explicit Agent Harness SSOT source names them.", + "- Read matching repository-local `SKILL.md` before planning or editing.", + "- Treat skill scope declarations as task-local constraints.", + "- If a matching skill lacks scope or validation guidance, ask for clarification before expanding writes.", ] @@ -877,75 +1059,18 @@ def scan_feature_specs(root: Path) -> list[str]: def scan_skills(root: Path) -> list[str]: - return sorted(rel(root, path) for path in root.rglob("SKILL.md") if not ignored(path)) - - -def skill_capability_lines(root: Path) -> list[str]: - lines: list[str] = [] - for path_text in scan_skills(root): - path = root / path_text - fields = skill_frontmatter(path) - name = fields.get("name") or fallback_skill_name(path_text) - description = fields.get("description") or f"Repository-local skill spec at {path_text}." - lines.extend( - [ - 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.", - ] - ) - return lines - - -def skill_frontmatter(path: Path) -> dict[str, str]: - try: - lines = normalize_newlines(path.read_text(encoding="utf-8-sig")).splitlines() - except (OSError, UnicodeDecodeError): - return {} - if not lines or lines[0].strip() != "---": - return {} - fields: dict[str, str] = {} - for line in lines[1:]: - if line.strip() == "---": - break - key, separator, value = line.partition(":") - if not separator: - continue - key = key.strip() - value = value.strip().strip("'\"") - if key in {"name", "description", "trigger"} and value: - fields[key] = value - return fields - - -def fallback_skill_name(path_text: str) -> str: - parent = Path(path_text).parent.name - return parent or path_text.replace("/", "-") - - -def capability_index_lines(root: Path) -> list[str]: - mcp_configs = scan_mcp_configs(root) - mcp_sources = format_values(mcp_configs) if mcp_configs else "none detected" - lines = skill_capability_lines(root) - lines.extend( - [ - "- Repository capability: MCP-backed external tools", - f" - Sources: MCP config candidates are evidence, not proof of active tools: {mcp_sources}.", - " - Runtime action: enumerate available servers, resources, and tools before use.", - ] - ) - return lines + return sorted(rel(root, path) for path in project_files(root) if path.name == "SKILL.md") def agent_adapter_lines(root: Path, target: Path, integration: str) -> list[str]: + adapter_support = integration if integration in SUPPORTED_ADAPTERS else "generic fallback" 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 agent platform target without claiming unavailable platform support.", + "- Repository capability layer: source-backed repository-local skills and MCP candidates only.", + "- Agent adapter layer: use explicit integration support when available; otherwise use generic fallback rules.", + "- Platform projection layer: apply only rules supported by the active target file.", f"- Active integration: {integration}", f"- Context target: {rel(root, target)}", + f"- Adapter support: {adapter_support}", ] if integration == "codex": lines.extend( @@ -954,26 +1079,43 @@ def agent_adapter_lines(root: Path, target: Path, integration: str) -> list[str] "- MCP discovery: platform runtime enumeration first; repository config candidates are evidence only unless supported by this adapter.", ] ) + elif integration == "zed": + lines.extend( + [ + "- Instruction discovery: Zed project instructions from the active context target.", + "- Skill discovery: evidence-only repository scan; activate skills through the Zed runtime when available.", + "- MCP discovery: enumerate Zed runtime tools before use; repository config candidates are evidence only.", + ] + ) else: lines.extend( [ - "- Skill discovery: evidence-only repository scan; platform activation is integration-specific.", - "- MCP discovery: platform-specific; repository config candidates are evidence only.", + "- Skill discovery: evidence-only repository scan; no platform activation is assumed.", + "- MCP discovery: no platform support is assumed; repository config candidates are evidence only.", ] ) return lines +def evidence_count(values: list[str]) -> str: + if not values: + return "none detected" + return f"{len(values)} detected" + + def scan_mcp_configs(root: Path) -> list[str]: return sorted( rel(root, path) - for path in root.rglob("*") - if path.is_file() and not ignored(path) and path.name in MCP_CONFIG_NAMES + for path in project_files(root) + if path.name in MCP_CONFIG_NAMES ) def ignored(path: Path) -> bool: - return any(part in {".git", "__pycache__", ".venv", "node_modules"} for part in path.parts) + parts = path.parts + if any(part in {".git", "__pycache__", ".venv", "node_modules"} for part in parts): + return True + return any(parts[index : index + 2] == (".specify", "extensions") for index in range(len(parts) - 1)) def extensions_status(root: Path) -> str: diff --git a/extensions/repository-governance/templates/repository-governance-template.md b/extensions/repository-governance/templates/repository-governance-template.md index ded767bdb0..780ab29726 100644 --- a/extensions/repository-governance/templates/repository-governance-template.md +++ b/extensions/repository-governance/templates/repository-governance-template.md @@ -12,10 +12,9 @@ Sync Impact Report ## Repository-Wide Instructions - Framework: Project Governance Projection Framework. -- Generate and review only the resolved active agent platform target. -- Use the Copilot instruction model for layering only; do not emit Copilot path-specific companion files. -- Keep instructions short, self-contained, and free of conflicting rules. -- Legacy managed-section cleanup: non-active context files enumerated by `CONTEXT_FILES`. +- Treat this file as the active project-governance entrypoint for coding-agent work in this repository. +- Keep task reasoning grounded in source-backed repository facts, matched SSOT routes, and explicit user instructions. +- Keep edits scoped to the active task and matched path family. - architecture methodology: owned by Architecture SSOT. ### Authority @@ -27,34 +26,44 @@ Sync Impact Report 5. Active `PROJECT GOVERNANCE` projection 6. Tests and CI results 7. Historical documents -8. Agent inference +8. Explicit assumptions for reversible local edits - Active projection is generated routing guidance and is subordinate to explicit vertical SSOT documents or source-backed repository facts on substantive conflicts. -## SSOT Routing +## SSOT Index - Architecture SSOT: owns architecture boundaries, interfaces, dependencies, runtime constraints, deployment assumptions, and scenario-level architecture decisions. - 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. - Agent Harness SSOT: owns agent task boundaries, tool usage, permissions, audit, validation, and failure handling. -- Architecture evidence: none detected -- Engineering evidence: none detected -- Code Style evidence: none detected -- Directory Structure evidence: none detected -- Agent Harness evidence: none detected +- Architecture SSOT index: + - status: missing + - source_refs: none detected + - gap: NEEDS_CLARIFICATION:ARCHITECTURE +- Engineering SSOT index: + - status: missing + - source_refs: none detected + - gap: NEEDS_CLARIFICATION:ENGINEERING +- Code Style SSOT index: + - status: missing + - source_refs: none detected + - gap: NEEDS_CLARIFICATION:CODE_STYLE +- Directory Structure SSOT index: + - status: missing + - source_refs: none detected + - gap: NEEDS_CLARIFICATION:DIRECTORY_STRUCTURE +- Agent Harness SSOT index: + - status: missing + - source_refs: none detected + - gap: NEEDS_CLARIFICATION:AGENT_HARNESS ### Missing SSOT Handling -- If a vertical SSOT is missing or incomplete, infer temporary guidance from current repository evidence. -- Mark inferred guidance as pending SSOT solidification. -- Do not present inferred guidance as an approved repository rule. -- Do not let inference override explicit SSOT content. - -## Repository Evidence - -- Repository facts are descriptive source-backed evidence. -- Repository facts do not override explicit vertical SSOT content. +- If a vertical SSOT is missing or incomplete, treat repository evidence as descriptive context only. +- Before changing a surface governed by missing SSOT, ask for clarification or record `NEEDS_CLARIFICATION:` in handoff. +- Use existing code and config facts for narrow edits only when task scope and validation are explicit. +- Do not invent repository policy from descriptive repository evidence. ## Path And Task Scope Rules @@ -65,24 +74,22 @@ Sync Impact Report - Agent instructions, permissions, MCP, external tools, skills, validation, or failure-handling changes: read Agent Harness SSOT before edits. - If multiple rules match, read every matched SSOT and apply the highest authority non-conflicting rule. -### Directory Governance +### Directory Structure Fallback -- Responsibility: one primary purpose per directory. -- Depth: 2. -- Coverage: include visible, hidden, generated, cache, config/env, tool, and agent directories. -- Mixed concerns: follow existing repo convention or split responsibility. -- Change impact: review linked code, tests, docs, config/env, data, assets, generated files, and tool outputs; update only when in scope and authorized. +- Use only when Directory Structure SSOT is missing and the task scope is explicit. +- Treat scanned repository areas as descriptive context, not as approved directory policy. +- Keep new or moved files aligned with existing nearby conventions unless the user supplies a different target. +- Record `NEEDS_CLARIFICATION:DIRECTORY_STRUCTURE` in handoff when placement is ambiguous. ## Agent Harness -- Repository Capability: abstract repository-local skill and MCP evidence into scenario capabilities. -- 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. -- Repository-local skills: use when the task matches a declared skill name, description, or trigger. -- MCP-backed external tools: use when the task needs external tool or resource access; enumerate runtime tools before use. +- Repository capability layer: source-backed repository-local skills and MCP candidates only. +- Agent adapter layer: use explicit integration support when available; otherwise use generic fallback rules. +- Platform projection layer: apply only rules supported by the active target file. +- Repository-local skills: evidence only unless an explicit Agent Harness SSOT source names them; read matching `SKILL.md` before planning or editing. +- MCP-backed external tools: indexed as MCP config candidates only; enumerate runtime tools before use. - Repository config candidates are evidence only unless the active adapter supports them. -- Repository-local skill specs should declare name, description or trigger, allowed read paths, allowed write paths, forbidden paths, outputs, and validation command. -- Read matching `SKILL.md` files before planning or editing. +- If a matching skill lacks scope or validation guidance, ask for clarification before expanding writes. - MCP default: read-only. - MCP mutation: explicit user intent with target, action, and expected effect. - Secrets: never log, never write. @@ -90,15 +97,10 @@ Sync Impact Report ## Write Boundaries - Scope: active task only. -- Active agent platform target: generated output, overwritten on generation. -- Legacy managed-section cleanup: non-active context files enumerated by `CONTEXT_FILES`. +- Agent context files: edit only when the user explicitly asks for instruction changes. - 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. -## Development Commands - -- none recorded - ## Handoff - changed files From 8ef2ab962e5dcbf8725f605d22de0d333f72a39b Mon Sep 17 00:00:00 2001 From: bigsmartben <30429295+bigsmartben@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:20:59 +0800 Subject: [PATCH 2/2] Fix fork community integration validation Allow fork-hosted community integration PRs to reuse the pr-template route without requiring upstream maintainer approval, while preserving the approval requirement for github/spec-kit. Assisted-by: OpenAI Codex (GPT-5, autonomous) --- .github/workflows/community-integration.yml | 4 ++- scripts/community/validate_integration.py | 38 ++++++++++++++------ tests/test_community_validate_integration.py | 9 +++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.github/workflows/community-integration.yml b/.github/workflows/community-integration.yml index b29c2b27d2..c0f35ca5e0 100644 --- a/.github/workflows/community-integration.yml +++ b/.github/workflows/community-integration.yml @@ -35,7 +35,9 @@ jobs: $argsList = @( 'scripts/community/validate_integration.py', '--branch', - '${{ github.head_ref }}' + '${{ github.head_ref }}', + '--repository-full-name', + '${{ github.repository }}' ) if (Test-Path -LiteralPath .pr-body.md) { $argsList += @('--pr-body-file', '.pr-body.md') diff --git a/scripts/community/validate_integration.py b/scripts/community/validate_integration.py index d66c33701e..c241fc6adf 100644 --- a/scripts/community/validate_integration.py +++ b/scripts/community/validate_integration.py @@ -20,6 +20,7 @@ SHA_RE = re.compile(r"\b[0-9a-fA-F]{40}\b") URL_RE = re.compile(r"^https://") COMMUNITY_BRANCH_RE = re.compile(r"^community/(?:\d+-)?[a-z0-9][a-z0-9._-]*$") +UPSTREAM_REPOSITORY = "github/spec-kit" class Validation: @@ -149,7 +150,16 @@ def validate_branch(branch: str | None, validation: Validation) -> None: ) -def validate_pr_body(body_path: Path | None, branch: str | None, validation: Validation) -> None: +def requires_direct_pr_approval(repository_full_name: str | None) -> bool: + return (repository_full_name or UPSTREAM_REPOSITORY).lower() == UPSTREAM_REPOSITORY + + +def validate_pr_body( + body_path: Path | None, + branch: str | None, + validation: Validation, + repository_full_name: str | None = None, +) -> None: if body_path is None: return try: @@ -182,15 +192,16 @@ def validate_pr_body(body_path: Path | None, branch: str | None, validation: Val if route == "pr-template": direct_pr_approval = field_value(body, "Maintainer direct PR approval") - validation.require( - direct_pr_approval is not None and URL_RE.match(direct_pr_approval) is not None, - ( - "direct pr-template community extension/preset PRs must include an https " - "Maintainer direct PR approval; otherwise submit extensions with " - ".github/ISSUE_TEMPLATE/extension_submission.yml and presets with " - ".github/ISSUE_TEMPLATE/preset_submission.yml" - ), - ) + if requires_direct_pr_approval(repository_full_name): + validation.require( + direct_pr_approval is not None and URL_RE.match(direct_pr_approval) is not None, + ( + "direct pr-template community extension/preset PRs must include an https " + "Maintainer direct PR approval; otherwise submit extensions with " + ".github/ISSUE_TEMPLATE/extension_submission.yml and presets with " + ".github/ISSUE_TEMPLATE/preset_submission.yml" + ), + ) source_repository = field_value(body, "Source repository") source_version = field_value(body, "Source version") @@ -220,6 +231,11 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--repo-root", type=Path, default=Path.cwd()) parser.add_argument("--branch", default=None, help="Current branch name, usually github.head_ref in CI") parser.add_argument("--pr-body-file", type=Path, default=None) + parser.add_argument( + "--repository-full-name", + default=UPSTREAM_REPOSITORY, + help="GitHub repository in owner/name form; upstream github/spec-kit enforces direct PR approval", + ) parser.add_argument("--show-warnings", action="store_true", help="Print non-blocking historical catalog style warnings") return parser.parse_args() @@ -230,7 +246,7 @@ def main() -> int: validation = Validation() validate_branch(args.branch, validation) - validate_pr_body(args.pr_body_file, args.branch, validation) + validate_pr_body(args.pr_body_file, args.branch, validation, args.repository_full_name) validate_catalog(repo_root / "extensions" / "catalog.community.json", "extensions", validation) validate_catalog(repo_root / "presets" / "catalog.community.json", "presets", validation) diff --git a/tests/test_community_validate_integration.py b/tests/test_community_validate_integration.py index 41c863b4f9..d3019adb27 100644 --- a/tests/test_community_validate_integration.py +++ b/tests/test_community_validate_integration.py @@ -38,6 +38,15 @@ def test_direct_pr_template_requires_maintainer_approval(tmp_path: Path) -> None assert any("Maintainer direct PR approval" in error for error in validation.errors) +def test_fork_pr_template_does_not_require_maintainer_approval(tmp_path: Path) -> None: + validation = Validation() + body_path = _write_body(tmp_path, _base_body(route="pr-template")) + + validate_pr_body(body_path, "community/example-extension", validation, "bigsmartben/spec-kit") + + assert validation.errors == [] + + def test_direct_pr_template_accepts_maintainer_approval(tmp_path: Path) -> None: validation = Validation() body_path = _write_body(