diff --git a/pyproject.toml b/pyproject.toml index 13be17cd7a..c488cdd37d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,13 @@ packages = ["src/specify_cli"] # Bundle core assets so `specify init` works without network access (air-gapped / enterprise) # Page templates (exclude commands/ — bundled separately below to avoid duplication) "templates/checklist-template.md" = "specify_cli/core_pack/templates/checklist-template.md" +"templates/architecture-development-template.md" = "specify_cli/core_pack/templates/architecture-development-template.md" +"templates/architecture-logical-template.md" = "specify_cli/core_pack/templates/architecture-logical-template.md" +"templates/architecture-physical-template.md" = "specify_cli/core_pack/templates/architecture-physical-template.md" +"templates/architecture-process-template.md" = "specify_cli/core_pack/templates/architecture-process-template.md" +"templates/architecture-scenario-template.md" = "specify_cli/core_pack/templates/architecture-scenario-template.md" +"templates/architecture-template.md" = "specify_cli/core_pack/templates/architecture-template.md" +"templates/agent-governance-template.md" = "specify_cli/core_pack/templates/agent-governance-template.md" "templates/constitution-template.md" = "specify_cli/core_pack/templates/constitution-template.md" "templates/plan-template.md" = "specify_cli/core_pack/templates/plan-template.md" "templates/spec-template.md" = "specify_cli/core_pack/templates/spec-template.md" @@ -70,4 +77,3 @@ omit = ["*/tests/*", "*/__pycache__/*"] precision = 2 show_missing = true skip_covered = false - diff --git a/scripts/bash/setup-arch.sh b/scripts/bash/setup-arch.sh new file mode 100755 index 0000000000..f6d566d275 --- /dev/null +++ b/scripts/bash/setup-arch.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -e + +# Parse command line arguments +JSON_MODE=false + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --help|-h) + echo "Usage: $0 [--json]" + echo " --json Output results in JSON format" + echo " --help Show this help message" + exit 0 + ;; + *) + ;; + esac +done + +# Get script directory and load common functions +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +REPO_ROOT=$(get_repo_root) +ARCH_DIR="$REPO_ROOT/.specify/memory" +ARCH_FILE="$ARCH_DIR/architecture.md" +SCENARIO_VIEW="$ARCH_DIR/architecture-scenario-view.md" +LOGICAL_VIEW="$ARCH_DIR/architecture-logical-view.md" +PROCESS_VIEW="$ARCH_DIR/architecture-process-view.md" +DEVELOPMENT_VIEW="$ARCH_DIR/architecture-development-view.md" +PHYSICAL_VIEW="$ARCH_DIR/architecture-physical-view.md" + +mkdir -p "$ARCH_DIR" + +copy_template_if_missing() { + local template_name="$1" + local destination="$2" + + if [[ -f "$destination" ]]; then + return 0 + fi + + local template + template=$(resolve_template "$template_name" "$REPO_ROOT") || true + if [[ -n "$template" ]] && [[ -f "$template" ]]; then + cp "$template" "$destination" + echo "Copied $template_name template to $destination" + else + echo "Warning: $template_name template not found" + touch "$destination" + fi +} + +copy_template_if_missing "architecture-template" "$ARCH_FILE" +copy_template_if_missing "architecture-scenario-template" "$SCENARIO_VIEW" +copy_template_if_missing "architecture-logical-template" "$LOGICAL_VIEW" +copy_template_if_missing "architecture-process-template" "$PROCESS_VIEW" +copy_template_if_missing "architecture-development-template" "$DEVELOPMENT_VIEW" +copy_template_if_missing "architecture-physical-template" "$PHYSICAL_VIEW" + +if $JSON_MODE; then + if has_jq; then + jq -cn \ + --arg arch_file "$ARCH_FILE" \ + --arg arch_dir "$ARCH_DIR" \ + --arg scenario_view "$SCENARIO_VIEW" \ + --arg logical_view "$LOGICAL_VIEW" \ + --arg process_view "$PROCESS_VIEW" \ + --arg development_view "$DEVELOPMENT_VIEW" \ + --arg physical_view "$PHYSICAL_VIEW" \ + '{ARCH_FILE:$arch_file,ARCH_DIR:$arch_dir,SCENARIO_VIEW:$scenario_view,LOGICAL_VIEW:$logical_view,PROCESS_VIEW:$process_view,DEVELOPMENT_VIEW:$development_view,PHYSICAL_VIEW:$physical_view}' + else + printf '{"ARCH_FILE":"%s","ARCH_DIR":"%s","SCENARIO_VIEW":"%s","LOGICAL_VIEW":"%s","PROCESS_VIEW":"%s","DEVELOPMENT_VIEW":"%s","PHYSICAL_VIEW":"%s"}\n' \ + "$(json_escape "$ARCH_FILE")" \ + "$(json_escape "$ARCH_DIR")" \ + "$(json_escape "$SCENARIO_VIEW")" \ + "$(json_escape "$LOGICAL_VIEW")" \ + "$(json_escape "$PROCESS_VIEW")" \ + "$(json_escape "$DEVELOPMENT_VIEW")" \ + "$(json_escape "$PHYSICAL_VIEW")" + fi +else + echo "ARCH_FILE: $ARCH_FILE" + echo "ARCH_DIR: $ARCH_DIR" + echo "SCENARIO_VIEW: $SCENARIO_VIEW" + echo "LOGICAL_VIEW: $LOGICAL_VIEW" + echo "PROCESS_VIEW: $PROCESS_VIEW" + echo "DEVELOPMENT_VIEW: $DEVELOPMENT_VIEW" + echo "PHYSICAL_VIEW: $PHYSICAL_VIEW" +fi diff --git a/scripts/powershell/setup-arch.ps1 b/scripts/powershell/setup-arch.ps1 new file mode 100755 index 0000000000..b2f7427360 --- /dev/null +++ b/scripts/powershell/setup-arch.ps1 @@ -0,0 +1,86 @@ +#!/usr/bin/env pwsh +# Setup project-level 4+1 architecture artifacts + +[CmdletBinding()] +param( + [switch]$Json, + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +if ($Help) { + Write-Output "Usage: ./setup-arch.ps1 [-Json] [-Help]" + Write-Output " -Json Output results in JSON format" + Write-Output " -Help Show this help message" + exit 0 +} + +. "$PSScriptRoot/common.ps1" + +function Convert-ToPlainPath { + param([Parameter(Mandatory = $true)][string]$Path) + + if ($Path -like 'Microsoft.PowerShell.Core\FileSystem::*') { + return $Path.Substring('Microsoft.PowerShell.Core\FileSystem::'.Length) + } + return $Path +} + +$repoRoot = Convert-ToPlainPath (Get-RepoRoot) +$archDir = Join-Path $repoRoot ".specify/memory" +$archFile = Join-Path $archDir "architecture.md" +$scenarioView = Join-Path $archDir "architecture-scenario-view.md" +$logicalView = Join-Path $archDir "architecture-logical-view.md" +$processView = Join-Path $archDir "architecture-process-view.md" +$developmentView = Join-Path $archDir "architecture-development-view.md" +$physicalView = Join-Path $archDir "architecture-physical-view.md" + +New-Item -ItemType Directory -Path $archDir -Force | Out-Null + +function Copy-TemplateIfMissing { + param( + [Parameter(Mandatory = $true)][string]$TemplateName, + [Parameter(Mandatory = $true)][string]$Destination + ) + + if (Test-Path -LiteralPath $Destination -PathType Leaf) { + return + } + + $template = Resolve-Template -TemplateName $TemplateName -RepoRoot $repoRoot + if ($template -and (Test-Path -LiteralPath $template -PathType Leaf)) { + Copy-Item -LiteralPath $template -Destination $Destination -Force + Write-Output "Copied $TemplateName template to $Destination" + } else { + Write-Warning "$TemplateName template not found" + New-Item -ItemType File -Path $Destination -Force | Out-Null + } +} + +Copy-TemplateIfMissing -TemplateName "architecture-template" -Destination $archFile +Copy-TemplateIfMissing -TemplateName "architecture-scenario-template" -Destination $scenarioView +Copy-TemplateIfMissing -TemplateName "architecture-logical-template" -Destination $logicalView +Copy-TemplateIfMissing -TemplateName "architecture-process-template" -Destination $processView +Copy-TemplateIfMissing -TemplateName "architecture-development-template" -Destination $developmentView +Copy-TemplateIfMissing -TemplateName "architecture-physical-template" -Destination $physicalView + +if ($Json) { + [PSCustomObject]@{ + ARCH_FILE = $archFile + ARCH_DIR = $archDir + SCENARIO_VIEW = $scenarioView + LOGICAL_VIEW = $logicalView + PROCESS_VIEW = $processView + DEVELOPMENT_VIEW = $developmentView + PHYSICAL_VIEW = $physicalView + } | ConvertTo-Json -Compress +} else { + Write-Output "ARCH_FILE: $archFile" + Write-Output "ARCH_DIR: $archDir" + Write-Output "SCENARIO_VIEW: $scenarioView" + Write-Output "LOGICAL_VIEW: $logicalView" + Write-Output "PROCESS_VIEW: $processView" + Write-Output "DEVELOPMENT_VIEW: $developmentView" + Write-Output "PHYSICAL_VIEW: $physicalView" +} diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index d3eb36391e..ccc8fbaa09 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -74,6 +74,10 @@ install_shared_infra as _install_shared_infra_impl, refresh_shared_templates as _refresh_shared_templates_impl, ) +from .agent_projection import ( + ensure_agent_governance_from_template as _ensure_agent_governance_from_template, + refresh_agent_projection as _refresh_agent_projection, +) # For cross-platform keyboard input import readchar @@ -928,6 +932,46 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker | console.print(f"[yellow]Warning: Could not initialize constitution: {e}[/yellow]") +def ensure_agent_governance_from_template(project_path: Path, tracker: StepTracker | None = None) -> None: + """Copy agent-governance template to memory if it doesn't exist.""" + try: + result = _ensure_agent_governance_from_template(project_path) + except Exception as e: + if tracker: + tracker.add("agent-governance", "Agent governance setup") + tracker.error("agent-governance", str(e)) + else: + console.print(f"[yellow]Warning: Could not initialize agent governance: {e}[/yellow]") + return + + if tracker: + tracker.add("agent-governance", "Agent governance setup") + if result is None: + tracker.error("agent-governance", "template not found") + else: + tracker.complete("agent-governance", "available") + + +def refresh_agent_projection(project_path: Path, tracker: StepTracker | None = None) -> None: + """Refresh generated agent governance projections.""" + try: + result = _refresh_agent_projection(project_path) + except Exception as e: + if tracker: + tracker.add("agent-projection", "Agent governance projection") + tracker.error("agent-projection", str(e)) + else: + console.print(f"[yellow]Warning: Could not refresh agent projection: {e}[/yellow]") + return + + if tracker: + tracker.add("agent-projection", "Agent governance projection") + if result.memory_path is None: + tracker.skip("agent-projection", "agent-governance template missing") + else: + tracker.complete("agent-projection", f"{len(result.projection_paths)} file(s) refreshed") + + INIT_OPTIONS_FILE = ".specify/init-options.json" @@ -973,6 +1017,9 @@ def _get_skills_dir(project_path: Path, selected_ai: str) -> Path: # Constants kept for backward compatibility with presets and extensions. DEFAULT_SKILLS_DIR = ".agents/skills" SKILL_DESCRIPTIONS = { + "arch": "Generate project-level 4+1 architecture view artifacts and synthesis.", + "agent": "Create or update agent governance and refresh agent instruction projections.", + "governance": "Create or update agent governance and refresh agent instruction projections.", "specify": "Create or update feature specifications from natural language descriptions.", "plan": "Generate technical implementation plans from feature specifications.", "tasks": "Break down implementation plans into actionable task lists.", @@ -1361,6 +1408,8 @@ def init( tracker.complete("shared-infra", f"scripts ({selected_script}) + templates") ensure_constitution_from_template(project_path, tracker=tracker) + ensure_agent_governance_from_template(project_path, tracker=tracker) + refresh_agent_projection(project_path, tracker=tracker) if not no_git: tracker.start("git") @@ -1629,11 +1678,12 @@ def _display_cmd(name: str) -> str: steps_lines.append(f"{step_num}. Start using {usage_label} with your coding agent:") - steps_lines.append(f" {step_num}.1 [cyan]{_display_cmd('constitution')}[/] - Establish project principles") - steps_lines.append(f" {step_num}.2 [cyan]{_display_cmd('specify')}[/] - Create baseline specification") - steps_lines.append(f" {step_num}.3 [cyan]{_display_cmd('plan')}[/] - Create implementation plan") - steps_lines.append(f" {step_num}.4 [cyan]{_display_cmd('tasks')}[/] - Generate actionable tasks") - steps_lines.append(f" {step_num}.5 [cyan]{_display_cmd('implement')}[/] - Execute implementation") + steps_lines.append(f" {step_num}.1 [cyan]{_display_cmd('arch')}[/] - Shape 4+1 architecture views") + steps_lines.append(f" {step_num}.2 [cyan]{_display_cmd('constitution')}[/] - Establish project principles") + steps_lines.append(f" {step_num}.3 [cyan]{_display_cmd('specify')}[/] - Create baseline specification") + steps_lines.append(f" {step_num}.4 [cyan]{_display_cmd('plan')}[/] - Create implementation plan") + steps_lines.append(f" {step_num}.5 [cyan]{_display_cmd('tasks')}[/] - Generate actionable tasks") + steps_lines.append(f" {step_num}.6 [cyan]{_display_cmd('implement')}[/] - Execute implementation") steps_panel = Panel("\n".join(steps_lines), title="Next Steps", border_style="cyan", padding=(1,2)) console.print() @@ -1997,6 +2047,7 @@ def _write_integration_json( installed_integrations=installed_integrations, settings=integration_settings, ) + refresh_agent_projection(project_root) def _clear_init_options_for_integration(project_root: Path, integration_key: str) -> None: @@ -2015,6 +2066,7 @@ def _remove_integration_json(project_root: Path) -> None: path = project_root / INTEGRATION_JSON if path.exists(): path.unlink() + refresh_agent_projection(project_root) _MANIFEST_READ_ERRORS = (ValueError, FileNotFoundError, OSError, UnicodeDecodeError) diff --git a/src/specify_cli/agent_projection.py b/src/specify_cli/agent_projection.py new file mode 100644 index 0000000000..5353b5b932 --- /dev/null +++ b/src/specify_cli/agent_projection.py @@ -0,0 +1,329 @@ +"""Agent governance memory and projection helpers.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import yaml + +from .integration_state import ( + INTEGRATION_JSON, + default_integration_key, + installed_integration_keys, + normalize_integration_state, +) + + +AGENT_GOVERNANCE_MEMORY = ".specify/memory/agent-governance.md" +AGENT_GOVERNANCE_TEMPLATE = ".specify/templates/agent-governance-template.md" + +PROJECTION_MARKER_START = "" +PROJECTION_MARKER_END = "" + + +@dataclass(frozen=True) +class AgentProjectionResult: + """Files updated by an agent projection refresh.""" + + memory_path: Path | None + projection_paths: list[Path] + + +def ensure_agent_governance_from_template(project_root: Path) -> Path | None: + """Copy agent-governance template to memory if missing.""" + memory_path = project_root / AGENT_GOVERNANCE_MEMORY + if memory_path.exists(): + return memory_path + + template_path = project_root / AGENT_GOVERNANCE_TEMPLATE + if not template_path.exists(): + return None + + memory_path.parent.mkdir(parents=True, exist_ok=True) + memory_path.write_bytes(template_path.read_bytes()) + return memory_path + + +def refresh_agent_projection(project_root: Path) -> AgentProjectionResult: + """Refresh repo-level and agent-specific governance projections. + + The source of truth is ``.specify/memory/agent-governance.md`` plus the + repository's current integration, skill, MCP, and extension state. Existing + text outside the generated projection markers is preserved. + """ + memory_path = ensure_agent_governance_from_template(project_root) + if memory_path is None: + return AgentProjectionResult(None, []) + + state = _read_integration_state(project_root) + installed = installed_integration_keys(state) + default_key = default_integration_key(state) + projection_paths = _projection_targets(project_root, state) + projection = _render_projection(project_root, memory_path, state) + updated: list[Path] = [] + + for path in projection_paths: + content = _adapter_prelude(path, default_key, installed) + if path.exists(): + existing = path.read_text(encoding="utf-8-sig") + new_content = _upsert_marked_section(existing, projection) + if new_content == existing: + continue + else: + path.parent.mkdir(parents=True, exist_ok=True) + new_content = content + "\n" + projection + + path.write_text(_normalize_newlines(new_content), encoding="utf-8") + updated.append(path) + + return AgentProjectionResult(memory_path, updated) + + +def _read_integration_state(project_root: Path) -> dict[str, Any]: + path = project_root / INTEGRATION_JSON + if not path.exists(): + return normalize_integration_state({}) + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError, UnicodeDecodeError): + return normalize_integration_state({}) + return normalize_integration_state(data if isinstance(data, dict) else {}) + + +def _projection_targets(project_root: Path, state: dict[str, Any]) -> list[Path]: + targets: list[Path] = [project_root / "AGENTS.md"] + + try: + from .integrations import get_integration + except Exception: + get_integration = None # type: ignore[assignment] + + for key in installed_integration_keys(state): + integration = get_integration(key) if get_integration else None + context_file = getattr(integration, "context_file", None) + if isinstance(context_file, str) and context_file.strip(): + targets.append(project_root / context_file) + + # Common adapter files. They are created when the corresponding + # integration is installed, and refreshed whenever they already exist so + # uninstall/switch operations do not leave stale generated projections. + for key, path in { + "claude": "CLAUDE.md", + "gemini": "GEMINI.md", + "copilot": ".github/copilot-instructions.md", + }.items(): + target = project_root / path + if key in installed_integration_keys(state) or target.exists(): + targets.append(target) + + deduped: list[Path] = [] + seen: set[str] = set() + for path in targets: + rel = path.resolve().as_posix() + if rel in seen: + continue + seen.add(rel) + deduped.append(path) + return deduped + + +def _render_projection( + project_root: Path, + memory_path: Path, + state: dict[str, Any], +) -> str: + installed = installed_integration_keys(state) + default_key = default_integration_key(state) + skills = _scan_skills(project_root) + mcp_configs = _scan_mcp_configs(project_root) + extensions = _scan_extensions(project_root) + governance_body = _read_governance_body(memory_path) + + lines = [ + PROJECTION_MARKER_START, + "# Repository Agent Governance Projection", + "", + "Generated from repository state. Do not edit this section directly; update", + f"`{AGENT_GOVERNANCE_MEMORY}`, integrations, skills, MCP config, or extensions instead.", + "", + "## Governing Source", + f"- Repository-level agent governance SSOT: `{AGENT_GOVERNANCE_MEMORY}`", + "- Project principles SSOT: `.specify/memory/constitution.md`", + "- Feature work SSOT: `specs//`", + "", + "## Repository Agent Governance", + governance_body or "- No repository-level agent governance rules found.", + "", + "## Active Integrations", + f"- Default integration: `{default_key or 'none'}`", + f"- Installed integrations: {', '.join(f'`{key}`' for key in installed) if installed else '`none`'}", + "", + "## Active Skills", + ] + + if skills: + for skill in skills: + lines.append(f"- `{skill}`") + else: + lines.append("- `none detected`") + + lines.extend(["", "## MCP Configuration"]) + if mcp_configs: + for config in mcp_configs: + lines.append(f"- `{config}`") + else: + lines.append("- `none detected`") + + lines.extend(["", "## Extensions"]) + if extensions: + for extension in extensions: + lines.append(f"- `{extension}`") + else: + lines.append("- `none detected`") + + lines.extend([ + "", + "## Required Operating Rules", + "- Follow current user instructions first.", + "- Treat `.specify/memory/agent-governance.md` as the source of truth for repository-level agent, skill, MCP, and integration behavior.", + "- Treat `.specify/memory/constitution.md` as the source of truth for Specify project principles and quality gates.", + "- Keep governance domains separate: agent governance, constitution, and feature artifacts keep their own authority.", + "- Agent code writes are allowed only while executing the generated Spec Kit implement command or integration-equivalent implement skill/alias.", + "- Before writing code, tests, build configuration, migrations, runtime assets, or other implementation files, verify the active change has `spec.md`, `plan.md`, and `tasks.md` under `specs//`.", + "- For bug fixes, refactors, and small code changes, create or update the required spec artifacts first; do not bypass the code-write gate.", + "- Do not edit governance, CI, MCP config, secrets, permissions, or tool settings unless explicitly requested.", + "- Do not overwrite user edits or modify files outside the active task scope.", + "- Report changed files, commands run, validation results, and unresolved risks before handoff.", + "", + f"_Projection source file: `{memory_path.relative_to(project_root).as_posix()}`_", + PROJECTION_MARKER_END, + "", + ]) + return "\n".join(lines) + + +def _read_governance_body(memory_path: Path) -> str: + """Return the user-governed content from agent-governance memory.""" + try: + content = memory_path.read_text(encoding="utf-8-sig") + except (OSError, UnicodeDecodeError): + return "" + + lines = content.replace("\r\n", "\n").replace("\r", "\n").splitlines() + filtered: list[str] = [] + in_sync_report = False + for line in lines: + stripped = line.strip() + if stripped == "": + in_sync_report = False + continue + filtered.append(line) + + body = "\n".join(filtered).strip() + if body.startswith("# "): + body = body.split("\n", 1)[1].strip() if "\n" in body else "" + return body + + +def _upsert_marked_section(content: str, projection: str) -> str: + start = content.find(PROJECTION_MARKER_START) + end = content.find(PROJECTION_MARKER_END, start if start != -1 else 0) + if start != -1 and end != -1 and end > start: + end += len(PROJECTION_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 _adapter_prelude(path: Path, default_key: str | None, installed: list[str]) -> str: + name = path.name + if name == "AGENTS.md": + return "# Repository Agent Governance\n\nThis file is governed by the Spec Kit governance command. Preserve user-authored instructions outside the generated Spec Kit projection markers." + if name == "CLAUDE.md": + return "# Claude Instructions\n\nRead `AGENTS.md` first; it is the repository-level agent governance projection governed by the Spec Kit governance command." + if name == "GEMINI.md": + return "# Gemini Instructions\n\nRead `AGENTS.md` first; it is the repository-level agent governance projection governed by the Spec Kit governance command." + if name == "copilot-instructions.md": + return "# GitHub Copilot Instructions\n\nRead `AGENTS.md` first; it is the repository-level agent governance projection governed by the Spec Kit governance command." + installed_text = ", ".join(installed) if installed else "none" + return ( + "# Agent Instructions\n\n" + "Read `AGENTS.md` first; it is the repository-level agent governance projection governed by the Spec Kit governance command.\n\n" + f"Default integration: `{default_key or 'none'}`. Installed integrations: `{installed_text}`." + ) + + +def _scan_skills(project_root: Path) -> list[str]: + skills: list[str] = [] + for skill_file in project_root.rglob("SKILL.md"): + if any(part in {".git", "__pycache__", ".venv", "node_modules"} for part in skill_file.parts): + continue + try: + rel = skill_file.relative_to(project_root).as_posix() + except ValueError: + rel = skill_file.as_posix() + skills.append(rel) + return sorted(skills) + + +def _scan_mcp_configs(project_root: Path) -> list[str]: + candidates: list[str] = [] + names = { + ".mcp.json", + "mcp.json", + "mcp.yml", + "mcp.yaml", + "mcp.config.json", + } + for path in project_root.rglob("*"): + if not path.is_file(): + continue + if any(part in {".git", "__pycache__", ".venv", "node_modules"} for part in path.parts): + continue + if path.name in names or "mcp" in path.name.lower(): + try: + candidates.append(path.relative_to(project_root).as_posix()) + except ValueError: + candidates.append(path.as_posix()) + return sorted(candidates) + + +def _scan_extensions(project_root: Path) -> list[str]: + registry = project_root / ".specify" / "extensions.yml" + if not registry.exists(): + return [] + try: + data = yaml.safe_load(registry.read_text(encoding="utf-8")) or {} + except (yaml.YAMLError, OSError, UnicodeDecodeError): + return [".specify/extensions.yml"] + if not isinstance(data, dict): + return [".specify/extensions.yml"] + extensions = data.get("extensions") + if isinstance(extensions, dict): + return sorted(str(key) for key in extensions) + if isinstance(extensions, list): + names = [] + for item in extensions: + if isinstance(item, dict) and item.get("id"): + names.append(str(item["id"])) + elif isinstance(item, str): + names.append(item) + return sorted(names) or [".specify/extensions.yml"] + return [".specify/extensions.yml"] + + +def _normalize_newlines(content: str) -> str: + return content.replace("\r\n", "\n").replace("\r", "\n") diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 4d78d5ac41..e700eba3e2 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -112,7 +112,7 @@ def render_frontmatter(fm: dict) -> str: return "" yaml_str = yaml.dump( - fm, default_flow_style=False, sort_keys=False, allow_unicode=True + fm, default_flow_style=False, sort_keys=False, allow_unicode=True, width=1000 ) return f"---\n{yaml_str}---\n" @@ -285,8 +285,8 @@ def render_skill_command( Technical debt note: Spec-kit currently has multiple SKILL.md generators (template packaging, init-time conversion, and extension/preset overrides). Keep the skill - frontmatter keys aligned (name/description/compatibility/metadata, with - metadata.author and metadata.source subkeys) to avoid drift across agents. + frontmatter keys aligned (name/description/purpose/trigger/boundaries/ + outputs/validation/compatibility/metadata) to avoid drift across agents. """ if not isinstance(frontmatter, dict): frontmatter = {} @@ -316,9 +316,42 @@ def build_skill_frontmatter( source: str, ) -> dict: """Build consistent SKILL.md frontmatter across all skill generators.""" + is_implement_skill = skill_name == "speckit-implement" + allowed_write_paths = [ + ".specify/**", + "specs/**", + ] + if is_implement_skill: + allowed_write_paths.extend([ + "**", + ]) skill_frontmatter = { "name": skill_name, "description": description, + "purpose": description, + "trigger": f"Invoke this skill for the `{skill_name}` Spec Kit workflow.", + "allowed-read-paths": [ + ".specify/**", + "specs/**", + "templates/**", + "scripts/**", + ], + "allowed-write-paths": allowed_write_paths, + "forbidden-paths": [ + ".git/**", + "**/.env*", + "**/secrets/**", + "**/*secret*", + "**/*token*", + ], + "outputs": [ + ( + "Implementation files, completed tasks.md checkboxes, validation results, and handoff summary" + if is_implement_skill + else "Workflow-specific spec artifacts and handoff summary" + ), + ], + "validation-command": "Run the validation commands required by the active Spec Kit workflow.", "compatibility": "Requires spec-kit project structure with .specify/ directory", "metadata": { "author": "github-spec-kit", diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 944ee4a06d..c3b53ef961 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -26,7 +26,10 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier _FALLBACK_CORE_COMMAND_NAMES = frozenset({ + "agent", + "governance", "analyze", + "arch", "checklist", "clarify", "constitution", diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 7ce107caec..7294889842 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -1483,24 +1483,18 @@ def setup( if not description: description = f"Spec Kit: {command_name} workflow" - # Build SKILL.md with manually formatted frontmatter to match - # the release packaging script output exactly (double-quoted - # values, no yaml.safe_dump quoting differences). - def _quote(v: str) -> str: - escaped = v.replace("\\", "\\\\").replace('"', '\\"') - return f'"{escaped}"' - - skill_content = ( - f"---\n" - f"name: {_quote(skill_name)}\n" - f"description: {_quote(description)}\n" - f"compatibility: {_quote('Requires spec-kit project structure with .specify/ directory')}\n" - f"metadata:\n" - f" author: {_quote('github-spec-kit')}\n" - f" source: {_quote('templates/commands/' + src_file.name)}\n" - f"---\n" - f"{processed_body}" + from specify_cli.agents import CommandRegistrar + + skill_frontmatter = CommandRegistrar.build_skill_frontmatter( + self.key, + skill_name, + description, + f"templates/commands/{src_file.name}", ) + frontmatter_text = yaml.safe_dump( + skill_frontmatter, sort_keys=False, width=1000 + ).strip() + skill_content = f"---\n{frontmatter_text}\n---\n{processed_body}" # Write speckit-/SKILL.md skill_dir = skills_dir / skill_name diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 88aef85285..ad7a75fe55 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -23,6 +23,7 @@ # Mapping of command template stem → argument-hint text shown inline # when a user invokes the slash command in Claude Code. ARGUMENT_HINTS: dict[str, str] = { + "arch": "Optional architecture scenario or 4+1 design focus", "specify": "Describe the feature you want to specify", "plan": "Optional guidance for the planning phase", "tasks": "Optional task generation constraints", @@ -30,6 +31,7 @@ "analyze": "Optional focus areas for analysis", "clarify": "Optional areas to clarify in the spec", "constitution": "Principles or values for the project constitution", + "governance": "Optional agent governance rules or projection scope", "checklist": "Domain or focus area for the checklist", "taskstoissues": "Optional filter or label for GitHub issues", } @@ -113,7 +115,9 @@ def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: s skill_frontmatter = self._build_skill_fm( skill_name, description, f"templates/commands/{template_name}.md" ) - frontmatter_text = yaml.safe_dump(skill_frontmatter, sort_keys=False).strip() + frontmatter_text = yaml.safe_dump( + skill_frontmatter, sort_keys=False, width=1000 + ).strip() return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n" def _build_skill_fm(self, name: str, description: str, source: str) -> dict: diff --git a/templates/agent-governance-template.md b/templates/agent-governance-template.md new file mode 100644 index 0000000000..ba80f3845f --- /dev/null +++ b/templates/agent-governance-template.md @@ -0,0 +1,72 @@ +# Repository Agent Governance + +This file is the source of truth for repository-level agent collaboration and generated agent instruction projections such as `AGENTS.md` and active integration context files. + +It does not define project principles, architecture decisions, or feature requirements. Those remain governed by their own source files. + + + +## Authority Order + +1. Current user instruction +2. This repository agent governance file +3. User-authored repository instructions preserved outside generated projection markers +4. `.specify/memory/constitution.md` +5. Active feature artifacts under `specs//` +6. Skill-local `SKILL.md` +7. Tool/MCP defaults + +## Source Of Truth + +- Project principles: `.specify/memory/constitution.md` +- Feature work: `specs//` +- Repository-level agent governance: `.specify/memory/agent-governance.md` +- Agent instruction projections: `AGENTS.md` and active integration context files +- Skill contracts: each `SKILL.md` +- MCP permissions: MCP configuration and allowlists + +## Write Boundaries + +- Agent code writes are allowed only while executing the generated Spec Kit implement command or integration-equivalent implement skill/alias, such as `/speckit.implement` or `/speckit-implement`. +- Before any agent writes source code, tests, build configuration, migrations, runtime assets, or other implementation files, the active change MUST have `spec.md`, `plan.md`, and `tasks.md` under `specs//`. +- Bug fixes, refactors, and small code changes are not exceptions. If the required spec artifacts do not exist, first create or update the spec artifacts through the Spec Kit workflow, then stop before implementation. +- Direct user requests to "just edit code" or similar are treated as requests to run the required spec workflow; they are not permission to bypass the code-write gate. +- Do not edit governance, CI, MCP config, secrets, permissions, or tool settings unless explicitly requested. +- Do not modify files outside the active task scope. +- Do not overwrite user edits. +- Do not rewrite generated files unless the owning workflow requires it. + +## Skill Contract + +Each skill must declare: + +- purpose +- trigger +- allowed read paths +- allowed write paths +- forbidden paths +- outputs +- validation command + +## MCP Policy + +- MCP tools are read-only by default. +- Mutating MCP calls require explicit user intent. +- External writes must report target, action, and result. +- Secrets and tokens must never be logged or written to repo files. + +## Validation + +Before handoff, report: + +- changed files +- commands run +- tests/validation result +- unresolved risks diff --git a/templates/architecture-development-template.md b/templates/architecture-development-template.md new file mode 100644 index 0000000000..d9f8c6976c --- /dev/null +++ b/templates/architecture-development-template.md @@ -0,0 +1,39 @@ +# Development View + +**Input**: `.specify/memory/architecture-logical-view.md`, `.specify/memory/architecture-process-view.md` + +**Purpose**: Derive architecture-level components, package boundary intent, contract/artifact semantics, and dependency rules from logical and process views. + +## Architecture-Level Components + +| Component / Capability Package | Responsibility | Input / Output Boundary | Collaborators | Explicitly Must Not Own | Source View Evidence | +|--------------------------------|----------------|-------------------------|---------------|--------------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Package Boundary Intent + +| Package / Boundary | Abstraction Level | Owned Concepts | May Depend On | Must Not Depend On | Evolution Rule | +|--------------------|-------------------|----------------|---------------|--------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Contracts and Artifacts + +| Contract / Artifact | Semantics | Producer | Consumer | Lifecycle | Architecture Consequence | +|---------------------|-----------|----------|----------|-----------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Dependency Rules + +| Rule | Allowed Direction | Forbidden Direction | Reason | Risk If Violated | +|------|-------------------|---------------------|--------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Development View Gaps + +| Gap | Affected Component / Boundary | Why It Matters | +|-----|-------------------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Prohibited Content + +Do not write source file paths, concrete package trees, classes, functions, implementation tasks, framework-specific wiring, or code generation notes here. diff --git a/templates/architecture-logical-template.md b/templates/architecture-logical-template.md new file mode 100644 index 0000000000..057874c3e1 --- /dev/null +++ b/templates/architecture-logical-template.md @@ -0,0 +1,39 @@ +# Logical View + +**Input**: `.specify/memory/architecture-scenario-view.md` + +**Purpose**: Derive capability boundaries, domain objects, states, relationships, and invariants from the scenario view. + +## Capability Boundaries + +| Capability / Boundary | Responsibility | Input | Output | Explicitly Does Not Own | Scenario Source | +|-----------------------|----------------|-------|--------|--------------------------|-----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Domain Objects and Relationships + +| Object | Meaning | Owning Capability | Key Relationships | Fact Source | Invariants | +|--------|---------|-------------------|-------------------|-------------|------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## State and Lifecycle + +| Object / Flow | State | Entered When | Exited When | Forbidden Transition | Responsible Boundary | +|---------------|-------|--------------|-------------|----------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Logical Decisions + +| Decision | Scope | Owner / Boundary | Affected Objects or Flows | Consequence | +|----------|-------|------------------|---------------------------|-------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Logical Gaps + +| Gap | Affected Capability / Object | Why It Matters | +|-----|------------------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Prohibited Content + +Do not write classes, DTOs, database tables, fields, method names, endpoints, schemas, or implementation data structures here. diff --git a/templates/architecture-physical-template.md b/templates/architecture-physical-template.md new file mode 100644 index 0000000000..9271d3f76c --- /dev/null +++ b/templates/architecture-physical-template.md @@ -0,0 +1,39 @@ +# Physical View + +**Input**: `.specify/memory/architecture-process-view.md`, `.specify/memory/architecture-development-view.md` + +**Purpose**: Derive deployment, hosting, external system, fact-source, observability, and operational boundaries from process and development views. + +## Deployment and Hosting Boundaries + +| Runtime / Hosting Unit | Carries | Boundary | Depends On | Release / Migration Impact | +|------------------------|---------|----------|------------|----------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## External System Collaboration + +| External System | Purpose | Exchanged Content | Authoritative Fact | Failure Impact | Isolation / Substitute Boundary | +|-----------------|---------|-------------------|--------------------|----------------|---------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Fact Sources and Observability + +| Fact / Event | Authoritative Source | Observable Location | Consumers | Traceability Requirement | +|--------------|----------------------|---------------------|-----------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Operations and Release Boundaries + +| Operational Concern | Responsible Boundary | Trigger | Affected Views | Architecture Consequence | +|---------------------|----------------------|---------|----------------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Physical View Gaps + +| Gap | Affected Deployment / External Boundary | Why It Matters | +|-----|-----------------------------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Prohibited Content + +Do not write Kubernetes YAML, cloud resource manifests, machine sizes, service SKUs, deployment scripts, runbooks, or concrete infrastructure configuration here. diff --git a/templates/architecture-process-template.md b/templates/architecture-process-template.md new file mode 100644 index 0000000000..143a085c21 --- /dev/null +++ b/templates/architecture-process-template.md @@ -0,0 +1,39 @@ +# Process View + +**Input**: `.specify/memory/architecture-scenario-view.md`, `.specify/memory/architecture-logical-view.md` + +**Purpose**: Derive runtime collaboration, handoffs, approvals, receipts, state advancement, and failure closure from scenario paths and logical boundaries. + +## Main Runtime Links + +| Runtime Link | Trigger | Source | Target | Transferred Content / Fact | Completion Condition | +|--------------|---------|--------|--------|----------------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Handoffs and Approvals + +| Handoff / Approval | From | To | Meaning | Accepted Path | Rejected / Returned Path | +|--------------------|------|----|---------|---------------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Receipts and User Participation + +| Receipt / Participation Point | Sender | Receiver | Content | User Action | Architecture Consequence | +|-------------------------------|--------|----------|---------|-------------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Failure, Degradation, and Closure + +| Failure / Branch | Detection Boundary | Responsible Boundary | Degradation or Compensation | User-Visible Result | Closure Condition | +|------------------|--------------------|----------------------|-----------------------------|---------------------|-------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Process Gaps + +| Gap | Affected Runtime Link / Scenario | Why It Matters | +|-----|----------------------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Prohibited Content + +Do not write call stacks, queue names, retry counts, thread/process details, endpoint sequences, workflow engine configuration, or orchestration code here. diff --git a/templates/architecture-scenario-template.md b/templates/architecture-scenario-template.md new file mode 100644 index 0000000000..359c45244b --- /dev/null +++ b/templates/architecture-scenario-template.md @@ -0,0 +1,37 @@ +# Scenario View + +**Purpose**: Produce the UC semantics for the architecture workflow. This view is the source for the logical, process, development, and physical views. + +## Actors and Participants + +| Actor / Participant | Goal | Responsibility | Boundary | +|---------------------|------|----------------|----------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Use Cases + +| Use Case | Actor | Goal | Preconditions | Scope Boundary | +|----------|-------|------|---------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Scenario Paths + +| Scenario | Main Path | Successful Outcome | Alternative / Failure Branches | +|----------|-----------|--------------------|--------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Acceptance Semantics + +| Acceptance Scenario | Observable Result | Must Hold | Not Covered | +|---------------------|-------------------|-----------|-------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Scenario Gaps + +| Gap | Affected Scenario | Why It Matters | +|-----|-------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Prohibited Content + +Do not write architecture components, class designs, APIs, database tables, implementation tasks, test strategy, deployment scripts, or framework choices here. diff --git a/templates/architecture-template.md b/templates/architecture-template.md new file mode 100644 index 0000000000..9f2395bd94 --- /dev/null +++ b/templates/architecture-template.md @@ -0,0 +1,48 @@ +# Architecture Synthesis: [PROJECT] + +**Input Views**: +- Scenario: `.specify/memory/architecture-scenario-view.md` +- Logical: `.specify/memory/architecture-logical-view.md` +- Process: `.specify/memory/architecture-process-view.md` +- Development: `.specify/memory/architecture-development-view.md` +- Physical: `.specify/memory/architecture-physical-view.md` + +**Note**: This synthesis is filled in by the `__SPECKIT_COMMAND_ARCH__` command after the five 4+1 view files are updated. + +## View Index + +| View | File | Purpose | Current Status | +|------|------|---------|----------------| +| Scenario | `.specify/memory/architecture-scenario-view.md` | UC-producing actor, use case, path, branch, and acceptance semantics | NEEDS ARCH UPDATE | +| Logical | `.specify/memory/architecture-logical-view.md` | Capability boundaries, domain objects, states, and invariants | NEEDS ARCH UPDATE | +| Process | `.specify/memory/architecture-process-view.md` | Runtime links, handoffs, approvals, receipts, failure closure | NEEDS ARCH UPDATE | +| Development | `.specify/memory/architecture-development-view.md` | Architecture-level components, package boundaries, contracts, dependencies | NEEDS ARCH UPDATE | +| Physical | `.specify/memory/architecture-physical-view.md` | Deployment, external systems, fact sources, observability, operations | NEEDS ARCH UPDATE | + +## Architecture Axis + +[Summarize the central design forces that connect the five views: primary scenario flow, authority boundary, fact-source model, collaboration model, deployment constraint, or failure-closure model.] + +## Cross-View Mapping + +| Stable Concept | Scenario View | Logical View | Process View | Development View | Physical View | Architecture Consequence | +|----------------|---------------|--------------|--------------|------------------|---------------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Key Architecture Conclusions + +| Conclusion | Affected Views | Boundary/Owner | Consequence | +|------------|----------------|----------------|-------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Cross-Cutting Constraints + +| Constraint | Source | Affected Views | Scope | Architecture Consequence | +|------------|--------|----------------|-------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Open Risks and Review Triggers + +| Risk or Trigger | Missing Evidence / Change Condition | Affected Views | Required Architecture Review | +|-----------------|-------------------------------------|----------------|------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | diff --git a/templates/commands/agent.md b/templates/commands/agent.md new file mode 100644 index 0000000000..4f1d411767 --- /dev/null +++ b/templates/commands/agent.md @@ -0,0 +1,62 @@ +--- +description: Create or update agent governance and refresh agent instruction projections. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Outline + +You are updating `.specify/memory/agent-governance.md`, the source of truth for how AI agents, skills, MCP tools, and integration adapters operate in this repository. + +**Note**: If `.specify/memory/agent-governance.md` does not exist yet, copy `.specify/templates/agent-governance-template.md` first. + +Follow this execution flow: + +1. Load `.specify/memory/agent-governance.md`. +2. Load supporting context if present: + - `.specify/memory/constitution.md` for project principles and quality gates. + - `.specify/memory/architecture.md` for architecture boundaries. + - `.specify/memory/uc.md` for business semantics. + - `.specify/integration.json` for installed/default integrations. + - Any `SKILL.md` files for skill-local contracts. + - MCP configuration files such as `.mcp.json`, `mcp.json`, `mcp.yml`, or `mcp.yaml`. + - `.specify/extensions.yml` for enabled extensions. +3. Update agent governance: + - Keep the authority order explicit. + - Keep source-of-truth boundaries between constitution, architecture, UC, skills, MCP, and feature artifacts. + - Keep write boundaries testable and concrete. + - Require explicit user intent for mutating MCP calls and external writes. + - Preserve user-authored repo-specific rules unless they conflict with higher authority. +4. Refresh projections: + - `AGENTS.md` + - active integration context files such as `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md`, and other registered `context_file` paths. + - Preserve content outside `` and ``. +5. Produce a Sync Impact Report in `.specify/memory/agent-governance.md`: + - Active/default integration + - Installed integrations + - Skills scanned + - MCP config files scanned + - Projection files refreshed + - Follow-up TODOs + +## Validation + +- No projection file should duplicate long governance text outside the generated projection markers. +- `AGENTS.md` is the repo-level agent governance projection. +- Agent-specific files are adapters that point back to `AGENTS.md`. +- Do not modify `.specify/memory/constitution.md`, `.specify/memory/architecture.md`, `.specify/memory/uc.md`, feature specs, plans, tasks, source code, tests, CI, MCP config, or secrets unless the user explicitly requested that separate change. + +## Output + +Report: + +- Whether `.specify/memory/agent-governance.md` was created or updated. +- Projection files refreshed. +- Skills and MCP config files detected. +- Any unresolved governance risks. diff --git a/templates/commands/arch.md b/templates/commands/arch.md new file mode 100644 index 0000000000..632ea76a30 --- /dev/null +++ b/templates/commands/arch.md @@ -0,0 +1,156 @@ +--- +description: Execute the 4+1 architecture workflow and generate architecture view artifacts. +scripts: + sh: scripts/bash/setup-arch.sh --json + ps: scripts/powershell/setup-arch.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Generate or update the project-level 4+1 architecture artifacts: + +- Main synthesis: `.specify/memory/architecture.md` +- Scenario view: `.specify/memory/architecture-scenario-view.md` +- Logical view: `.specify/memory/architecture-logical-view.md` +- Process view: `.specify/memory/architecture-process-view.md` +- Development view: `.specify/memory/architecture-development-view.md` +- Physical view: `.specify/memory/architecture-physical-view.md` + +The scenario view is the entry point. It produces the UC semantics for this architecture pass: actors, goals, use cases, scenario paths, branches, and acceptance meaning. The other four views are derived from the scenario view. + +## Operating Boundaries + +- Write only the six architecture artifacts listed above. +- Do not require `.specify/memory/uc.md`. If it exists, read it only as supporting reference, not as a hard prerequisite or sole source of truth. +- Do not modify `.specify/memory/uc.md`, `.specify/memory/constitution.md`, feature specs, plans, tasks, source code, tests, or root `docs/`. +- Stay at abstract architecture-design level. +- Do not write concrete classes, files, functions, endpoints, DTO fields, database tables, framework selections, library choices, UI component details, deployment manifests, task breakdowns, test strategy, validation anchors, code notes, deployment scripts, or runbooks. +- If evidence is insufficient, record a specific gap in the affected view instead of inventing business facts, components, interfaces, modules, deployment units, or numeric metrics. + +## Outline + +1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for `ARCH_FILE`, `ARCH_DIR`, `SCENARIO_VIEW`, `LOGICAL_VIEW`, `PROCESS_VIEW`, `DEVELOPMENT_VIEW`, and `PHYSICAL_VIEW`. + +2. **Load context**: + - Read all six architecture artifacts created by setup. + - Read `.specify/memory/uc.md` if present as optional scenario background. + - Read the five view templates under `.specify/templates/`. + +3. **Execute architecture workflow**: + - Phase 0: Fill `SCENARIO_VIEW`. + - Phase 1: Fill `LOGICAL_VIEW` from `SCENARIO_VIEW`. + - Phase 2: Fill `PROCESS_VIEW` from `SCENARIO_VIEW` and `LOGICAL_VIEW`. + - Phase 3: Fill `DEVELOPMENT_VIEW` from `LOGICAL_VIEW` and `PROCESS_VIEW`. + - Phase 4: Fill `PHYSICAL_VIEW` from `PROCESS_VIEW` and `DEVELOPMENT_VIEW`. + - Phase 5: Update `ARCH_FILE` as a synthesis and index over the five views. + +4. **Stop and report**: Report the six updated paths and any explicit unresolved architecture gaps. + +## Phases + +### Phase 0: Scenario View + +**Output**: `.specify/memory/architecture-scenario-view.md` + +Create or update the UC-producing scenario view: + +- Actors and external participants +- Use cases and goals +- Preconditions and scope boundaries +- Main scenario paths +- Alternative and failure branches +- Acceptance semantics +- Open scenario questions + +This phase is authoritative for scenario semantics inside the architecture workflow. Do not defer UC creation to a separate command. + +### Phase 1: Logical View + +**Input**: `.specify/memory/architecture-scenario-view.md` +**Output**: `.specify/memory/architecture-logical-view.md` + +Derive: + +- System capability boundaries +- Domain objects and relationships +- Object ownership and fact sources +- State lifecycle and invariants +- Governance or decision boundaries that are architectural, not organizational process notes + +Do not write class models, DTOs, database tables, field lists, method names, endpoint names, or implementation data structures. + +### Phase 2: Process View + +**Input**: `.specify/memory/architecture-scenario-view.md`, `.specify/memory/architecture-logical-view.md` +**Output**: `.specify/memory/architecture-process-view.md` + +Derive: + +- Main runtime links +- Handoffs and approvals +- Receipts and user participation points +- State advancement across scenario paths +- Failure, degradation, compensation, and closure + +Do not write call stacks, queue names, retry counts, thread/process details, endpoint sequences, or implementation orchestration code. + +### Phase 3: Development View + +**Input**: `.specify/memory/architecture-logical-view.md`, `.specify/memory/architecture-process-view.md` +**Output**: `.specify/memory/architecture-development-view.md` + +Derive: + +- Architecture-level components or capability packages +- Package boundary intent +- Contract and artifact semantics +- Dependency direction and forbidden crossings +- Component responsibility, collaborators, and input/output boundary + +Do not write source file paths, classes, functions, module-by-module implementation tasks, or framework-specific wiring. + +### Phase 4: Physical View + +**Input**: `.specify/memory/architecture-process-view.md`, `.specify/memory/architecture-development-view.md` +**Output**: `.specify/memory/architecture-physical-view.md` + +Derive: + +- Deployment and hosting boundaries +- External system collaboration +- Fact-source placement +- Observability and operational boundaries +- Release or runtime ownership constraints + +Do not write Kubernetes YAML, cloud resource manifests, machine sizes, concrete service SKUs, deployment scripts, or runbooks. + +### Phase 5: Architecture Synthesis + +**Input**: all five view files +**Output**: `architecture.md` + +Update the main synthesis file: + +- View index with links to all five view files +- Architecture axis and central design forces +- Cross-view mapping table +- Key boundaries and constraints +- Open risks and architecture review triggers + +Do not copy every detail from the view files. Summarize the architecture conclusions that connect multiple views. + +## Quality Bar + +- Scenario view must contain enough UC semantics for the other four views to derive from it. +- Every non-placeholder conclusion must be traceable to a scenario, object, runtime link, component boundary, deployment boundary, or stated constraint. +- Use stable names consistently across all five views and the synthesis file. +- Keep uncertainty specific: record what is unknown, which view it affects, and which architecture conclusion cannot yet be made. +- Remove generic statements such as "scalable", "secure", "observable", or "modular" unless they name owner, affected view, scope, and architecture consequence. diff --git a/templates/commands/governance.md b/templates/commands/governance.md new file mode 100644 index 0000000000..d2fa3bdd58 --- /dev/null +++ b/templates/commands/governance.md @@ -0,0 +1,63 @@ +--- +description: Create or update agent governance and refresh agent instruction projections. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Outline + +You are updating repository-level agent governance for this project. The source file is `.specify/memory/agent-governance.md`. Generated agent instruction projections such as `AGENTS.md` and active integration context files are owned by `__SPECKIT_COMMAND_GOVERNANCE__`. + +This command governs agent collaboration, skill usage, MCP/tool permissions, and integration adapter behavior. It MUST NOT redefine project principles or feature requirements. + +**Note**: If `.specify/memory/agent-governance.md` does not exist yet, copy `.specify/templates/agent-governance-template.md` first. + +Follow this execution flow: + +1. Load `.specify/memory/agent-governance.md`. +2. Load supporting context if present: + - `.specify/memory/constitution.md` for project principles and quality gates. + - `.specify/integration.json` for installed/default integrations. + - Any `SKILL.md` files for skill-local contracts. + - MCP configuration files such as `.mcp.json`, `mcp.json`, `mcp.yml`, or `mcp.yaml`. + - `.specify/extensions.yml` for enabled extensions. +3. Update agent governance: + - Keep the authority order explicit. + - Keep source-of-truth boundaries between agent governance, constitution, skills, MCP, and feature artifacts. + - Keep write boundaries testable and concrete. + - Require explicit user intent for mutating MCP calls and external writes. + - Preserve user-authored repo-specific rules unless they conflict with higher authority. +4. Refresh projections: + - `AGENTS.md` + - active integration context files such as `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md`, and other registered `context_file` paths. + - Preserve content outside `` and ``. +5. Produce a Sync Impact Report in `.specify/memory/agent-governance.md`: + - Active/default integration + - Installed integrations + - Skills scanned + - MCP config files scanned + - Projection files refreshed + - Follow-up TODOs + +## Validation + +- No projection file should duplicate long governance text outside the generated projection markers. +- `AGENTS.md` is the repo-level agent governance projection owned by `__SPECKIT_COMMAND_GOVERNANCE__`. +- Agent-specific files are adapters that point back to `AGENTS.md`. +- Specify governance files keep their own authority and must not be rewritten by this command unless the user explicitly requests that separate change. +- Do not modify `.specify/memory/constitution.md`, feature specs, plans, tasks, source code, tests, CI, MCP config, or secrets unless the user explicitly requested that separate change. + +## Output + +Report: + +- Whether `.specify/memory/agent-governance.md` was created or updated. +- Projection files refreshed. +- Skills and MCP config files detected. +- Any unresolved governance risks. diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 52a042161f..7c8dfc6bea 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -13,6 +13,16 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Code-Write Authority + +This command is the only Spec Kit workflow that may write implementation files. +Implementation files include source code, tests, build configuration, migrations, runtime assets, and other files that change repository behavior. + +- Do not write implementation files unless this command is active. +- Bug fixes, refactors, and one-line changes must still enter through this command before implementation files are changed. +- Before writing implementation files, confirm the active feature directory contains `spec.md`, `plan.md`, and `tasks.md`. If any are missing, stop and instruct the user to run the required Spec Kit workflow commands first. +- Writes to `.specify/`, `specs//`, and generated agent command/context files remain governed by their owning Spec Kit workflows. + ## Pre-Execution Checks **Check for extension hooks (before implementation)**: diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index 0b74a6f1a9..e34c1c7d66 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -252,7 +252,7 @@ def test_init_options_includes_context_file(self, tmp_path): # -- Complete file inventory ------------------------------------------ COMMAND_STEMS = [ - "analyze", "checklist", "clarify", "constitution", + "agent", "arch", "governance", "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", ] @@ -274,18 +274,27 @@ def _expected_files(self, script_variant: str) -> list[str]: if script_variant == "sh": for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.sh", - "setup-plan.sh", "setup-tasks.sh"]: + "setup-arch.sh", "setup-plan.sh", "setup-tasks.sh"]: files.append(f".specify/scripts/bash/{name}") else: for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1", - "setup-plan.ps1", "setup-tasks.ps1"]: + "setup-arch.ps1", "setup-plan.ps1", "setup-tasks.ps1"]: files.append(f".specify/scripts/powershell/{name}") - for name in ["checklist-template.md", + for name in ["agent-governance-template.md", + "architecture-development-template.md", + "architecture-logical-template.md", + "architecture-physical-template.md", + "architecture-process-template.md", + "architecture-scenario-template.md", + "architecture-template.md", + "checklist-template.md", "constitution-template.md", "plan-template.md", "spec-template.md", "tasks-template.md"]: files.append(f".specify/templates/{name}") + files.append("AGENTS.md") + files.append(".specify/memory/agent-governance.md") files.append(".specify/memory/constitution.md") # Bundled workflow files.append(".specify/workflows/speckit/workflow.yml") diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 89140de1c3..65b51757fb 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -100,7 +100,7 @@ def test_skill_directory_structure(self, tmp_path): skill_files = [f for f in created if "scripts" not in f.parts] expected_commands = { - "analyze", "checklist", "clarify", "constitution", + "agent", "arch", "governance", "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", } @@ -114,7 +114,7 @@ def test_skill_directory_structure(self, tmp_path): assert actual_commands == expected_commands def test_skill_frontmatter_structure(self, tmp_path): - """SKILL.md must have name, description, compatibility, metadata.""" + """SKILL.md must have governance contract frontmatter.""" i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) created = i.setup(tmp_path, m) @@ -127,8 +127,23 @@ def test_skill_frontmatter_structure(self, tmp_path): fm = yaml.safe_load(parts[1]) assert "name" in fm, f"{f} frontmatter missing 'name'" assert "description" in fm, f"{f} frontmatter missing 'description'" + assert "purpose" in fm, f"{f} frontmatter missing 'purpose'" + assert "trigger" in fm, f"{f} frontmatter missing 'trigger'" + assert "allowed-read-paths" in fm, f"{f} frontmatter missing 'allowed-read-paths'" + assert "allowed-write-paths" in fm, f"{f} frontmatter missing 'allowed-write-paths'" + assert "forbidden-paths" in fm, f"{f} frontmatter missing 'forbidden-paths'" + assert "outputs" in fm, f"{f} frontmatter missing 'outputs'" + assert "validation-command" in fm, f"{f} frontmatter missing 'validation-command'" assert "compatibility" in fm, f"{f} frontmatter missing 'compatibility'" assert "metadata" in fm, f"{f} frontmatter missing 'metadata'" + assert ".specify/**" in fm["allowed-read-paths"] + assert ".git/**" in fm["forbidden-paths"] + if fm["name"] == "speckit-implement": + assert "**" in fm["allowed-write-paths"] + assert "Implementation files, completed tasks.md checkboxes, validation results, and handoff summary" in fm["outputs"] + else: + assert fm["allowed-write-paths"] == [".specify/**", "specs/**"] + assert "Workflow-specific spec artifacts and handoff summary" in fm["outputs"] assert fm["metadata"]["author"] == "github-spec-kit" assert "source" in fm["metadata"] @@ -359,7 +374,7 @@ def test_options_include_skills_flag(self): # -- Complete file inventory ------------------------------------------ _SKILL_COMMANDS = [ - "analyze", "checklist", "clarify", "constitution", + "agent", "arch", "governance", "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", ] @@ -378,6 +393,7 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/integration.json", f".specify/integrations/{self.KEY}.manifest.json", ".specify/integrations/speckit.manifest.json", + ".specify/memory/agent-governance.md", ".specify/memory/constitution.md", ] # Script variant @@ -386,6 +402,7 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/scripts/bash/check-prerequisites.sh", ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", + ".specify/scripts/bash/setup-arch.sh", ".specify/scripts/bash/setup-plan.sh", ".specify/scripts/bash/setup-tasks.sh", ] @@ -394,11 +411,19 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/scripts/powershell/check-prerequisites.ps1", ".specify/scripts/powershell/common.ps1", ".specify/scripts/powershell/create-new-feature.ps1", + ".specify/scripts/powershell/setup-arch.ps1", ".specify/scripts/powershell/setup-plan.ps1", ".specify/scripts/powershell/setup-tasks.ps1", ] # Templates files += [ + ".specify/templates/agent-governance-template.md", + ".specify/templates/architecture-development-template.md", + ".specify/templates/architecture-logical-template.md", + ".specify/templates/architecture-physical-template.md", + ".specify/templates/architecture-process-template.md", + ".specify/templates/architecture-scenario-template.md", + ".specify/templates/architecture-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -413,7 +438,8 @@ def _expected_files(self, script_variant: str) -> list[str]: # Agent context file (if set) if i.context_file: files.append(i.context_file) - return sorted(files) + files.append("AGENTS.md") + return sorted(set(files)) def test_complete_file_inventory_sh(self, tmp_path): """Every file produced by specify init --integration --script sh.""" diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 56862e534c..94f56c125e 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -483,6 +483,9 @@ def test_init_options_includes_context_file(self, tmp_path): # -- Complete file inventory ------------------------------------------ COMMAND_STEMS = [ + "agent", + "arch", + "governance", "analyze", "checklist", "clarify", @@ -515,6 +518,7 @@ def _expected_files(self, script_variant: str) -> list[str]: "check-prerequisites.sh", "common.sh", "create-new-feature.sh", + "setup-arch.sh", "setup-plan.sh", "setup-tasks.sh", ]: @@ -524,12 +528,20 @@ def _expected_files(self, script_variant: str) -> list[str]: "check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1", + "setup-arch.ps1", "setup-plan.ps1", "setup-tasks.ps1", ]: files.append(f".specify/scripts/powershell/{name}") for name in [ + "agent-governance-template.md", + "architecture-development-template.md", + "architecture-logical-template.md", + "architecture-physical-template.md", + "architecture-process-template.md", + "architecture-scenario-template.md", + "architecture-template.md", "checklist-template.md", "constitution-template.md", "plan-template.md", @@ -538,6 +550,8 @@ def _expected_files(self, script_variant: str) -> list[str]: ]: files.append(f".specify/templates/{name}") + files.append("AGENTS.md") + files.append(".specify/memory/agent-governance.md") files.append(".specify/memory/constitution.md") # Bundled workflow files.append(".specify/workflows/speckit/workflow.yml") diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index 956c7a796f..d2d81247a8 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -362,6 +362,9 @@ def test_init_options_includes_context_file(self, tmp_path): # -- Complete file inventory ------------------------------------------ COMMAND_STEMS = [ + "agent", + "arch", + "governance", "analyze", "checklist", "clarify", @@ -394,6 +397,7 @@ def _expected_files(self, script_variant: str) -> list[str]: "check-prerequisites.sh", "common.sh", "create-new-feature.sh", + "setup-arch.sh", "setup-plan.sh", "setup-tasks.sh", ]: @@ -403,12 +407,20 @@ def _expected_files(self, script_variant: str) -> list[str]: "check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1", + "setup-arch.ps1", "setup-plan.ps1", "setup-tasks.ps1", ]: files.append(f".specify/scripts/powershell/{name}") for name in [ + "agent-governance-template.md", + "architecture-development-template.md", + "architecture-logical-template.md", + "architecture-physical-template.md", + "architecture-process-template.md", + "architecture-scenario-template.md", + "architecture-template.md", "checklist-template.md", "constitution-template.md", "plan-template.md", @@ -417,6 +429,8 @@ def _expected_files(self, script_variant: str) -> list[str]: ]: files.append(f".specify/templates/{name}") + files.append("AGENTS.md") + files.append(".specify/memory/agent-governance.md") files.append(".specify/memory/constitution.md") # Bundled workflow files.append(".specify/workflows/speckit/workflow.yml") diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index c6e9259b09..339f970db4 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -29,7 +29,7 @@ def test_setup_creates_agent_md_files(self, tmp_path): m = IntegrationManifest("copilot", tmp_path) created = copilot.setup(tmp_path, m) assert len(created) > 0 - agent_files = [f for f in created if ".agent." in f.name] + agent_files = [f for f in created if f.name.endswith(".agent.md")] assert len(agent_files) > 0 for f in agent_files: assert f.parent == tmp_path / ".github" / "agents" @@ -125,11 +125,11 @@ def test_directory_structure(self, tmp_path): agents_dir = tmp_path / ".github" / "agents" assert agents_dir.is_dir() agent_files = sorted(agents_dir.glob("speckit.*.agent.md")) - assert len(agent_files) == 9 expected_commands = { - "analyze", "checklist", "clarify", "constitution", + "agent", "arch", "governance", "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", } + assert len(agent_files) == len(expected_commands) actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files} assert actual_commands == expected_commands @@ -178,7 +178,11 @@ def test_complete_file_inventory_sh(self, tmp_path): assert result.exit_code == 0 actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()) expected = sorted([ + "AGENTS.md", + ".github/agents/speckit.agent.agent.md", ".github/agents/speckit.analyze.agent.md", + ".github/agents/speckit.arch.agent.md", + ".github/agents/speckit.governance.agent.md", ".github/agents/speckit.checklist.agent.md", ".github/agents/speckit.clarify.agent.md", ".github/agents/speckit.constitution.agent.md", @@ -187,7 +191,10 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/agents/speckit.specify.agent.md", ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", + ".github/prompts/speckit.agent.prompt.md", ".github/prompts/speckit.analyze.prompt.md", + ".github/prompts/speckit.arch.prompt.md", + ".github/prompts/speckit.governance.prompt.md", ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", ".github/prompts/speckit.constitution.prompt.md", @@ -205,13 +212,22 @@ def test_complete_file_inventory_sh(self, tmp_path): ".specify/scripts/bash/check-prerequisites.sh", ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", + ".specify/scripts/bash/setup-arch.sh", ".specify/scripts/bash/setup-plan.sh", ".specify/scripts/bash/setup-tasks.sh", + ".specify/templates/agent-governance-template.md", + ".specify/templates/architecture-development-template.md", + ".specify/templates/architecture-logical-template.md", + ".specify/templates/architecture-physical-template.md", + ".specify/templates/architecture-process-template.md", + ".specify/templates/architecture-scenario-template.md", + ".specify/templates/architecture-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", + ".specify/memory/agent-governance.md", ".specify/memory/constitution.md", ".specify/workflows/speckit/workflow.yml", ".specify/workflows/workflow-registry.json", @@ -238,7 +254,11 @@ def test_complete_file_inventory_ps(self, tmp_path): assert result.exit_code == 0 actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()) expected = sorted([ + "AGENTS.md", + ".github/agents/speckit.agent.agent.md", ".github/agents/speckit.analyze.agent.md", + ".github/agents/speckit.arch.agent.md", + ".github/agents/speckit.governance.agent.md", ".github/agents/speckit.checklist.agent.md", ".github/agents/speckit.clarify.agent.md", ".github/agents/speckit.constitution.agent.md", @@ -247,7 +267,10 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/agents/speckit.specify.agent.md", ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", + ".github/prompts/speckit.agent.prompt.md", ".github/prompts/speckit.analyze.prompt.md", + ".github/prompts/speckit.arch.prompt.md", + ".github/prompts/speckit.governance.prompt.md", ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", ".github/prompts/speckit.constitution.prompt.md", @@ -265,13 +288,22 @@ def test_complete_file_inventory_ps(self, tmp_path): ".specify/scripts/powershell/check-prerequisites.ps1", ".specify/scripts/powershell/common.ps1", ".specify/scripts/powershell/create-new-feature.ps1", + ".specify/scripts/powershell/setup-arch.ps1", ".specify/scripts/powershell/setup-plan.ps1", ".specify/scripts/powershell/setup-tasks.ps1", + ".specify/templates/agent-governance-template.md", + ".specify/templates/architecture-development-template.md", + ".specify/templates/architecture-logical-template.md", + ".specify/templates/architecture-physical-template.md", + ".specify/templates/architecture-process-template.md", + ".specify/templates/architecture-scenario-template.md", + ".specify/templates/architecture-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", + ".specify/memory/agent-governance.md", ".specify/memory/constitution.md", ".specify/workflows/speckit/workflow.yml", ".specify/workflows/workflow-registry.json", @@ -286,7 +318,7 @@ class TestCopilotSkillsMode: """Tests for Copilot integration in --skills mode.""" _SKILL_COMMANDS = [ - "analyze", "checklist", "clarify", "constitution", + "agent", "arch", "governance", "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", ] @@ -604,6 +636,7 @@ def test_complete_file_inventory_skills_sh(self, tmp_path): expected = sorted([ # Skill files *[f".github/skills/speckit-{cmd}/SKILL.md" for cmd in self._SKILL_COMMANDS], + "AGENTS.md", # Context file ".github/copilot-instructions.md", # Integration metadata @@ -615,14 +648,23 @@ def test_complete_file_inventory_skills_sh(self, tmp_path): ".specify/scripts/bash/check-prerequisites.sh", ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", + ".specify/scripts/bash/setup-arch.sh", ".specify/scripts/bash/setup-plan.sh", ".specify/scripts/bash/setup-tasks.sh", # Templates + ".specify/templates/agent-governance-template.md", + ".specify/templates/architecture-development-template.md", + ".specify/templates/architecture-logical-template.md", + ".specify/templates/architecture-physical-template.md", + ".specify/templates/architecture-process-template.md", + ".specify/templates/architecture-scenario-template.md", + ".specify/templates/architecture-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", + ".specify/memory/agent-governance.md", ".specify/memory/constitution.md", # Bundled workflow ".specify/workflows/speckit/workflow.yml", @@ -724,4 +766,4 @@ def test_init_skills_next_steps_show_skill_syntax(self, tmp_path): # Must NOT show the dotted /speckit.plan form assert "/speckit.plan" not in result.output, ( f"Should not show /speckit.plan in skills mode:\n{result.output}" - ) \ No newline at end of file + ) diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index 4f515a01d2..aff63f58aa 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -256,7 +256,10 @@ def test_complete_file_inventory_sh(self, tmp_path): ) expected = sorted([ "AGENTS.md", + ".myagent/commands/speckit.agent.md", ".myagent/commands/speckit.analyze.md", + ".myagent/commands/speckit.arch.md", + ".myagent/commands/speckit.governance.md", ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", ".myagent/commands/speckit.constitution.md", @@ -269,12 +272,21 @@ def test_complete_file_inventory_sh(self, tmp_path): ".specify/integration.json", ".specify/integrations/generic.manifest.json", ".specify/integrations/speckit.manifest.json", + ".specify/memory/agent-governance.md", ".specify/memory/constitution.md", ".specify/scripts/bash/check-prerequisites.sh", ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", + ".specify/scripts/bash/setup-arch.sh", ".specify/scripts/bash/setup-plan.sh", ".specify/scripts/bash/setup-tasks.sh", + ".specify/templates/agent-governance-template.md", + ".specify/templates/architecture-development-template.md", + ".specify/templates/architecture-logical-template.md", + ".specify/templates/architecture-physical-template.md", + ".specify/templates/architecture-process-template.md", + ".specify/templates/architecture-scenario-template.md", + ".specify/templates/architecture-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -312,7 +324,10 @@ def test_complete_file_inventory_ps(self, tmp_path): ) expected = sorted([ "AGENTS.md", + ".myagent/commands/speckit.agent.md", ".myagent/commands/speckit.analyze.md", + ".myagent/commands/speckit.arch.md", + ".myagent/commands/speckit.governance.md", ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", ".myagent/commands/speckit.constitution.md", @@ -325,12 +340,21 @@ def test_complete_file_inventory_ps(self, tmp_path): ".specify/integration.json", ".specify/integrations/generic.manifest.json", ".specify/integrations/speckit.manifest.json", + ".specify/memory/agent-governance.md", ".specify/memory/constitution.md", ".specify/scripts/powershell/check-prerequisites.ps1", ".specify/scripts/powershell/common.ps1", ".specify/scripts/powershell/create-new-feature.ps1", + ".specify/scripts/powershell/setup-arch.ps1", ".specify/scripts/powershell/setup-plan.ps1", ".specify/scripts/powershell/setup-tasks.ps1", + ".specify/templates/agent-governance-template.md", + ".specify/templates/architecture-development-template.md", + ".specify/templates/architecture-logical-template.md", + ".specify/templates/architecture-physical-template.md", + ".specify/templates/architecture-process-template.md", + ".specify/templates/architecture-scenario-template.md", + ".specify/templates/architecture-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", diff --git a/tests/test_agent_projection.py b/tests/test_agent_projection.py new file mode 100644 index 0000000000..83d5f0cef7 --- /dev/null +++ b/tests/test_agent_projection.py @@ -0,0 +1,112 @@ +import json +import shutil +from pathlib import Path + +from specify_cli.agent_projection import ( + AGENT_GOVERNANCE_MEMORY, + PROJECTION_MARKER_START, + ensure_agent_governance_from_template, + refresh_agent_projection, +) + + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _copy_template(project: Path, name: str) -> None: + dest = project / ".specify" / "templates" / name + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(REPO_ROOT / "templates" / name, dest) + + +def test_ensure_agent_governance_from_template(tmp_path): + _copy_template(tmp_path, "agent-governance-template.md") + + result = ensure_agent_governance_from_template(tmp_path) + + assert result == tmp_path / AGENT_GOVERNANCE_MEMORY + content = result.read_text(encoding="utf-8") + assert "# Repository Agent Governance" in content + assert "## Authority Order" in content + assert "Agent code writes are allowed only while executing the generated Spec Kit implement command" in content + assert "/speckit.implement" in content + assert "/speckit-implement" in content + assert "Bug fixes, refactors, and small code changes are not exceptions" in content + assert content.index("2. This repository agent governance file") < content.index( + "3. User-authored repository instructions preserved outside generated projection markers" + ) + + +def test_refresh_agent_projection_creates_repo_and_agent_adapters(tmp_path): + _copy_template(tmp_path, "agent-governance-template.md") + (tmp_path / ".specify" / "integration.json").parent.mkdir(parents=True, exist_ok=True) + (tmp_path / ".specify" / "integration.json").write_text( + json.dumps( + { + "integration": "gemini", + "default_integration": "gemini", + "installed_integrations": ["gemini", "copilot"], + "integration_settings": {}, + } + ), + encoding="utf-8", + ) + (tmp_path / ".gemini" / "commands" / "speckit-test" / "SKILL.md").parent.mkdir( + parents=True, + exist_ok=True, + ) + (tmp_path / ".gemini" / "commands" / "speckit-test" / "SKILL.md").write_text( + "# Test Skill\n", + encoding="utf-8", + ) + (tmp_path / ".mcp.json").write_text("{}", encoding="utf-8") + + result = refresh_agent_projection(tmp_path) + + assert result.memory_path == tmp_path / AGENT_GOVERNANCE_MEMORY + assert (tmp_path / "AGENTS.md").exists() + assert (tmp_path / "GEMINI.md").exists() + assert (tmp_path / ".github" / "copilot-instructions.md").exists() + agents = (tmp_path / "AGENTS.md").read_text(encoding="utf-8") + assert PROJECTION_MARKER_START in agents + assert "Default integration: `gemini`" in agents + assert "Feature work SSOT: `specs//`" in agents + assert "Agent code writes are allowed only while executing the generated Spec Kit implement command" in agents + assert "verify the active change has `spec.md`, `plan.md`, and `tasks.md`" in agents + assert "Architecture SSOT: artifacts produced by `/speckit.arch`" not in agents + assert "Scenario semantics: `/speckit.arch` scenario view" not in agents + assert "Business semantics SSOT: `.specify/memory/uc.md`" not in agents + assert "`.gemini/commands/speckit-test/SKILL.md`" in agents + assert "`.mcp.json`" in agents + + +def test_refresh_agent_projection_preserves_user_content(tmp_path): + _copy_template(tmp_path, "agent-governance-template.md") + agents = tmp_path / "AGENTS.md" + agents.write_text("# Custom Rules\n\nKeep this.\n", encoding="utf-8") + + refresh_agent_projection(tmp_path) + + content = agents.read_text(encoding="utf-8") + assert "# Custom Rules" in content + assert "Keep this." in content + assert PROJECTION_MARKER_START in content + + +def test_refresh_agent_projection_projects_governance_memory_rules(tmp_path): + _copy_template(tmp_path, "agent-governance-template.md") + memory = ensure_agent_governance_from_template(tmp_path) + assert memory is not None + content = memory.read_text(encoding="utf-8") + memory.write_text( + content + "\n## Repository-Specific Rules\n\n- Always report MCP writes before execution.\n", + encoding="utf-8", + ) + + refresh_agent_projection(tmp_path) + + agents = (tmp_path / "AGENTS.md").read_text(encoding="utf-8") + assert "## Repository Agent Governance" in agents + assert "## Repository-Specific Rules" in agents + assert "- Always report MCP writes before execution." in agents + assert "Sync Impact Report" not in agents diff --git a/tests/test_arch_templates.py b/tests/test_arch_templates.py new file mode 100644 index 0000000000..a76dc10d33 --- /dev/null +++ b/tests/test_arch_templates.py @@ -0,0 +1,76 @@ +"""Quality guards for 4+1 architecture templates and command.""" + +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +TEMPLATES = PROJECT_ROOT / "templates" + + +def _read_template(name: str) -> str: + return (TEMPLATES / name).read_text(encoding="utf-8") + + +def test_arch_command_is_phase_based_and_does_not_require_uc_command(): + content = _read_template("commands/arch.md") + + assert "scripts:" in content + assert "setup-arch.sh --json" in content + assert "setup-arch.ps1 -Json" in content + for phase in [ + "Phase 0: Scenario View", + "Phase 1: Logical View", + "Phase 2: Process View", + "Phase 3: Development View", + "Phase 4: Physical View", + "Phase 5: Architecture Synthesis", + ]: + assert phase in content + assert "Do not require `.specify/memory/uc.md`" in content + assert "__SPECKIT_COMMAND_UC__" not in content + assert "Read `.specify/memory/constitution.md`" not in content + assert ".specify/memory/architecture/" not in content + + +def test_architecture_synthesis_references_five_view_files(): + content = _read_template("architecture-template.md") + + for filename in [ + "architecture-scenario-view.md", + "architecture-logical-view.md", + "architecture-process-view.md", + "architecture-development-view.md", + "architecture-physical-view.md", + ]: + assert f".specify/memory/{filename}" in content + assert "Cross-View Mapping" in content + assert "Key Architecture Conclusions" in content + assert ".specify/memory/architecture/" not in content + + +def test_init_next_steps_place_arch_before_constitution(): + init_source = (PROJECT_ROOT / "src" / "specify_cli" / "__init__.py").read_text(encoding="utf-8") + + arch_index = init_source.index("_display_cmd('arch')") + constitution_index = init_source.index("_display_cmd('constitution')") + + assert arch_index < constitution_index + + +def test_view_templates_define_inputs_and_reject_implementation_detail(): + scenario = _read_template("architecture-scenario-template.md") + logical = _read_template("architecture-logical-template.md") + process = _read_template("architecture-process-template.md") + development = _read_template("architecture-development-template.md") + physical = _read_template("architecture-physical-template.md") + + assert "Produce the UC semantics" in scenario + assert "Do not write architecture components" in scenario + assert "**Input**: `.specify/memory/architecture-scenario-view.md`" in logical + assert "Do not write classes, DTOs, database tables" in logical + assert "**Input**: `.specify/memory/architecture-scenario-view.md`, `.specify/memory/architecture-logical-view.md`" in process + assert "Do not write call stacks, queue names, retry counts" in process + assert "**Input**: `.specify/memory/architecture-logical-view.md`, `.specify/memory/architecture-process-view.md`" in development + assert "Do not write source file paths, concrete package trees" in development + assert "**Input**: `.specify/memory/architecture-process-view.md`, `.specify/memory/architecture-development-view.md`" in physical + assert "Do not write Kubernetes YAML, cloud resource manifests" in physical diff --git a/tests/test_setup_arch.py b/tests/test_setup_arch.py new file mode 100644 index 0000000000..3f9a5e3e2b --- /dev/null +++ b/tests/test_setup_arch.py @@ -0,0 +1,166 @@ +"""Tests for setup-arch project-level architecture artifact initialization.""" + +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +from tests.conftest import requires_bash + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +COMMON_SH = PROJECT_ROOT / "scripts" / "bash" / "common.sh" +SETUP_ARCH_SH = PROJECT_ROOT / "scripts" / "bash" / "setup-arch.sh" +COMMON_PS = PROJECT_ROOT / "scripts" / "powershell" / "common.ps1" +SETUP_ARCH_PS = PROJECT_ROOT / "scripts" / "powershell" / "setup-arch.ps1" +ARCH_TEMPLATES = [ + "architecture-template.md", + "architecture-scenario-template.md", + "architecture-logical-template.md", + "architecture-process-template.md", + "architecture-development-template.md", + "architecture-physical-template.md", +] + +HAS_PWSH = shutil.which("pwsh") is not None +_POWERSHELL = shutil.which("powershell.exe") or shutil.which("powershell") + + +def _install_bash_scripts(repo: Path) -> None: + d = repo / ".specify" / "scripts" / "bash" + d.mkdir(parents=True, exist_ok=True) + shutil.copy(COMMON_SH, d / "common.sh") + shutil.copy(SETUP_ARCH_SH, d / "setup-arch.sh") + + +def _install_ps_scripts(repo: Path) -> None: + d = repo / ".specify" / "scripts" / "powershell" + d.mkdir(parents=True, exist_ok=True) + shutil.copy(COMMON_PS, d / "common.ps1") + shutil.copy(SETUP_ARCH_PS, d / "setup-arch.ps1") + + +def _install_templates(repo: Path) -> None: + d = repo / ".specify" / "templates" + d.mkdir(parents=True, exist_ok=True) + for name in ARCH_TEMPLATES: + shutil.copy(PROJECT_ROOT / "templates" / name, d / name) + + +def _clean_env() -> dict[str, str]: + env = os.environ.copy() + for key in list(env): + if key.startswith("SPECIFY_"): + env.pop(key) + return env + + +def _powershell_script_arg(exe: str, script: Path) -> str: + if sys.platform != "win32" and str(exe).endswith("powershell.exe") and shutil.which("wslpath"): + result = subprocess.run( + ["wslpath", "-w", str(script)], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + return str(script) + + +@pytest.fixture +def arch_repo(tmp_path: Path) -> Path: + repo = tmp_path / "proj" + repo.mkdir() + (repo / ".specify").mkdir() + _install_templates(repo) + _install_bash_scripts(repo) + _install_ps_scripts(repo) + return repo + + +def _json_from_output(output: str) -> dict[str, str]: + for line in reversed(output.strip().splitlines()): + line = line.strip() + if line.startswith("{") and line.endswith("}"): + return json.loads(line) + raise AssertionError(f"No JSON object found in output:\n{output}") + + +def _assert_arch_json(repo: Path, data: dict[str, str], *, exact_paths: bool = True) -> None: + expected = { + "ARCH_FILE": repo / ".specify" / "memory" / "architecture.md", + "ARCH_DIR": repo / ".specify" / "memory", + "SCENARIO_VIEW": repo / ".specify" / "memory" / "architecture-scenario-view.md", + "LOGICAL_VIEW": repo / ".specify" / "memory" / "architecture-logical-view.md", + "PROCESS_VIEW": repo / ".specify" / "memory" / "architecture-process-view.md", + "DEVELOPMENT_VIEW": repo / ".specify" / "memory" / "architecture-development-view.md", + "PHYSICAL_VIEW": repo / ".specify" / "memory" / "architecture-physical-view.md", + } + assert set(data) == set(expected) + for key, path in expected.items(): + if exact_paths: + assert Path(data[key]) == path + else: + normalized = data[key].replace("\\", "/") + assert normalized.endswith(path.relative_to(repo).as_posix()) + assert path.is_file() if key != "ARCH_DIR" else path.is_dir() + + +@requires_bash +def test_setup_arch_bash_creates_all_artifacts_and_json(arch_repo: Path) -> None: + script = arch_repo / ".specify" / "scripts" / "bash" / "setup-arch.sh" + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=arch_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + data = _json_from_output(result.stdout) + _assert_arch_json(arch_repo, data) + assert "Scenario View" in (arch_repo / ".specify" / "memory" / "architecture-scenario-view.md").read_text(encoding="utf-8") + + +@requires_bash +def test_setup_arch_bash_preserves_existing_files(arch_repo: Path) -> None: + existing = arch_repo / ".specify" / "memory" / "architecture-scenario-view.md" + existing.parent.mkdir(parents=True) + existing.write_text("# Custom Scenario\n", encoding="utf-8") + + script = arch_repo / ".specify" / "scripts" / "bash" / "setup-arch.sh" + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=arch_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + assert existing.read_text(encoding="utf-8") == "# Custom Scenario\n" + + +@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") +def test_setup_arch_powershell_creates_all_artifacts_and_json(arch_repo: Path) -> None: + script = arch_repo / ".specify" / "scripts" / "powershell" / "setup-arch.ps1" + exe = "pwsh" if HAS_PWSH else _POWERSHELL + result = subprocess.run( + [exe, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", _powershell_script_arg(exe, script), "-Json"], + cwd=arch_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + data = _json_from_output(result.stdout) + _assert_arch_json(arch_repo, data, exact_paths=False)