Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -70,4 +77,3 @@ omit = ["*/tests/*", "*/__pycache__/*"]
precision = 2
show_missing = true
skip_covered = false

94 changes: 94 additions & 0 deletions scripts/bash/setup-arch.sh
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions scripts/powershell/setup-arch.ps1
Original file line number Diff line number Diff line change
@@ -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"
}
62 changes: 57 additions & 5 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -928,6 +932,46 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker |
console.print(f"[yellow]Warning: Could not initialize constitution: {e}[/yellow]")


def ensure_agent_governance_from_template(project_path: Path, tracker: StepTracker | None = None) -> None:
"""Copy agent-governance template to memory if it doesn't exist."""
try:
result = _ensure_agent_governance_from_template(project_path)
except Exception as e:
if tracker:
tracker.add("agent-governance", "Agent governance setup")
tracker.error("agent-governance", str(e))
else:
console.print(f"[yellow]Warning: Could not initialize agent governance: {e}[/yellow]")
return

if tracker:
tracker.add("agent-governance", "Agent governance setup")
if result is None:
tracker.error("agent-governance", "template not found")
else:
tracker.complete("agent-governance", "available")


def refresh_agent_projection(project_path: Path, tracker: StepTracker | None = None) -> None:
"""Refresh generated agent governance projections."""
try:
result = _refresh_agent_projection(project_path)
except Exception as e:
if tracker:
tracker.add("agent-projection", "Agent governance projection")
tracker.error("agent-projection", str(e))
else:
console.print(f"[yellow]Warning: Could not refresh agent projection: {e}[/yellow]")
return

if tracker:
tracker.add("agent-projection", "Agent governance projection")
if result.memory_path is None:
tracker.skip("agent-projection", "agent-governance template missing")
else:
tracker.complete("agent-projection", f"{len(result.projection_paths)} file(s) refreshed")


INIT_OPTIONS_FILE = ".specify/init-options.json"


Expand Down Expand Up @@ -973,6 +1017,9 @@ def _get_skills_dir(project_path: Path, selected_ai: str) -> Path:
# Constants kept for backward compatibility with presets and extensions.
DEFAULT_SKILLS_DIR = ".agents/skills"
SKILL_DESCRIPTIONS = {
"arch": "Generate project-level 4+1 architecture view artifacts and synthesis.",
"agent": "Create or update agent governance and refresh agent instruction projections.",
"governance": "Create or update agent governance and refresh agent instruction projections.",
"specify": "Create or update feature specifications from natural language descriptions.",
"plan": "Generate technical implementation plans from feature specifications.",
"tasks": "Break down implementation plans into actionable task lists.",
Expand Down Expand Up @@ -1361,6 +1408,8 @@ def init(
tracker.complete("shared-infra", f"scripts ({selected_script}) + templates")

ensure_constitution_from_template(project_path, tracker=tracker)
ensure_agent_governance_from_template(project_path, tracker=tracker)
refresh_agent_projection(project_path, tracker=tracker)

if not no_git:
tracker.start("git")
Expand Down Expand Up @@ -1629,11 +1678,12 @@ def _display_cmd(name: str) -> str:

steps_lines.append(f"{step_num}. Start using {usage_label} with your coding agent:")

steps_lines.append(f" {step_num}.1 [cyan]{_display_cmd('constitution')}[/] - Establish project principles")
steps_lines.append(f" {step_num}.2 [cyan]{_display_cmd('specify')}[/] - Create baseline specification")
steps_lines.append(f" {step_num}.3 [cyan]{_display_cmd('plan')}[/] - Create implementation plan")
steps_lines.append(f" {step_num}.4 [cyan]{_display_cmd('tasks')}[/] - Generate actionable tasks")
steps_lines.append(f" {step_num}.5 [cyan]{_display_cmd('implement')}[/] - Execute implementation")
steps_lines.append(f" {step_num}.1 [cyan]{_display_cmd('arch')}[/] - Shape 4+1 architecture views")
steps_lines.append(f" {step_num}.2 [cyan]{_display_cmd('constitution')}[/] - Establish project principles")
steps_lines.append(f" {step_num}.3 [cyan]{_display_cmd('specify')}[/] - Create baseline specification")
steps_lines.append(f" {step_num}.4 [cyan]{_display_cmd('plan')}[/] - Create implementation plan")
steps_lines.append(f" {step_num}.5 [cyan]{_display_cmd('tasks')}[/] - Generate actionable tasks")
steps_lines.append(f" {step_num}.6 [cyan]{_display_cmd('implement')}[/] - Execute implementation")

steps_panel = Panel("\n".join(steps_lines), title="Next Steps", border_style="cyan", padding=(1,2))
console.print()
Expand Down Expand Up @@ -1997,6 +2047,7 @@ def _write_integration_json(
installed_integrations=installed_integrations,
settings=integration_settings,
)
refresh_agent_projection(project_root)


def _clear_init_options_for_integration(project_root: Path, integration_key: str) -> None:
Expand All @@ -2015,6 +2066,7 @@ def _remove_integration_json(project_root: Path) -> None:
path = project_root / INTEGRATION_JSON
if path.exists():
path.unlink()
refresh_agent_projection(project_root)


_MANIFEST_READ_ERRORS = (ValueError, FileNotFoundError, OSError, UnicodeDecodeError)
Expand Down
Loading