From adcb67adad1eeb3afb9f36ca18da0bbab3936b8c Mon Sep 17 00:00:00 2001 From: bigben <245982990@qq.com> Date: Tue, 12 May 2026 10:34:17 +0800 Subject: [PATCH 1/3] Add arch and agent command support --- pyproject.toml | 8 +- scripts/bash/setup-arch.sh | 94 ++++++ scripts/powershell/setup-arch.ps1 | 86 +++++ src/specify_cli/__init__.py | 61 +++- src/specify_cli/agent_projection.py | 295 ++++++++++++++++++ src/specify_cli/extensions.py | 1 + .../integrations/claude/__init__.py | 1 + templates/agent-governance-template.md | 67 ++++ .../architecture-development-template.md | 39 +++ templates/architecture-logical-template.md | 39 +++ templates/architecture-physical-template.md | 39 +++ templates/architecture-process-template.md | 39 +++ templates/architecture-scenario-template.md | 37 +++ templates/architecture-template.md | 48 +++ templates/commands/agent.md | 63 ++++ templates/commands/arch.md | 156 +++++++++ .../test_integration_base_markdown.py | 14 +- .../test_integration_base_skills.py | 12 +- .../test_integration_base_toml.py | 9 + .../test_integration_base_yaml.py | 9 + .../integrations/test_integration_copilot.py | 33 +- .../integrations/test_integration_generic.py | 16 + tests/test_agent_projection.py | 81 +++++ tests/test_arch_templates.py | 76 +++++ tests/test_setup_arch.py | 166 ++++++++++ 25 files changed, 1473 insertions(+), 16 deletions(-) create mode 100755 scripts/bash/setup-arch.sh create mode 100755 scripts/powershell/setup-arch.ps1 create mode 100644 src/specify_cli/agent_projection.py create mode 100644 templates/agent-governance-template.md create mode 100644 templates/architecture-development-template.md create mode 100644 templates/architecture-logical-template.md create mode 100644 templates/architecture-physical-template.md create mode 100644 templates/architecture-process-template.md create mode 100644 templates/architecture-scenario-template.md create mode 100644 templates/architecture-template.md create mode 100644 templates/commands/agent.md create mode 100644 templates/commands/arch.md create mode 100644 tests/test_agent_projection.py create mode 100644 tests/test_arch_templates.py create mode 100644 tests/test_setup_arch.py diff --git a/pyproject.toml b/pyproject.toml index d7a949d8b1..c213843453 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 325692900e..031119ebdd 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 @@ -906,6 +910,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" @@ -951,6 +995,8 @@ 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.", "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.", @@ -1339,6 +1385,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") @@ -1607,11 +1655,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() @@ -1975,6 +2024,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: @@ -1993,6 +2043,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..8f2fb1cb54 --- /dev/null +++ b/src/specify_cli/agent_projection.py @@ -0,0 +1,295 @@ +"""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) + + lines = [ + PROJECTION_MARKER_START, + "# 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"- Agent governance SSOT: `{AGENT_GOVERNANCE_MEMORY}`", + "- Project principles: `.specify/memory/constitution.md`", + "- Business semantics: `.specify/memory/uc.md`", + "- Architecture boundaries: `.specify/memory/architecture.md`", + "", + "## 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 agent, skill, and MCP behavior.", + "- Treat `.specify/memory/constitution.md` as the source of truth for project principles and quality gates.", + "- 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 _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 "# Repo Agent Governance\n\nThis file is the repository-level agent governance projection." + if name == "CLAUDE.md": + return "# Claude Instructions\n\nRead `AGENTS.md` first; it is the repository-level agent governance projection." + if name == "GEMINI.md": + return "# Gemini Instructions\n\nRead `AGENTS.md` first; it is the repository-level agent governance projection." + if name == "copilot-instructions.md": + return "# GitHub Copilot Instructions\n\nRead `AGENTS.md` first; it is the repository-level agent governance projection." + 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.\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/extensions.py b/src/specify_cli/extensions.py index 944ee4a06d..c3f3617643 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -27,6 +27,7 @@ _FALLBACK_CORE_COMMAND_NAMES = frozenset({ "analyze", + "arch", "checklist", "clarify", "constitution", diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 88aef85285..b45a4963da 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", diff --git a/templates/agent-governance-template.md b/templates/agent-governance-template.md new file mode 100644 index 0000000000..71c6545676 --- /dev/null +++ b/templates/agent-governance-template.md @@ -0,0 +1,67 @@ +# Agent Governance + + + +## Authority Order + +1. Current user instruction +2. This agent governance file +3. `.specify/memory/constitution.md` +4. `.specify/memory/architecture.md` +5. `.specify/memory/uc.md` +6. Active feature artifacts under `specs//` +7. Skill-local `SKILL.md` +8. Tool/MCP defaults + +## Source Of Truth + +- Project principles: `.specify/memory/constitution.md` +- Business semantics: `.specify/memory/uc.md` +- Architecture boundaries: `.specify/memory/architecture.md` +- Feature work: `specs//` +- Agent operations: `.specify/memory/agent-governance.md` +- Skill contracts: each `SKILL.md` +- MCP permissions: MCP configuration and allowlists + +## Write Boundaries + +- 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..53db5b78b9 --- /dev/null +++ b/templates/commands/agent.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 `.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/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index 0b74a6f1a9..ecfeefeb4b 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", + "analyze", "arch", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", ] @@ -274,14 +274,20 @@ 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 ["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}") diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 89140de1c3..9b2484f3dc 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", + "analyze", "arch", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", } @@ -359,7 +359,7 @@ def test_options_include_skills_flag(self): # -- Complete file inventory ------------------------------------------ _SKILL_COMMANDS = [ - "analyze", "checklist", "clarify", "constitution", + "analyze", "arch", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", ] @@ -386,6 +386,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 +395,18 @@ 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/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/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 56862e534c..689878a3da 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -484,6 +484,7 @@ def test_init_options_includes_context_file(self, tmp_path): COMMAND_STEMS = [ "analyze", + "arch", "checklist", "clarify", "constitution", @@ -515,6 +516,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 +526,19 @@ 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 [ + "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", diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index 956c7a796f..3111b6b78a 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -363,6 +363,7 @@ def test_init_options_includes_context_file(self, tmp_path): COMMAND_STEMS = [ "analyze", + "arch", "checklist", "clarify", "constitution", @@ -394,6 +395,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 +405,19 @@ 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 [ + "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", diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index c6e9259b09..e7d47cfc68 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -125,9 +125,9 @@ 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 + assert len(agent_files) == 10 expected_commands = { - "analyze", "checklist", "clarify", "constitution", + "analyze", "arch", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", } actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files} @@ -179,6 +179,7 @@ def test_complete_file_inventory_sh(self, tmp_path): actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()) expected = sorted([ ".github/agents/speckit.analyze.agent.md", + ".github/agents/speckit.arch.agent.md", ".github/agents/speckit.checklist.agent.md", ".github/agents/speckit.clarify.agent.md", ".github/agents/speckit.constitution.agent.md", @@ -188,6 +189,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", ".github/prompts/speckit.analyze.prompt.md", + ".github/prompts/speckit.arch.prompt.md", ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", ".github/prompts/speckit.constitution.prompt.md", @@ -205,8 +207,15 @@ 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/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", @@ -239,6 +248,7 @@ def test_complete_file_inventory_ps(self, tmp_path): actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()) expected = sorted([ ".github/agents/speckit.analyze.agent.md", + ".github/agents/speckit.arch.agent.md", ".github/agents/speckit.checklist.agent.md", ".github/agents/speckit.clarify.agent.md", ".github/agents/speckit.constitution.agent.md", @@ -248,6 +258,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", ".github/prompts/speckit.analyze.prompt.md", + ".github/prompts/speckit.arch.prompt.md", ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", ".github/prompts/speckit.constitution.prompt.md", @@ -265,8 +276,15 @@ 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/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", @@ -286,7 +304,7 @@ class TestCopilotSkillsMode: """Tests for Copilot integration in --skills mode.""" _SKILL_COMMANDS = [ - "analyze", "checklist", "clarify", "constitution", + "analyze", "arch", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", ] @@ -615,9 +633,16 @@ 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/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", @@ -724,4 +749,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..9ce5964e04 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -257,6 +257,7 @@ def test_complete_file_inventory_sh(self, tmp_path): expected = sorted([ "AGENTS.md", ".myagent/commands/speckit.analyze.md", + ".myagent/commands/speckit.arch.md", ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", ".myagent/commands/speckit.constitution.md", @@ -273,8 +274,15 @@ 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/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", @@ -313,6 +321,7 @@ def test_complete_file_inventory_ps(self, tmp_path): expected = sorted([ "AGENTS.md", ".myagent/commands/speckit.analyze.md", + ".myagent/commands/speckit.arch.md", ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", ".myagent/commands/speckit.constitution.md", @@ -329,8 +338,15 @@ 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/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..cac2e14ba9 --- /dev/null +++ b/tests/test_agent_projection.py @@ -0,0 +1,81 @@ +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 "# Agent Governance" in content + assert "## Authority Order" in content + + +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 "`.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 + 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) From 57f509248c4da0fef9bb3f651cca968a2ebdade2 Mon Sep 17 00:00:00 2001 From: bigben <245982990@qq.com> Date: Tue, 12 May 2026 14:57:40 +0800 Subject: [PATCH 2/3] Add agent governance projection support --- src/specify_cli/__init__.py | 49 +++ src/specify_cli/agent_projection.py | 329 ++++++++++++++++++ src/specify_cli/agents.py | 39 ++- src/specify_cli/extensions.py | 1 + src/specify_cli/integrations/base.py | 28 +- .../integrations/claude/__init__.py | 5 +- templates/agent-governance-template.md | 72 ++++ templates/commands/governance.md | 63 ++++ templates/commands/implement.md | 10 + .../test_integration_base_markdown.py | 7 +- .../test_integration_base_skills.py | 26 +- .../test_integration_base_toml.py | 4 + .../test_integration_base_yaml.py | 4 + .../integrations/test_integration_copilot.py | 21 +- .../integrations/test_integration_generic.py | 6 + tests/test_agent_projection.py | 112 ++++++ 16 files changed, 745 insertions(+), 31 deletions(-) create mode 100644 src/specify_cli/agent_projection.py create mode 100644 templates/agent-governance-template.md create mode 100644 templates/commands/governance.md create mode 100644 tests/test_agent_projection.py diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 325692900e..3c4c2a0e36 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 @@ -906,6 +910,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" @@ -951,6 +995,7 @@ 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 = { + "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.", @@ -1339,6 +1384,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") @@ -1975,6 +2022,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: @@ -1993,6 +2041,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..924361ad74 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -26,6 +26,7 @@ from packaging.specifiers import SpecifierSet, InvalidSpecifier _FALLBACK_CORE_COMMAND_NAMES = frozenset({ + "governance", "analyze", "checklist", "clarify", 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..8b888df6c8 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -30,6 +30,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 +114,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/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..26ed89726a 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", + "governance", "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", ] @@ -281,11 +281,14 @@ def _expected_files(self, script_variant: str) -> list[str]: "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", + "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..1670518505 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", + "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", + "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 @@ -399,6 +415,7 @@ def _expected_files(self, script_variant: str) -> list[str]: ] # Templates files += [ + ".specify/templates/agent-governance-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -413,7 +430,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..1efb37b7c3 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -483,6 +483,7 @@ def test_init_options_includes_context_file(self, tmp_path): # -- Complete file inventory ------------------------------------------ COMMAND_STEMS = [ + "governance", "analyze", "checklist", "clarify", @@ -530,6 +531,7 @@ def _expected_files(self, script_variant: str) -> list[str]: files.append(f".specify/scripts/powershell/{name}") for name in [ + "agent-governance-template.md", "checklist-template.md", "constitution-template.md", "plan-template.md", @@ -538,6 +540,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..3b2b9f9ff8 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -362,6 +362,7 @@ def test_init_options_includes_context_file(self, tmp_path): # -- Complete file inventory ------------------------------------------ COMMAND_STEMS = [ + "governance", "analyze", "checklist", "clarify", @@ -409,6 +410,7 @@ def _expected_files(self, script_variant: str) -> list[str]: files.append(f".specify/scripts/powershell/{name}") for name in [ + "agent-governance-template.md", "checklist-template.md", "constitution-template.md", "plan-template.md", @@ -417,6 +419,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..39a9dd047b 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -125,9 +125,9 @@ 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 + assert len(agent_files) == 10 expected_commands = { - "analyze", "checklist", "clarify", "constitution", + "governance", "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", } actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files} @@ -178,7 +178,9 @@ 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.analyze.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", @@ -188,6 +190,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", ".github/prompts/speckit.analyze.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", @@ -207,11 +210,13 @@ def test_complete_file_inventory_sh(self, tmp_path): ".specify/scripts/bash/create-new-feature.sh", ".specify/scripts/bash/setup-plan.sh", ".specify/scripts/bash/setup-tasks.sh", + ".specify/templates/agent-governance-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 +243,9 @@ 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.analyze.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", @@ -248,6 +255,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", ".github/prompts/speckit.analyze.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", @@ -267,11 +275,13 @@ def test_complete_file_inventory_ps(self, tmp_path): ".specify/scripts/powershell/create-new-feature.ps1", ".specify/scripts/powershell/setup-plan.ps1", ".specify/scripts/powershell/setup-tasks.ps1", + ".specify/templates/agent-governance-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 +296,7 @@ class TestCopilotSkillsMode: """Tests for Copilot integration in --skills mode.""" _SKILL_COMMANDS = [ - "analyze", "checklist", "clarify", "constitution", + "governance", "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", ] @@ -604,6 +614,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 @@ -618,11 +629,13 @@ def test_complete_file_inventory_skills_sh(self, tmp_path): ".specify/scripts/bash/setup-plan.sh", ".specify/scripts/bash/setup-tasks.sh", # Templates + ".specify/templates/agent-governance-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 +737,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..5902756fd8 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -257,6 +257,7 @@ def test_complete_file_inventory_sh(self, tmp_path): expected = sorted([ "AGENTS.md", ".myagent/commands/speckit.analyze.md", + ".myagent/commands/speckit.governance.md", ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", ".myagent/commands/speckit.constitution.md", @@ -269,12 +270,14 @@ 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-plan.sh", ".specify/scripts/bash/setup-tasks.sh", + ".specify/templates/agent-governance-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -313,6 +316,7 @@ def test_complete_file_inventory_ps(self, tmp_path): expected = sorted([ "AGENTS.md", ".myagent/commands/speckit.analyze.md", + ".myagent/commands/speckit.governance.md", ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", ".myagent/commands/speckit.constitution.md", @@ -325,12 +329,14 @@ 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-plan.ps1", ".specify/scripts/powershell/setup-tasks.ps1", + ".specify/templates/agent-governance-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 From 9165712809369c549de4e0664622c0a9dff8f783 Mon Sep 17 00:00:00 2001 From: bigben <245982990@qq.com> Date: Wed, 13 May 2026 11:13:11 +0800 Subject: [PATCH 3/3] Refine arch command as architecture SSOT --- .../architecture-development-template.md | 34 +++++ templates/architecture-logical-template.md | 34 +++++ templates/architecture-physical-template.md | 34 +++++ templates/architecture-process-template.md | 34 +++++ templates/architecture-scenario-template.md | 34 +++++ templates/architecture-template.md | 38 +++++- templates/commands/arch.md | 125 +++++++++--------- tests/test_arch_templates.py | 93 ++++++++++++- 8 files changed, 358 insertions(+), 68 deletions(-) diff --git a/templates/architecture-development-template.md b/templates/architecture-development-template.md index d9f8c6976c..59bcb47136 100644 --- a/templates/architecture-development-template.md +++ b/templates/architecture-development-template.md @@ -4,6 +4,40 @@ **Purpose**: Derive architecture-level components, package boundary intent, contract/artifact semantics, and dependency rules from logical and process views. +## Architecture Intent + +[State what component, package, contract, or dependency meaning this view must preserve.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Development Consequence | +|---------|----------------------------|-------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Must Not Own | +|----------|----------------------------|-------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Development Impact | +|-----------------|-------------|--------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Source Boundary / Contract / Dependency Rule | Risk If Violated | +|-----------|----------------------------------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Architecture-Level Components | Component / Capability Package | Responsibility | Input / Output Boundary | Collaborators | Explicitly Must Not Own | Source View Evidence | diff --git a/templates/architecture-logical-template.md b/templates/architecture-logical-template.md index 057874c3e1..3e6514b850 100644 --- a/templates/architecture-logical-template.md +++ b/templates/architecture-logical-template.md @@ -4,6 +4,40 @@ **Purpose**: Derive capability boundaries, domain objects, states, relationships, and invariants from the scenario view. +## Architecture Intent + +[State what logical separation, authority, or lifecycle meaning this view must preserve.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Logical Consequence | +|---------|----------------------------|---------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Does Not Own | +|----------|----------------------------|-------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Logical Impact | +|-----------------|-------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Source Scenario / Object / State | Risk If Violated | +|-----------|----------------------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Capability Boundaries | Capability / Boundary | Responsibility | Input | Output | Explicitly Does Not Own | Scenario Source | diff --git a/templates/architecture-physical-template.md b/templates/architecture-physical-template.md index 9271d3f76c..a300eaaaa5 100644 --- a/templates/architecture-physical-template.md +++ b/templates/architecture-physical-template.md @@ -4,6 +4,40 @@ **Purpose**: Derive deployment, hosting, external system, fact-source, observability, and operational boundaries from process and development views. +## Architecture Intent + +[State what deployment, fact-source, operational, or external-boundary meaning this view must preserve.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Physical Consequence | +|---------|----------------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Does Not Carry | +|----------|----------------------------|---------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Physical Impact | +|-----------------|-------------|-----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Source Deployment / External / Fact Boundary | Risk If Violated | +|-----------|----------------------------------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Deployment and Hosting Boundaries | Runtime / Hosting Unit | Carries | Boundary | Depends On | Release / Migration Impact | diff --git a/templates/architecture-process-template.md b/templates/architecture-process-template.md index 143a085c21..a24bce6ca8 100644 --- a/templates/architecture-process-template.md +++ b/templates/architecture-process-template.md @@ -4,6 +4,40 @@ **Purpose**: Derive runtime collaboration, handoffs, approvals, receipts, state advancement, and failure closure from scenario paths and logical boundaries. +## Architecture Intent + +[State what runtime collaboration, handoff, or failure-closure meaning this view must preserve.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Process Consequence | +|---------|----------------------------|---------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Does Not Control | +|----------|----------------------------|-----------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Process Impact | +|-----------------|-------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Source Scenario / Runtime Link | Risk If Violated | +|-----------|--------------------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Main Runtime Links | Runtime Link | Trigger | Source | Target | Transferred Content / Fact | Completion Condition | diff --git a/templates/architecture-scenario-template.md b/templates/architecture-scenario-template.md index 359c45244b..08026e3258 100644 --- a/templates/architecture-scenario-template.md +++ b/templates/architecture-scenario-template.md @@ -2,6 +2,40 @@ **Purpose**: Produce the UC semantics for the architecture workflow. This view is the source for the logical, process, development, and physical views. +## Architecture Intent + +[State what scenario-level meaning this view must stabilize for later architecture decisions.] + +## Core Tensions + +| Tension | Current Tradeoff Direction | Scenario Consequence | +|---------|----------------------------|----------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Must Remain Stable Because | Explicitly Does Not Cover | +|----------|----------------------------|---------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Scenario Impact | +|-----------------|-------------|-----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Invariants + +| Invariant | Scenario Evidence | Risk If Violated | +|-----------|-------------------|------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Non-goals / Anti-patterns + +| Non-goal / Anti-pattern | Why It Is Out of Scope or Harmful | +|-------------------------|-----------------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + ## Actors and Participants | Actor / Participant | Goal | Responsibility | Boundary | diff --git a/templates/architecture-template.md b/templates/architecture-template.md index 9f2395bd94..c0bbd5a100 100644 --- a/templates/architecture-template.md +++ b/templates/architecture-template.md @@ -19,14 +19,44 @@ | 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 +## Architecture Intent + +[State what architecture intent the five views stabilize together.] + +## Central Design Forces [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 +## Primary Tradeoffs + +| Tradeoff | Chosen Direction | Consequence | Revisit When | +|----------|------------------|-------------|--------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Stable Boundaries + +| Boundary | Affected Views | Must Remain Stable Because | Forbidden Crossing | +|----------|----------------|----------------------------|--------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Change Axes + +| Expected Change | Isolated By | Affected Views | Architecture Consequence | +|-----------------|-------------|----------------|--------------------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Anti-patterns + +| Anti-pattern | Why It Violates Intent | Affected Views | +|--------------|------------------------|----------------| +| NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | + +## Cross-View Architecture Model + +This section normalizes the 4+1 design results into the architecture SSOT for later `plan` reasoning. Record how concepts derive, constrain, depend on, or guard each other. This is architecture design synthesis, not tracking or audit. Do not treat view-specific concepts as equivalent or interchangeable. -| Stable Concept | Scenario View | Logical View | Process View | Development View | Physical View | Architecture Consequence | -|----------------|---------------|--------------|--------------|------------------|---------------|--------------------------| +| Architecture Concept | Scenario Meaning | Logical Interpretation | Runtime Role | Development Boundary | Physical Constraint | Plan Reasoning Constraint | +|----------------------|------------------|------------------------|--------------|----------------------|---------------------|---------------------------| | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | NEEDS ARCH UPDATE | ## Key Architecture Conclusions diff --git a/templates/commands/arch.md b/templates/commands/arch.md index 632ea76a30..4361839bff 100644 --- a/templates/commands/arch.md +++ b/templates/commands/arch.md @@ -15,7 +15,7 @@ You **MUST** consider the user input before proceeding (if not empty). ## Goal -Generate or update the project-level 4+1 architecture artifacts: +Generate or update the project-level architecture SSOT as 4+1 architecture artifacts: - Main synthesis: `.specify/memory/architecture.md` - Scenario view: `.specify/memory/architecture-scenario-view.md` @@ -26,6 +26,8 @@ Generate or update the project-level 4+1 architecture artifacts: 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. +The six artifacts are the authoritative architecture design source for later iterations. They serve two purposes: produce high-quality 4+1 architecture reasoning in this command, and constrain later `plan` reasoning to stay inside the architecture SSOT. + ## Operating Boundaries - Write only the six architecture artifacts listed above. @@ -35,6 +37,21 @@ The scenario view is the entry point. It produces the UC semantics for this arch - 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. +## Architecture Layers + +### Architecture Reasoning Layer + +Reason only in the 4+1 views. Use each view template as the source of truth for that view's reasoning contract and artifact structure. Produce architecture design inference, not tracking, audit, or implementation planning. Maintain a cross-view architecture model that normalizes architecture meaning for synthesis and later `plan` reasoning while preserving each view's distinct concept type. Every conclusion must still be grounded in a scenario, object, state, collaboration, boundary, deployment constraint, or stated external constraint. Do not translate, rename, or equate concepts across views through notation-specific terms. + +### Representation Layer + +Markdown tables are the default artifact structure. Optional diagrams are renderings, not reasoning inputs. + +- Add optional diagrams only after the relevant view's reasoning is complete. +- Render only facts already present in that view. +- Do not introduce concepts, boundaries, relationships, deployment units, or cross-view concept alignments. +- C4, UML, Mermaid, and PlantUML are notation choices only; they must not change 4+1 view responsibilities. + ## 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`. @@ -42,115 +59,97 @@ The scenario view is the entry point. It produces the UC semantics for this arch 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/`. + - Read the six architecture 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. + - Phase -1: Establish architecture framing before writing any view. + - Phase 0: Fill `SCENARIO_VIEW` using its template. + - Phase 1: Fill `LOGICAL_VIEW` using its template and `SCENARIO_VIEW`. + - Phase 2: Fill `PROCESS_VIEW` using its template, `SCENARIO_VIEW`, and `LOGICAL_VIEW`. + - Phase 3: Fill `DEVELOPMENT_VIEW` using its template, `LOGICAL_VIEW`, and `PROCESS_VIEW`. + - Phase 4: Fill `PHYSICAL_VIEW` using its template, `PROCESS_VIEW`, and `DEVELOPMENT_VIEW`. + - Phase 5: Update `ARCH_FILE` using its synthesis template and the five completed views. 4. **Stop and report**: Report the six updated paths and any explicit unresolved architecture gaps. ## Phases -### Phase 0: Scenario View +### Phase -1: Architecture Framing -**Output**: `.specify/memory/architecture-scenario-view.md` +**Output**: Working framing used to constrain all five views and the synthesis. Do not create an additional file. + +Before filling any view, identify the architecture judgment this pass must preserve: + +- View dimensions: scenario, logical, process, development, and physical +- Architecture intent: what this architecture pass is trying to make stable or explicit +- Core tensions: the main design forces in conflict, and the current tradeoff direction +- Stable boundaries: responsibilities or authority lines that should remain stable across iterations +- Change axes: business, workflow, operational, or integration areas expected to vary and therefore needing isolation +- Responsibility collision risks: responsibilities that agents or teams are likely to merge incorrectly +- Invariants: architecture rules later iterations must not violate +- Non-goals / anti-patterns: concerns this architecture pass does not solve, plus designs that would drift from the intent +- Implementation details to exclude: concrete classes, files, APIs, data schemas, frameworks, infrastructure manifests, or task plans that must stay out of the architecture layer -Create or update the UC-producing scenario view: +Use this framing as the decision filter for every later phase. If a view cannot support a framing claim with scenario, boundary, lifecycle, collaboration, component, deployment, or stated-constraint evidence, record a specific gap instead of inventing facts. +Defer any optional diagram or notation-specific rendering until the affected view's 4+1 reasoning is complete. -- Actors and external participants -- Use cases and goals -- Preconditions and scope boundaries -- Main scenario paths -- Alternative and failure branches -- Acceptance semantics -- Open scenario questions +### Phase 0: Scenario View + +**Output**: `.specify/memory/architecture-scenario-view.md` -This phase is authoritative for scenario semantics inside the architecture workflow. Do not defer UC creation to a separate command. +Create or update the UC-producing scenario view by following the scenario view template. 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. +Derive the logical view from the scenario view by following the logical view template. ### 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. +Derive the process view from the scenario and logical views by following the process view template. ### 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. +Derive the development view from the logical and process views by following the development view template. ### 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. +Derive the physical view from the process and development views by following the physical view template. ### Phase 5: Architecture Synthesis **Input**: all five view files **Output**: `architecture.md` -Update the main synthesis file: +Update the main synthesis file by following the synthesis template. Do not copy every detail from the view files. Summarize the architecture conclusions that connect multiple views. -- 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 +## Architecture Gates -Do not copy every detail from the view files. Summarize the architecture conclusions that connect multiple views. +- ERROR if any view contains implementation details prohibited by the Operating Boundaries. +- ERROR if a boundary has responsibilities but no explicit non-responsibility or forbidden crossing. +- ERROR if a major architecture decision lacks consequence, tradeoff, or affected view. +- ERROR if an invariant is not tied to a scenario, state, boundary, collaboration, deployment constraint, or stated external constraint. +- ERROR if default tables or optional diagrams introduce concepts, relationships, or deployment units not justified by the framing or source views. +- ERROR if notation-specific output changes 4+1 view responsibilities or introduces architecture conclusions. +- ERROR if the cross-view architecture model erases the distinct meaning of a view-specific concept. +- ERROR if the workflow maps Use Case, Domain Object, Component, Container, or Deployment Unit as equivalent concepts across views. +- Record a specific gap instead of inventing business facts, authority boundaries, lifecycle rules, components, interfaces, deployment units, or numeric metrics. ## 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. +- Every non-placeholder conclusion must be grounded in 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/tests/test_arch_templates.py b/tests/test_arch_templates.py index a76dc10d33..cdd32f0e6f 100644 --- a/tests/test_arch_templates.py +++ b/tests/test_arch_templates.py @@ -1,5 +1,6 @@ """Quality guards for 4+1 architecture templates and command.""" +import re from pathlib import Path @@ -18,6 +19,7 @@ def test_arch_command_is_phase_based_and_does_not_require_uc_command(): assert "setup-arch.sh --json" in content assert "setup-arch.ps1 -Json" in content for phase in [ + "Phase -1: Architecture Framing", "Phase 0: Scenario View", "Phase 1: Logical View", "Phase 2: Process View", @@ -26,12 +28,62 @@ def test_arch_command_is_phase_based_and_does_not_require_uc_command(): "Phase 5: Architecture Synthesis", ]: assert phase in content + assert "Before filling any view, identify the architecture judgment" in content + assert "Architecture Reasoning Layer" in content + assert "Representation Layer" in content + assert "project-level architecture SSOT" in content + assert "constrain later `plan` reasoning to stay inside the architecture SSOT" in content + assert "Use each view template as the source of truth for that view's reasoning contract" in content + assert "Produce architecture design inference, not tracking, audit, or implementation planning" in content + assert "normalizes architecture meaning for synthesis and later `plan` reasoning" in content + assert "Markdown tables are the default artifact structure" in content + assert "Optional diagrams are renderings, not reasoning inputs" in content + assert "Add optional diagrams only after the relevant view's reasoning is complete" in content + assert "Defer any optional diagram or notation-specific rendering until the affected view's 4+1 reasoning" in content + assert "Representation choices:" not in content + assert "Before filling tables" not in content + assert "Architecture Gates" in content + assert "ERROR if a boundary has responsibilities but no explicit non-responsibility" in content + assert "ERROR if notation-specific output changes 4+1 view responsibilities" in content + assert "Use Case, Domain Object, Component, Container, or Deployment Unit" in content + for term in ["C4", "UML", "Mermaid", "PlantUML"]: + assert len(re.findall(rf"\b{re.escape(term)}\b", content)) == 1 assert "Do not require `.specify/memory/uc.md`" in content + assert "Read the six architecture templates under `.specify/templates/`" 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_arch_command_delegates_view_details_to_templates(): + content = _read_template("commands/arch.md") + + delegated_phrases = [ + "using its template", + "following the scenario view template", + "following the logical view template", + "following the process view template", + "following the development view template", + "following the physical view template", + "following the synthesis template", + ] + for phrase in delegated_phrases: + assert phrase in content + + template_owned_details = [ + "Actors and external participants", + "System capability boundaries", + "Main runtime links", + "Architecture-level components or capability packages", + "Deployment and hosting boundaries", + "Do not write class models", + "Do not write call stacks", + "Do not write Kubernetes YAML", + ] + for phrase in template_owned_details: + assert phrase not in content + + def test_architecture_synthesis_references_five_view_files(): content = _read_template("architecture-template.md") @@ -43,8 +95,21 @@ def test_architecture_synthesis_references_five_view_files(): "architecture-physical-view.md", ]: assert f".specify/memory/{filename}" in content - assert "Cross-View Mapping" in content + assert "Cross-View Architecture Model" in content + assert "normalizes the 4+1 design results into the architecture SSOT for later `plan` reasoning" in content + assert "This is architecture design synthesis, not tracking or audit" in content + assert "Do not treat view-specific concepts as equivalent or interchangeable" in content + assert "Plan Reasoning Constraint" in content assert "Key Architecture Conclusions" in content + for section in [ + "Architecture Intent", + "Central Design Forces", + "Primary Tradeoffs", + "Stable Boundaries", + "Change Axes", + "Anti-patterns", + ]: + assert section in content assert ".specify/memory/architecture/" not in content @@ -74,3 +139,29 @@ def test_view_templates_define_inputs_and_reject_implementation_detail(): 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 + + for content in [scenario, logical, process, development, physical]: + for section in [ + "Architecture Intent", + "Core Tensions", + "Stable Boundaries", + "Change Axes", + "Invariants", + "Non-goals / Anti-patterns", + ]: + assert section in content + + +def test_view_templates_keep_notations_out_of_reasoning_contracts(): + view_contents = [ + _read_template("architecture-scenario-template.md"), + _read_template("architecture-logical-template.md"), + _read_template("architecture-process-template.md"), + _read_template("architecture-development-template.md"), + _read_template("architecture-physical-template.md"), + ] + + notation_terms = ["C4", "UML", "Mermaid", "PlantUML", "notation-specific"] + for content in view_contents: + for term in notation_terms: + assert term not in content