diff --git a/pyproject.toml b/pyproject.toml index fc4b306351..dae79b0f6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ packages = ["src/specify_cli"] [tool.hatch.build.targets.wheel.force-include] # Bundle core assets so `specify init` works without network access (air-gapped / enterprise) # Page templates (exclude commands/ — bundled separately below to avoid duplication) -"templates/agent-file-template.md" = "specify_cli/core_pack/templates/agent-file-template.md" "templates/checklist-template.md" = "specify_cli/core_pack/templates/checklist-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" diff --git a/scripts/bash/update-agent-context.sh b/scripts/bash/update-agent-context.sh deleted file mode 100644 index 2f71bb893c..0000000000 --- a/scripts/bash/update-agent-context.sh +++ /dev/null @@ -1,857 +0,0 @@ -#!/usr/bin/env bash - -# Update agent context files with information from plan.md -# -# This script maintains AI agent context files by parsing feature specifications -# and updating agent-specific configuration files with project information. -# -# MAIN FUNCTIONS: -# 1. Environment Validation -# - Verifies git repository structure and branch information -# - Checks for required plan.md files and templates -# - Validates file permissions and accessibility -# -# 2. Plan Data Extraction -# - Parses plan.md files to extract project metadata -# - Identifies language/version, frameworks, databases, and project types -# - Handles missing or incomplete specification data gracefully -# -# 3. Agent File Management -# - Creates new agent context files from templates when needed -# - Updates existing agent files with new project information -# - Preserves manual additions and custom configurations -# - Supports multiple AI agent formats and directory structures -# -# 4. Content Generation -# - Generates language-specific build/test commands -# - Creates appropriate project directory structures -# - Updates technology stacks and recent changes sections -# - Maintains consistent formatting and timestamps -# -# 5. Multi-Agent Support -# - Handles agent-specific file paths and naming conventions -# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Forge, Goose, Antigravity or Generic -# - Can update single agents or all existing agent files -# - Creates default Claude file if no agent files exist -# -# Usage: ./update-agent-context.sh [agent_type] -# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic -# Leave empty to update all existing agent files - -set -e - -# Enable strict error handling -set -u -set -o pipefail - -#============================================================================== -# Configuration and Global Variables -#============================================================================== - -# Get script directory and load common functions -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" - -# Get all paths and variables from common functions -_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } -eval "$_paths_output" -unset _paths_output - -NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code -AGENT_TYPE="${1:-}" - -# Agent-specific file paths -CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" -GEMINI_FILE="$REPO_ROOT/GEMINI.md" -COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md" -CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" -QWEN_FILE="$REPO_ROOT/QWEN.md" -AGENTS_FILE="$REPO_ROOT/AGENTS.md" -WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" -JUNIE_FILE="$REPO_ROOT/.junie/AGENTS.md" -KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" -AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" -ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" -CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" -QODER_FILE="$REPO_ROOT/QODER.md" -# Amp, Kiro CLI, IBM Bob, Pi, Forge, and Goose all share AGENTS.md — use AGENTS_FILE to avoid -# updating the same file multiple times. -AMP_FILE="$AGENTS_FILE" -SHAI_FILE="$REPO_ROOT/SHAI.md" -TABNINE_FILE="$REPO_ROOT/TABNINE.md" -KIRO_FILE="$AGENTS_FILE" -AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md" -BOB_FILE="$AGENTS_FILE" -VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md" -KIMI_FILE="$REPO_ROOT/KIMI.md" -TRAE_FILE="$REPO_ROOT/.trae/rules/project_rules.md" -IFLOW_FILE="$REPO_ROOT/IFLOW.md" -FORGE_FILE="$AGENTS_FILE" - -# Template file -TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" - -# Global variables for parsed plan data -NEW_LANG="" -NEW_FRAMEWORK="" -NEW_DB="" -NEW_PROJECT_TYPE="" - -#============================================================================== -# Utility Functions -#============================================================================== - -log_info() { - echo "INFO: $1" -} - -log_success() { - echo "✓ $1" -} - -log_error() { - echo "ERROR: $1" >&2 -} - -log_warning() { - echo "WARNING: $1" >&2 -} - -# Track temporary files for cleanup on interrupt -_CLEANUP_FILES=() - -# Cleanup function for temporary files -cleanup() { - local exit_code=$? - # Disarm traps to prevent re-entrant loop - trap - EXIT INT TERM - if [ ${#_CLEANUP_FILES[@]} -gt 0 ]; then - for f in "${_CLEANUP_FILES[@]}"; do - rm -f "$f" "$f.bak" "$f.tmp" - done - fi - exit $exit_code -} - -# Set up cleanup trap -trap cleanup EXIT INT TERM - -#============================================================================== -# Validation Functions -#============================================================================== - -validate_environment() { - # Check if we have a current branch/feature (git or non-git) - if [[ -z "$CURRENT_BRANCH" ]]; then - log_error "Unable to determine current feature" - if [[ "$HAS_GIT" == "true" ]]; then - log_info "Make sure you're on a feature branch" - else - log_info "Set SPECIFY_FEATURE environment variable or create a feature first" - fi - exit 1 - fi - - # Check if plan.md exists - if [[ ! -f "$NEW_PLAN" ]]; then - log_error "No plan.md found at $NEW_PLAN" - log_info "Make sure you're working on a feature with a corresponding spec directory" - if [[ "$HAS_GIT" != "true" ]]; then - log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first" - fi - exit 1 - fi - - # Check if template exists (needed for new files) - if [[ ! -f "$TEMPLATE_FILE" ]]; then - log_warning "Template file not found at $TEMPLATE_FILE" - log_warning "Creating new agent files will fail" - fi -} - -#============================================================================== -# Plan Parsing Functions -#============================================================================== - -extract_plan_field() { - local field_pattern="$1" - local plan_file="$2" - - grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ - head -1 | \ - sed "s|^\*\*${field_pattern}\*\*: ||" | \ - sed 's/^[ \t]*//;s/[ \t]*$//' | \ - grep -v "NEEDS CLARIFICATION" | \ - grep -v "^N/A$" || echo "" -} - -parse_plan_data() { - local plan_file="$1" - - if [[ ! -f "$plan_file" ]]; then - log_error "Plan file not found: $plan_file" - return 1 - fi - - if [[ ! -r "$plan_file" ]]; then - log_error "Plan file is not readable: $plan_file" - return 1 - fi - - log_info "Parsing plan data from $plan_file" - - NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file") - NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file") - NEW_DB=$(extract_plan_field "Storage" "$plan_file") - NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file") - - # Log what we found - if [[ -n "$NEW_LANG" ]]; then - log_info "Found language: $NEW_LANG" - else - log_warning "No language information found in plan" - fi - - if [[ -n "$NEW_FRAMEWORK" ]]; then - log_info "Found framework: $NEW_FRAMEWORK" - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then - log_info "Found database: $NEW_DB" - fi - - if [[ -n "$NEW_PROJECT_TYPE" ]]; then - log_info "Found project type: $NEW_PROJECT_TYPE" - fi -} - -format_technology_stack() { - local lang="$1" - local framework="$2" - local parts=() - - # Add non-empty parts - [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang") - [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework") - - # Join with proper formatting - if [[ ${#parts[@]} -eq 0 ]]; then - echo "" - elif [[ ${#parts[@]} -eq 1 ]]; then - echo "${parts[0]}" - else - # Join multiple parts with " + " - local result="${parts[0]}" - for ((i=1; i<${#parts[@]}; i++)); do - result="$result + ${parts[i]}" - done - echo "$result" - fi -} - -#============================================================================== -# Template and Content Generation Functions -#============================================================================== - -get_project_structure() { - local project_type="$1" - - if [[ "$project_type" == *"web"* ]]; then - echo "backend/\\nfrontend/\\ntests/" - else - echo "src/\\ntests/" - fi -} - -get_commands_for_language() { - local lang="$1" - - case "$lang" in - *"Python"*) - echo "cd src && pytest && ruff check ." - ;; - *"Rust"*) - echo "cargo test && cargo clippy" - ;; - *"JavaScript"*|*"TypeScript"*) - echo "npm test && npm run lint" - ;; - *) - echo "# Add commands for $lang" - ;; - esac -} - -get_language_conventions() { - local lang="$1" - echo "$lang: Follow standard conventions" -} - -# Escape sed replacement-side specials for | delimiter. -# & and \ are replacement-side specials; | is our sed delimiter. -_esc_sed() { printf '%s\n' "$1" | sed 's/[\\&|]/\\&/g'; } - -create_new_agent_file() { - local target_file="$1" - local temp_file="$2" - local project_name - project_name=$(_esc_sed "$3") - local current_date="$4" - - if [[ ! -f "$TEMPLATE_FILE" ]]; then - log_error "Template not found at $TEMPLATE_FILE" - return 1 - fi - - if [[ ! -r "$TEMPLATE_FILE" ]]; then - log_error "Template file is not readable: $TEMPLATE_FILE" - return 1 - fi - - log_info "Creating new agent context file from template..." - - if ! cp "$TEMPLATE_FILE" "$temp_file"; then - log_error "Failed to copy template file" - return 1 - fi - - # Replace template placeholders - local project_structure - project_structure=$(get_project_structure "$NEW_PROJECT_TYPE") - project_structure=$(_esc_sed "$project_structure") - - local commands - commands=$(get_commands_for_language "$NEW_LANG") - - local language_conventions - language_conventions=$(get_language_conventions "$NEW_LANG") - - local escaped_lang=$(_esc_sed "$NEW_LANG") - local escaped_framework=$(_esc_sed "$NEW_FRAMEWORK") - commands=$(_esc_sed "$commands") - language_conventions=$(_esc_sed "$language_conventions") - local escaped_branch=$(_esc_sed "$CURRENT_BRANCH") - - # Build technology stack and recent change strings conditionally - local tech_stack - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then - tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)" - elif [[ -n "$escaped_lang" ]]; then - tech_stack="- $escaped_lang ($escaped_branch)" - elif [[ -n "$escaped_framework" ]]; then - tech_stack="- $escaped_framework ($escaped_branch)" - else - tech_stack="- ($escaped_branch)" - fi - - local recent_change - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then - recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework" - elif [[ -n "$escaped_lang" ]]; then - recent_change="- $escaped_branch: Added $escaped_lang" - elif [[ -n "$escaped_framework" ]]; then - recent_change="- $escaped_branch: Added $escaped_framework" - else - recent_change="- $escaped_branch: Added" - fi - - local substitutions=( - "s|\[PROJECT NAME\]|$project_name|" - "s|\[DATE\]|$current_date|" - "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|" - "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g" - "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|" - "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|" - "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" - ) - - for substitution in "${substitutions[@]}"; do - if ! sed -i.bak -e "$substitution" "$temp_file"; then - log_error "Failed to perform substitution: $substitution" - rm -f "$temp_file" "$temp_file.bak" - return 1 - fi - done - - # Convert literal \n sequences to actual newlines (portable — works on BSD + GNU) - awk '{gsub(/\\n/,"\n")}1' "$temp_file" > "$temp_file.tmp" - mv "$temp_file.tmp" "$temp_file" - - # Clean up backup files from sed -i.bak - rm -f "$temp_file.bak" - - # Prepend Cursor frontmatter for .mdc files so rules are auto-included - if [[ "$target_file" == *.mdc ]]; then - local frontmatter_file - frontmatter_file=$(mktemp) || return 1 - _CLEANUP_FILES+=("$frontmatter_file") - printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" - cat "$temp_file" >> "$frontmatter_file" - mv "$frontmatter_file" "$temp_file" - fi - - return 0 -} - - - - -update_existing_agent_file() { - local target_file="$1" - local current_date="$2" - - log_info "Updating existing agent context file..." - - # Use a single temporary file for atomic update - local temp_file - temp_file=$(mktemp) || { - log_error "Failed to create temporary file" - return 1 - } - _CLEANUP_FILES+=("$temp_file") - - # Process the file in one pass - local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") - local new_tech_entries=() - local new_change_entry="" - - # Prepare new technology entries - if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then - new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then - new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)") - fi - - # Prepare new change entry - if [[ -n "$tech_stack" ]]; then - new_change_entry="- $CURRENT_BRANCH: Added $tech_stack" - elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then - new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB" - fi - - # Check if sections exist in the file - local has_active_technologies=0 - local has_recent_changes=0 - - if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then - has_active_technologies=1 - fi - - if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then - has_recent_changes=1 - fi - - # Process file line by line - local in_tech_section=false - local in_changes_section=false - local tech_entries_added=false - local changes_entries_added=false - local existing_changes_count=0 - local file_ended=false - - while IFS= read -r line || [[ -n "$line" ]]; do - # Handle Active Technologies section - if [[ "$line" == "## Active Technologies" ]]; then - echo "$line" >> "$temp_file" - in_tech_section=true - continue - elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then - # Add new tech entries before closing the section - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - echo "$line" >> "$temp_file" - in_tech_section=false - continue - elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then - # Add new tech entries before empty line in tech section - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - echo "$line" >> "$temp_file" - continue - fi - - # Handle Recent Changes section - if [[ "$line" == "## Recent Changes" ]]; then - echo "$line" >> "$temp_file" - # Add new change entry right after the heading - if [[ -n "$new_change_entry" ]]; then - echo "$new_change_entry" >> "$temp_file" - fi - in_changes_section=true - changes_entries_added=true - continue - elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then - echo "$line" >> "$temp_file" - in_changes_section=false - continue - elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then - # Keep only first 2 existing changes - if [[ $existing_changes_count -lt 2 ]]; then - echo "$line" >> "$temp_file" - ((existing_changes_count++)) - fi - continue - fi - - # Update timestamp - if [[ "$line" =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then - echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" - else - echo "$line" >> "$temp_file" - fi - done < "$target_file" - - # Post-loop check: if we're still in the Active Technologies section and haven't added new entries - if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - - # If sections don't exist, add them at the end of the file - if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then - echo "" >> "$temp_file" - echo "## Active Technologies" >> "$temp_file" - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" - tech_entries_added=true - fi - - if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then - echo "" >> "$temp_file" - echo "## Recent Changes" >> "$temp_file" - echo "$new_change_entry" >> "$temp_file" - changes_entries_added=true - fi - - # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion - if [[ "$target_file" == *.mdc ]]; then - if ! head -1 "$temp_file" | grep -q '^---'; then - local frontmatter_file - frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; } - _CLEANUP_FILES+=("$frontmatter_file") - printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" - cat "$temp_file" >> "$frontmatter_file" - mv "$frontmatter_file" "$temp_file" - fi - fi - - # Move temp file to target atomically - if ! mv "$temp_file" "$target_file"; then - log_error "Failed to update target file" - rm -f "$temp_file" - return 1 - fi - - return 0 -} -#============================================================================== -# Main Agent File Update Function -#============================================================================== - -update_agent_file() { - local target_file="$1" - local agent_name="$2" - - if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then - log_error "update_agent_file requires target_file and agent_name parameters" - return 1 - fi - - log_info "Updating $agent_name context file: $target_file" - - local project_name - project_name=$(basename "$REPO_ROOT") - local current_date - current_date=$(date +%Y-%m-%d) - - # Create directory if it doesn't exist - local target_dir - target_dir=$(dirname "$target_file") - if [[ ! -d "$target_dir" ]]; then - if ! mkdir -p "$target_dir"; then - log_error "Failed to create directory: $target_dir" - return 1 - fi - fi - - if [[ ! -f "$target_file" ]]; then - # Create new file from template - local temp_file - temp_file=$(mktemp) || { - log_error "Failed to create temporary file" - return 1 - } - _CLEANUP_FILES+=("$temp_file") - - if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then - if mv "$temp_file" "$target_file"; then - log_success "Created new $agent_name context file" - else - log_error "Failed to move temporary file to $target_file" - rm -f "$temp_file" - return 1 - fi - else - log_error "Failed to create new agent file" - rm -f "$temp_file" - return 1 - fi - else - # Update existing file - if [[ ! -r "$target_file" ]]; then - log_error "Cannot read existing file: $target_file" - return 1 - fi - - if [[ ! -w "$target_file" ]]; then - log_error "Cannot write to existing file: $target_file" - return 1 - fi - - if update_existing_agent_file "$target_file" "$current_date"; then - log_success "Updated existing $agent_name context file" - else - log_error "Failed to update existing agent file" - return 1 - fi - fi - - return 0 -} - -#============================================================================== -# Agent Selection and Processing -#============================================================================== - -update_specific_agent() { - local agent_type="$1" - - case "$agent_type" in - claude) - update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1 - ;; - gemini) - update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1 - ;; - copilot) - update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1 - ;; - cursor-agent) - update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1 - ;; - qwen) - update_agent_file "$QWEN_FILE" "Qwen Code" || return 1 - ;; - opencode) - update_agent_file "$AGENTS_FILE" "opencode" || return 1 - ;; - codex) - update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1 - ;; - windsurf) - update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1 - ;; - junie) - update_agent_file "$JUNIE_FILE" "Junie" || return 1 - ;; - kilocode) - update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1 - ;; - auggie) - update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1 - ;; - roo) - update_agent_file "$ROO_FILE" "Roo Code" || return 1 - ;; - codebuddy) - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1 - ;; - qodercli) - update_agent_file "$QODER_FILE" "Qoder CLI" || return 1 - ;; - amp) - update_agent_file "$AMP_FILE" "Amp" || return 1 - ;; - shai) - update_agent_file "$SHAI_FILE" "SHAI" || return 1 - ;; - tabnine) - update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1 - ;; - kiro-cli) - update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1 - ;; - agy) - update_agent_file "$AGY_FILE" "Antigravity" || return 1 - ;; - bob) - update_agent_file "$BOB_FILE" "IBM Bob" || return 1 - ;; - vibe) - update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1 - ;; - kimi) - update_agent_file "$KIMI_FILE" "Kimi Code" || return 1 - ;; - trae) - update_agent_file "$TRAE_FILE" "Trae" || return 1 - ;; - pi) - update_agent_file "$AGENTS_FILE" "Pi Coding Agent" || return 1 - ;; - iflow) - update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1 - ;; - forge) - update_agent_file "$AGENTS_FILE" "Forge" || return 1 - ;; - goose) - update_agent_file "$AGENTS_FILE" "Goose" || return 1 - ;; - generic) - log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent." - ;; - *) - log_error "Unknown agent type '$agent_type'" - log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic" - exit 1 - ;; - esac -} - -# Helper: skip non-existent files and files already updated (dedup by -# realpath so that variables pointing to the same file — e.g. AMP_FILE, -# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once). -# Uses a linear array instead of associative array for bash 3.2 compatibility. -# Note: defined at top level because bash 3.2 does not support true -# nested/local functions. _updated_paths, _found_agent, and _all_ok are -# initialised exclusively inside update_all_existing_agents so that -# sourcing this script has no side effects on the caller's environment. - -_update_if_new() { - local file="$1" name="$2" - [[ -f "$file" ]] || return 0 - local real_path - real_path=$(realpath "$file" 2>/dev/null || echo "$file") - local p - if [[ ${#_updated_paths[@]} -gt 0 ]]; then - for p in "${_updated_paths[@]}"; do - [[ "$p" == "$real_path" ]] && return 0 - done - fi - # Record the file as seen before attempting the update so that: - # (a) aliases pointing to the same path are not retried on failure - # (b) _found_agent reflects file existence, not update success - _updated_paths+=("$real_path") - _found_agent=true - update_agent_file "$file" "$name" -} - -update_all_existing_agents() { - _found_agent=false - _updated_paths=() - local _all_ok=true - - _update_if_new "$CLAUDE_FILE" "Claude Code" || _all_ok=false - _update_if_new "$GEMINI_FILE" "Gemini CLI" || _all_ok=false - _update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false - _update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false - _update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false - _update_if_new "$AGENTS_FILE" "Codex/opencode/Amp/Kiro/Bob/Pi/Forge/Goose" || _all_ok=false - _update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false - _update_if_new "$JUNIE_FILE" "Junie" || _all_ok=false - _update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false - _update_if_new "$AUGGIE_FILE" "Auggie CLI" || _all_ok=false - _update_if_new "$ROO_FILE" "Roo Code" || _all_ok=false - _update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI" || _all_ok=false - _update_if_new "$SHAI_FILE" "SHAI" || _all_ok=false - _update_if_new "$TABNINE_FILE" "Tabnine CLI" || _all_ok=false - _update_if_new "$QODER_FILE" "Qoder CLI" || _all_ok=false - _update_if_new "$AGY_FILE" "Antigravity" || _all_ok=false - _update_if_new "$VIBE_FILE" "Mistral Vibe" || _all_ok=false - _update_if_new "$KIMI_FILE" "Kimi Code" || _all_ok=false - _update_if_new "$TRAE_FILE" "Trae" || _all_ok=false - _update_if_new "$IFLOW_FILE" "iFlow CLI" || _all_ok=false - - # If no agent files exist, create a default Claude file - if [[ "$_found_agent" == false ]]; then - log_info "No existing agent files found, creating default Claude file..." - update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1 - fi - - [[ "$_all_ok" == true ]] -} -print_summary() { - echo - log_info "Summary of changes:" - - if [[ -n "$NEW_LANG" ]]; then - echo " - Added language: $NEW_LANG" - fi - - if [[ -n "$NEW_FRAMEWORK" ]]; then - echo " - Added framework: $NEW_FRAMEWORK" - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then - echo " - Added database: $NEW_DB" - fi - - echo - log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic]" -} - -#============================================================================== -# Main Execution -#============================================================================== - -main() { - # Validate environment before proceeding - validate_environment - - log_info "=== Updating agent context files for feature $CURRENT_BRANCH ===" - - # Parse the plan file to extract project information - if ! parse_plan_data "$NEW_PLAN"; then - log_error "Failed to parse plan data" - exit 1 - fi - - # Process based on agent type argument - local success=true - - if [[ -z "$AGENT_TYPE" ]]; then - # No specific agent provided - update all existing agent files - log_info "No agent specified, updating all existing agent files..." - if ! update_all_existing_agents; then - success=false - fi - else - # Specific agent provided - update only that agent - log_info "Updating specific agent: $AGENT_TYPE" - if ! update_specific_agent "$AGENT_TYPE"; then - success=false - fi - fi - - # Print summary - print_summary - - if [[ "$success" == true ]]; then - log_success "Agent context update completed successfully" - exit 0 - else - log_error "Agent context update completed with errors" - exit 1 - fi -} - -# Execute main function if script is run directly -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - main "$@" -fi diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 deleted file mode 100644 index 3ee45d383c..0000000000 --- a/scripts/powershell/update-agent-context.ps1 +++ /dev/null @@ -1,515 +0,0 @@ -#!/usr/bin/env pwsh -<#! -.SYNOPSIS -Update agent context files with information from plan.md (PowerShell version) - -.DESCRIPTION -Mirrors the behavior of scripts/bash/update-agent-context.sh: - 1. Environment Validation - 2. Plan Data Extraction - 3. Agent File Management (create from template or update existing) - 4. Content Generation (technology stack, recent changes, timestamp) - 5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, junie, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, trae, pi, iflow, forge, goose, generic) - -.PARAMETER AgentType -Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist). - -.EXAMPLE -./update-agent-context.ps1 -AgentType claude - -.EXAMPLE -./update-agent-context.ps1 # Updates all existing agent files - -.NOTES -Relies on common helper functions in common.ps1 -#> -param( - [Parameter(Position=0)] - [ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','junie','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','vibe','qodercli','kimi','trae','pi','iflow','forge','goose','generic')] - [string]$AgentType -) - -$ErrorActionPreference = 'Stop' - -# Import common helpers -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -. (Join-Path $ScriptDir 'common.ps1') - -# Acquire environment paths -$envData = Get-FeaturePathsEnv -$REPO_ROOT = $envData.REPO_ROOT -$CURRENT_BRANCH = $envData.CURRENT_BRANCH -$HAS_GIT = $envData.HAS_GIT -$IMPL_PLAN = $envData.IMPL_PLAN -$NEW_PLAN = $IMPL_PLAN - -# Agent file paths -$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md' -$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md' -$COPILOT_FILE = Join-Path $REPO_ROOT '.github/copilot-instructions.md' -$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc' -$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md' -$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md' -$WINDSURF_FILE = Join-Path $REPO_ROOT '.windsurf/rules/specify-rules.md' -$JUNIE_FILE = Join-Path $REPO_ROOT '.junie/AGENTS.md' -$KILOCODE_FILE = Join-Path $REPO_ROOT '.kilocode/rules/specify-rules.md' -$AUGGIE_FILE = Join-Path $REPO_ROOT '.augment/rules/specify-rules.md' -$ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md' -$CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md' -$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md' -$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md' -$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md' -$TABNINE_FILE = Join-Path $REPO_ROOT 'TABNINE.md' -$KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md' -$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md' -$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md' -$VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-agents.md' -$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md' -$TRAE_FILE = Join-Path $REPO_ROOT '.trae/rules/project_rules.md' -$IFLOW_FILE = Join-Path $REPO_ROOT 'IFLOW.md' -$FORGE_FILE = Join-Path $REPO_ROOT 'AGENTS.md' -$GOOSE_FILE = Join-Path $REPO_ROOT 'AGENTS.md' - -$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md' - -# Parsed plan data placeholders -$script:NEW_LANG = '' -$script:NEW_FRAMEWORK = '' -$script:NEW_DB = '' -$script:NEW_PROJECT_TYPE = '' - -function Write-Info { - param( - [Parameter(Mandatory=$true)] - [string]$Message - ) - Write-Host "INFO: $Message" -} - -function Write-Success { - param( - [Parameter(Mandatory=$true)] - [string]$Message - ) - Write-Host "$([char]0x2713) $Message" -} - -function Write-WarningMsg { - param( - [Parameter(Mandatory=$true)] - [string]$Message - ) - Write-Warning $Message -} - -function Write-Err { - param( - [Parameter(Mandatory=$true)] - [string]$Message - ) - Write-Host "ERROR: $Message" -ForegroundColor Red -} - -function Validate-Environment { - if (-not $CURRENT_BRANCH) { - Write-Err 'Unable to determine current feature' - if ($HAS_GIT) { Write-Info "Make sure you're on a feature branch" } else { Write-Info 'Set SPECIFY_FEATURE environment variable or create a feature first' } - exit 1 - } - if (-not (Test-Path $NEW_PLAN)) { - Write-Err "No plan.md found at $NEW_PLAN" - Write-Info 'Ensure you are working on a feature with a corresponding spec directory' - if (-not $HAS_GIT) { Write-Info 'Use: $env:SPECIFY_FEATURE=your-feature-name or create a new feature first' } - exit 1 - } - if (-not (Test-Path $TEMPLATE_FILE)) { - Write-Err "Template file not found at $TEMPLATE_FILE" - Write-Info 'Run specify init to scaffold .specify/templates, or add agent-file-template.md there.' - exit 1 - } -} - -function Extract-PlanField { - param( - [Parameter(Mandatory=$true)] - [string]$FieldPattern, - [Parameter(Mandatory=$true)] - [string]$PlanFile - ) - if (-not (Test-Path $PlanFile)) { return '' } - # Lines like **Language/Version**: Python 3.12 - $regex = "^\*\*$([Regex]::Escape($FieldPattern))\*\*: (.+)$" - Get-Content -LiteralPath $PlanFile -Encoding utf8 | ForEach-Object { - if ($_ -match $regex) { - $val = $Matches[1].Trim() - if ($val -notin @('NEEDS CLARIFICATION','N/A')) { return $val } - } - } | Select-Object -First 1 -} - -function Parse-PlanData { - param( - [Parameter(Mandatory=$true)] - [string]$PlanFile - ) - if (-not (Test-Path $PlanFile)) { Write-Err "Plan file not found: $PlanFile"; return $false } - Write-Info "Parsing plan data from $PlanFile" - $script:NEW_LANG = Extract-PlanField -FieldPattern 'Language/Version' -PlanFile $PlanFile - $script:NEW_FRAMEWORK = Extract-PlanField -FieldPattern 'Primary Dependencies' -PlanFile $PlanFile - $script:NEW_DB = Extract-PlanField -FieldPattern 'Storage' -PlanFile $PlanFile - $script:NEW_PROJECT_TYPE = Extract-PlanField -FieldPattern 'Project Type' -PlanFile $PlanFile - - if ($NEW_LANG) { Write-Info "Found language: $NEW_LANG" } else { Write-WarningMsg 'No language information found in plan' } - if ($NEW_FRAMEWORK) { Write-Info "Found framework: $NEW_FRAMEWORK" } - if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Info "Found database: $NEW_DB" } - if ($NEW_PROJECT_TYPE) { Write-Info "Found project type: $NEW_PROJECT_TYPE" } - return $true -} - -function Format-TechnologyStack { - param( - [Parameter(Mandatory=$false)] - [string]$Lang, - [Parameter(Mandatory=$false)] - [string]$Framework - ) - $parts = @() - if ($Lang -and $Lang -ne 'NEEDS CLARIFICATION') { $parts += $Lang } - if ($Framework -and $Framework -notin @('NEEDS CLARIFICATION','N/A')) { $parts += $Framework } - if (-not $parts) { return '' } - return ($parts -join ' + ') -} - -function Get-ProjectStructure { - param( - [Parameter(Mandatory=$false)] - [string]$ProjectType - ) - if ($ProjectType -match 'web') { return "backend/`nfrontend/`ntests/" } else { return "src/`ntests/" } -} - -function Get-CommandsForLanguage { - param( - [Parameter(Mandatory=$false)] - [string]$Lang - ) - switch -Regex ($Lang) { - 'Python' { return "cd src; pytest; ruff check ." } - 'Rust' { return "cargo test; cargo clippy" } - 'JavaScript|TypeScript' { return "npm test; npm run lint" } - default { return "# Add commands for $Lang" } - } -} - -function Get-LanguageConventions { - param( - [Parameter(Mandatory=$false)] - [string]$Lang - ) - if ($Lang) { "${Lang}: Follow standard conventions" } else { 'General: Follow standard conventions' } -} - -function New-AgentFile { - param( - [Parameter(Mandatory=$true)] - [string]$TargetFile, - [Parameter(Mandatory=$true)] - [string]$ProjectName, - [Parameter(Mandatory=$true)] - [datetime]$Date - ) - if (-not (Test-Path $TEMPLATE_FILE)) { Write-Err "Template not found at $TEMPLATE_FILE"; return $false } - $temp = New-TemporaryFile - Copy-Item -LiteralPath $TEMPLATE_FILE -Destination $temp -Force - - $projectStructure = Get-ProjectStructure -ProjectType $NEW_PROJECT_TYPE - $commands = Get-CommandsForLanguage -Lang $NEW_LANG - $languageConventions = Get-LanguageConventions -Lang $NEW_LANG - - $escaped_lang = $NEW_LANG - $escaped_framework = $NEW_FRAMEWORK - $escaped_branch = $CURRENT_BRANCH - - $content = Get-Content -LiteralPath $temp -Raw -Encoding utf8 - $content = $content -replace '\[PROJECT NAME\]',$ProjectName - $content = $content -replace '\[DATE\]',$Date.ToString('yyyy-MM-dd') - - # Build the technology stack string safely - $techStackForTemplate = "" - if ($escaped_lang -and $escaped_framework) { - $techStackForTemplate = "- $escaped_lang + $escaped_framework ($escaped_branch)" - } elseif ($escaped_lang) { - $techStackForTemplate = "- $escaped_lang ($escaped_branch)" - } elseif ($escaped_framework) { - $techStackForTemplate = "- $escaped_framework ($escaped_branch)" - } - - $content = $content -replace '\[EXTRACTED FROM ALL PLAN.MD FILES\]',$techStackForTemplate - # For project structure we manually embed (keep newlines) - $escapedStructure = [Regex]::Escape($projectStructure) - $content = $content -replace '\[ACTUAL STRUCTURE FROM PLANS\]',$escapedStructure - # Replace escaped newlines placeholder after all replacements - $content = $content -replace '\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]',$commands - $content = $content -replace '\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]',$languageConventions - - # Build the recent changes string safely - $recentChangesForTemplate = "" - if ($escaped_lang -and $escaped_framework) { - $recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_lang} + ${escaped_framework}" - } elseif ($escaped_lang) { - $recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_lang}" - } elseif ($escaped_framework) { - $recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_framework}" - } - - $content = $content -replace '\[LAST 3 FEATURES AND WHAT THEY ADDED\]',$recentChangesForTemplate - # Convert literal \n sequences introduced by Escape to real newlines - $content = $content -replace '\\n',[Environment]::NewLine - - # Prepend Cursor frontmatter for .mdc files so rules are auto-included - if ($TargetFile -match '\.mdc$') { - $frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') -join [Environment]::NewLine - $content = $frontmatter + $content - } - - $parent = Split-Path -Parent $TargetFile - if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null } - Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8 - Remove-Item $temp -Force - return $true -} - -function Update-ExistingAgentFile { - param( - [Parameter(Mandatory=$true)] - [string]$TargetFile, - [Parameter(Mandatory=$true)] - [datetime]$Date - ) - if (-not (Test-Path $TargetFile)) { return (New-AgentFile -TargetFile $TargetFile -ProjectName (Split-Path $REPO_ROOT -Leaf) -Date $Date) } - - $techStack = Format-TechnologyStack -Lang $NEW_LANG -Framework $NEW_FRAMEWORK - $newTechEntries = @() - if ($techStack) { - $escapedTechStack = [Regex]::Escape($techStack) - if (-not (Select-String -Pattern $escapedTechStack -Path $TargetFile -Quiet)) { - $newTechEntries += "- $techStack ($CURRENT_BRANCH)" - } - } - if ($NEW_DB -and $NEW_DB -notin @('N/A','NEEDS CLARIFICATION')) { - $escapedDB = [Regex]::Escape($NEW_DB) - if (-not (Select-String -Pattern $escapedDB -Path $TargetFile -Quiet)) { - $newTechEntries += "- $NEW_DB ($CURRENT_BRANCH)" - } - } - $newChangeEntry = '' - if ($techStack) { $newChangeEntry = "- ${CURRENT_BRANCH}: Added ${techStack}" } - elseif ($NEW_DB -and $NEW_DB -notin @('N/A','NEEDS CLARIFICATION')) { $newChangeEntry = "- ${CURRENT_BRANCH}: Added ${NEW_DB}" } - - $lines = Get-Content -LiteralPath $TargetFile -Encoding utf8 - $output = New-Object System.Collections.Generic.List[string] - $inTech = $false; $inChanges = $false; $techAdded = $false; $changeAdded = $false; $existingChanges = 0 - - for ($i=0; $i -lt $lines.Count; $i++) { - $line = $lines[$i] - if ($line -eq '## Active Technologies') { - $output.Add($line) - $inTech = $true - continue - } - if ($inTech -and $line -match '^##\s') { - if (-not $techAdded -and $newTechEntries.Count -gt 0) { $newTechEntries | ForEach-Object { $output.Add($_) }; $techAdded = $true } - $output.Add($line); $inTech = $false; continue - } - if ($inTech -and [string]::IsNullOrWhiteSpace($line)) { - if (-not $techAdded -and $newTechEntries.Count -gt 0) { $newTechEntries | ForEach-Object { $output.Add($_) }; $techAdded = $true } - $output.Add($line); continue - } - if ($line -eq '## Recent Changes') { - $output.Add($line) - if ($newChangeEntry) { $output.Add($newChangeEntry); $changeAdded = $true } - $inChanges = $true - continue - } - if ($inChanges -and $line -match '^##\s') { $output.Add($line); $inChanges = $false; continue } - if ($inChanges -and $line -match '^- ') { - if ($existingChanges -lt 2) { $output.Add($line); $existingChanges++ } - continue - } - if ($line -match '(\*\*)?Last updated(\*\*)?: .*\d{4}-\d{2}-\d{2}') { - $output.Add(($line -replace '\d{4}-\d{2}-\d{2}',$Date.ToString('yyyy-MM-dd'))) - continue - } - $output.Add($line) - } - - # Post-loop check: if we're still in the Active Technologies section and haven't added new entries - if ($inTech -and -not $techAdded -and $newTechEntries.Count -gt 0) { - $newTechEntries | ForEach-Object { $output.Add($_) } - } - - # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion - if ($TargetFile -match '\.mdc$' -and $output.Count -gt 0 -and $output[0] -ne '---') { - $frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') - $output.InsertRange(0, $frontmatter) - } - - Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8 - return $true -} - -function Update-AgentFile { - param( - [Parameter(Mandatory=$true)] - [string]$TargetFile, - [Parameter(Mandatory=$true)] - [string]$AgentName - ) - if (-not $TargetFile -or -not $AgentName) { Write-Err 'Update-AgentFile requires TargetFile and AgentName'; return $false } - Write-Info "Updating $AgentName context file: $TargetFile" - $projectName = Split-Path $REPO_ROOT -Leaf - $date = Get-Date - - $dir = Split-Path -Parent $TargetFile - if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null } - - if (-not (Test-Path $TargetFile)) { - if (New-AgentFile -TargetFile $TargetFile -ProjectName $projectName -Date $date) { Write-Success "Created new $AgentName context file" } else { Write-Err 'Failed to create new agent file'; return $false } - } else { - try { - if (Update-ExistingAgentFile -TargetFile $TargetFile -Date $date) { Write-Success "Updated existing $AgentName context file" } else { Write-Err 'Failed to update agent file'; return $false } - } catch { - Write-Err "Cannot access or update existing file: $TargetFile. $_" - return $false - } - } - return $true -} - -function Update-SpecificAgent { - param( - [Parameter(Mandatory=$true)] - [string]$Type - ) - switch ($Type) { - 'claude' { Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code' } - 'gemini' { Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI' } - 'copilot' { Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot' } - 'cursor-agent' { Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE' } - 'qwen' { Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code' } - 'opencode' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'opencode' } - 'codex' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex CLI' } - 'windsurf' { Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf' } - 'junie' { Update-AgentFile -TargetFile $JUNIE_FILE -AgentName 'Junie' } - 'kilocode' { Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code' } - 'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' } - 'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' } - 'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI' } - 'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' } - 'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' } - 'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' } - 'tabnine' { Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI' } - 'kiro-cli' { Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI' } - 'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' } - 'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' } - 'vibe' { Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe' } - 'kimi' { Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code' } - 'trae' { Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae' } - 'pi' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Pi Coding Agent' } - 'iflow' { Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI' } - 'forge' { Update-AgentFile -TargetFile $FORGE_FILE -AgentName 'Forge' } - 'goose' { Update-AgentFile -TargetFile $GOOSE_FILE -AgentName 'Goose' } - 'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' } - default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic'; return $false } - } -} - -function Update-AllExistingAgents { - $found = $false - $ok = $true - $updatedPaths = @() - - # Helper function to update only if file exists and hasn't been updated yet - function Update-IfNew { - param( - [Parameter(Mandatory=$true)] - [string]$FilePath, - [Parameter(Mandatory=$true)] - [string]$AgentName - ) - - if (-not (Test-Path $FilePath)) { return $true } - - # Get the real path to detect duplicates (e.g., AMP_FILE, KIRO_FILE, BOB_FILE all point to AGENTS.md) - $realPath = (Get-Item -LiteralPath $FilePath).FullName - - # Check if we've already updated this file - if ($updatedPaths -contains $realPath) { - return $true - } - - # Record the file as seen before attempting the update - # Use parent scope (1) to modify Update-AllExistingAgents' local variables - Set-Variable -Name updatedPaths -Value ($updatedPaths + $realPath) -Scope 1 - Set-Variable -Name found -Value $true -Scope 1 - - # Perform the update - return (Update-AgentFile -TargetFile $FilePath -AgentName $AgentName) - } - - if (-not (Update-IfNew -FilePath $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false } - if (-not (Update-IfNew -FilePath $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false } - if (-not (Update-IfNew -FilePath $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false } - if (-not (Update-IfNew -FilePath $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false } - if (-not (Update-IfNew -FilePath $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false } - if (-not (Update-IfNew -FilePath $AGENTS_FILE -AgentName 'Codex/opencode/Amp/Kiro/Bob/Pi/Forge/Goose')) { $ok = $false } - if (-not (Update-IfNew -FilePath $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false } - if (-not (Update-IfNew -FilePath $JUNIE_FILE -AgentName 'Junie')) { $ok = $false } - if (-not (Update-IfNew -FilePath $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false } - if (-not (Update-IfNew -FilePath $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false } - if (-not (Update-IfNew -FilePath $ROO_FILE -AgentName 'Roo Code')) { $ok = $false } - if (-not (Update-IfNew -FilePath $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false } - if (-not (Update-IfNew -FilePath $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false } - if (-not (Update-IfNew -FilePath $SHAI_FILE -AgentName 'SHAI')) { $ok = $false } - if (-not (Update-IfNew -FilePath $TABNINE_FILE -AgentName 'Tabnine CLI')) { $ok = $false } - if (-not (Update-IfNew -FilePath $AGY_FILE -AgentName 'Antigravity')) { $ok = $false } - if (-not (Update-IfNew -FilePath $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false } - if (-not (Update-IfNew -FilePath $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false } - if (-not (Update-IfNew -FilePath $TRAE_FILE -AgentName 'Trae')) { $ok = $false } - if (-not (Update-IfNew -FilePath $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false } - - if (-not $found) { - Write-Info 'No existing agent files found, creating default Claude file...' - if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false } - } - return $ok -} - -function Print-Summary { - Write-Host '' - Write-Info 'Summary of changes:' - if ($NEW_LANG) { Write-Host " - Added language: $NEW_LANG" } - if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" } - if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" } - Write-Host '' - Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|forge|goose|generic]' -} - -function Main { - Validate-Environment - Write-Info "=== Updating agent context files for feature $CURRENT_BRANCH ===" - if (-not (Parse-PlanData -PlanFile $NEW_PLAN)) { Write-Err 'Failed to parse plan data'; exit 1 } - $success = $true - if ($AgentType) { - Write-Info "Updating specific agent: $AgentType" - if (-not (Update-SpecificAgent -Type $AgentType)) { $success = $false } - } - else { - Write-Info 'No agent specified, updating all existing agent files...' - if (-not (Update-AllExistingAgents)) { $success = $false } - } - Print-Summary - if ($success) { Write-Success 'Agent context update completed successfully'; exit 0 } else { Write-Err 'Agent context update completed with errors'; exit 1 } -} - -Main diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 0608e7a8ac..8c6fd02b9f 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1261,15 +1261,11 @@ def init( manifest.save() # Write .specify/integration.json - script_ext = "sh" if selected_script == "sh" else "ps1" integration_json = project_path / ".specify" / "integration.json" integration_json.parent.mkdir(parents=True, exist_ok=True) integration_json.write_text(json.dumps({ "integration": resolved_integration.key, "version": get_speckit_version(), - "scripts": { - "update-context": f".specify/integrations/{resolved_integration.key}/scripts/update-context.{script_ext}", - }, }, indent=2) + "\n", encoding="utf-8") tracker.complete("integration", resolved_integration.config.get("name", resolved_integration.key)) @@ -1373,6 +1369,7 @@ def init( "ai": selected_ai, "integration": resolved_integration.key, "branch_numbering": branch_numbering or "sequential", + "context_file": resolved_integration.context_file, "here": here, "preset": preset, "script": selected_script, @@ -1737,18 +1734,13 @@ def _read_integration_json(project_root: Path) -> dict[str, Any]: def _write_integration_json( project_root: Path, integration_key: str, - script_type: str, ) -> None: """Write ``.specify/integration.json`` for *integration_key*.""" - script_ext = "sh" if script_type == "sh" else "ps1" dest = project_root / INTEGRATION_JSON dest.parent.mkdir(parents=True, exist_ok=True) dest.write_text(json.dumps({ "integration": integration_key, "version": get_speckit_version(), - "scripts": { - "update-context": f".specify/integrations/{integration_key}/scripts/update-context.{script_ext}", - }, }, indent=2) + "\n", encoding="utf-8") @@ -1936,7 +1928,7 @@ def integration_install( raw_options=integration_options, ) manifest.save() - _write_integration_json(project_root, integration.key, selected_script) + _write_integration_json(project_root, integration.key) _update_init_options_for_integration(project_root, integration, script_type=selected_script) except Exception as e: @@ -2013,6 +2005,7 @@ def _update_init_options_for_integration( opts = load_init_options(project_root) opts["integration"] = integration.key opts["ai"] = integration.key + opts["context_file"] = integration.context_file if script_type: opts["script"] = script_type if isinstance(integration, SkillsIntegration): @@ -2064,6 +2057,7 @@ def integration_uninstall( opts.pop("integration", None) opts.pop("ai", None) opts.pop("ai_skills", None) + opts.pop("context_file", None) save_init_options(project_root, opts) raise typer.Exit(0) @@ -2082,6 +2076,10 @@ def integration_uninstall( removed, skipped = manifest.uninstall(project_root, force=force) + # Remove managed context section from the agent context file + if integration: + integration.remove_context_section(project_root) + _remove_integration_json(project_root) # Update init-options.json to clear the integration @@ -2090,6 +2088,7 @@ def integration_uninstall( opts.pop("integration", None) opts.pop("ai", None) opts.pop("ai_skills", None) + opts.pop("context_file", None) save_init_options(project_root, opts) name = (integration.config or {}).get("name", key) if integration else key @@ -2156,6 +2155,7 @@ def integration_switch( ) raise typer.Exit(1) removed, skipped = old_manifest.uninstall(project_root, force=force) + current_integration.remove_context_section(project_root) if removed: console.print(f" Removed {len(removed)} file(s)") if skipped: @@ -2186,6 +2186,7 @@ def integration_switch( opts.pop("integration", None) opts.pop("ai", None) opts.pop("ai_skills", None) + opts.pop("context_file", None) save_init_options(project_root, opts) # Ensure shared infrastructure is present (safe to run unconditionally; @@ -2212,7 +2213,7 @@ def integration_switch( raw_options=integration_options, ) manifest.save() - _write_integration_json(project_root, target_integration.key, selected_script) + _write_integration_json(project_root, target_integration.key) _update_init_options_for_integration(project_root, target_integration, script_type=selected_script) except Exception as e: @@ -2320,7 +2321,7 @@ def integration_upgrade( raw_options=integration_options, ) new_manifest.save() - _write_integration_json(project_root, key, selected_script) + _write_integration_json(project_root, key) _update_init_options_for_integration(project_root, integration, script_type=selected_script) except Exception as exc: # Don't teardown — setup overwrites in-place, so teardown would diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 32fc6cdbf0..1a0e5a8317 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -110,9 +110,9 @@ def _adjust_script_paths(self, frontmatter: dict) -> dict: """Normalize script paths in frontmatter to generated project locations. Rewrites known repo-relative and top-level script paths under the - `scripts` and `agent_scripts` keys (for example `../../scripts/`, - `../../templates/`, `../../memory/`, `scripts/`, `templates/`, and - `memory/`) to the `.specify/...` paths used in generated projects. + ``scripts`` key (for example ``../../scripts/``, + ``../../templates/``, ``../../memory/``, ``scripts/``, ``templates/``, and + ``memory/``) to the ``.specify/...`` paths used in generated projects. Args: frontmatter: Frontmatter dictionary @@ -122,11 +122,8 @@ def _adjust_script_paths(self, frontmatter: dict) -> dict: """ frontmatter = deepcopy(frontmatter) - for script_key in ("scripts", "agent_scripts"): - scripts = frontmatter.get(script_key) - if not isinstance(scripts, dict): - continue - + scripts = frontmatter.get("scripts") + if isinstance(scripts, dict): for key, script_path in scripts.items(): if isinstance(script_path, str): scripts[key] = self.rewrite_project_relative_paths(script_path) @@ -333,11 +330,8 @@ def resolve_skill_placeholders( frontmatter = {} scripts = frontmatter.get("scripts", {}) or {} - agent_scripts = frontmatter.get("agent_scripts", {}) or {} if not isinstance(scripts, dict): scripts = {} - if not isinstance(agent_scripts, dict): - agent_scripts = {} init_opts = load_init_options(project_root) if not isinstance(init_opts, dict): @@ -351,17 +345,14 @@ def resolve_skill_placeholders( ) secondary_variant = "sh" if default_variant == "ps" else "ps" - if default_variant in scripts or default_variant in agent_scripts: + if default_variant in scripts: fallback_order.append(default_variant) - if secondary_variant in scripts or secondary_variant in agent_scripts: + if secondary_variant in scripts: fallback_order.append(secondary_variant) for key in scripts: if key not in fallback_order: fallback_order.append(key) - for key in agent_scripts: - if key not in fallback_order: - fallback_order.append(key) script_variant = fallback_order[0] if fallback_order else None @@ -370,14 +361,12 @@ def resolve_skill_placeholders( script_command = script_command.replace("{ARGS}", "$ARGUMENTS") body = body.replace("{SCRIPT}", script_command) - agent_script_command = ( - agent_scripts.get(script_variant) if script_variant else None - ) - if agent_script_command: - agent_script_command = agent_script_command.replace("{ARGS}", "$ARGUMENTS") - body = body.replace("{AGENT_SCRIPT}", agent_script_command) - body = body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", agent_name) + + # Resolve __CONTEXT_FILE__ from init-options + context_file = init_opts.get("context_file") or "" + body = body.replace("__CONTEXT_FILE__", context_file) + return CommandRegistrar.rewrite_project_relative_paths(body) def _convert_argument_placeholder( diff --git a/src/specify_cli/integrations/agy/scripts/update-context.ps1 b/src/specify_cli/integrations/agy/scripts/update-context.ps1 deleted file mode 100644 index 9eeb461657..0000000000 --- a/src/specify_cli/integrations/agy/scripts/update-context.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -# update-context.ps1 — Antigravity (agy) integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. - -$ErrorActionPreference = 'Stop' - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType agy diff --git a/src/specify_cli/integrations/agy/scripts/update-context.sh b/src/specify_cli/integrations/agy/scripts/update-context.sh deleted file mode 100755 index d7303f6197..0000000000 --- a/src/specify_cli/integrations/agy/scripts/update-context.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Antigravity (agy) integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. - -set -euo pipefail - -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" agy diff --git a/src/specify_cli/integrations/amp/scripts/update-context.ps1 b/src/specify_cli/integrations/amp/scripts/update-context.ps1 deleted file mode 100644 index c217b99f9a..0000000000 --- a/src/specify_cli/integrations/amp/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Amp integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType amp diff --git a/src/specify_cli/integrations/amp/scripts/update-context.sh b/src/specify_cli/integrations/amp/scripts/update-context.sh deleted file mode 100755 index 56cbf6e787..0000000000 --- a/src/specify_cli/integrations/amp/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Amp integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" amp diff --git a/src/specify_cli/integrations/auggie/scripts/update-context.ps1 b/src/specify_cli/integrations/auggie/scripts/update-context.ps1 deleted file mode 100644 index 49e7e6b5f3..0000000000 --- a/src/specify_cli/integrations/auggie/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Auggie CLI integration: create/update .augment/rules/specify-rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType auggie diff --git a/src/specify_cli/integrations/auggie/scripts/update-context.sh b/src/specify_cli/integrations/auggie/scripts/update-context.sh deleted file mode 100755 index 4cf80bba2b..0000000000 --- a/src/specify_cli/integrations/auggie/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Auggie CLI integration: create/update .augment/rules/specify-rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" auggie diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 2c01e25b0e..4c71b165e5 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -84,6 +84,11 @@ class IntegrationBase(ABC): context_file: str | None = None """Relative path to the agent context file (e.g. ``CLAUDE.md``).""" + # -- Markers for managed context section ------------------------------ + + CONTEXT_MARKER_START = "" + CONTEXT_MARKER_END = "" + # -- Public API ------------------------------------------------------- @classmethod @@ -380,22 +385,235 @@ def install_scripts( return created + # -- Agent context file management ------------------------------------ + + @staticmethod + def _ensure_mdc_frontmatter(content: str) -> str: + """Ensure ``.mdc`` content has YAML frontmatter with ``alwaysApply: true``. + + If frontmatter is missing, prepend it. If frontmatter exists but + ``alwaysApply`` is absent or not ``true``, inject/fix it. + + Uses string/regex manipulation to preserve comments and formatting + in existing frontmatter. + """ + import re as _re + + leading_ws = len(content) - len(content.lstrip()) + leading = content[:leading_ws] + stripped = content[leading_ws:] + + if not stripped.startswith("---"): + return "---\nalwaysApply: true\n---\n\n" + content + + # Match frontmatter block: ---\n...\n--- + match = _re.match( + r"^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)", + stripped, + _re.DOTALL, + ) + if not match: + return "---\nalwaysApply: true\n---\n\n" + content + + opening, fm_text, closing, sep, rest = match.groups() + newline = "\r\n" if "\r\n" in opening else "\n" + + # Already correct? + if _re.search( + r"(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$", fm_text + ): + return content + + # alwaysApply exists but wrong value — fix in place while preserving + # indentation and any trailing inline comment. + if _re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:", fm_text): + fm_text = _re.sub( + r"(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$", + r"\1alwaysApply: true\2", + fm_text, + count=1, + ) + elif fm_text.strip(): + fm_text = fm_text + newline + "alwaysApply: true" + else: + fm_text = "alwaysApply: true" + + return f"{leading}{opening}{fm_text}{closing}{sep}{rest}" + + @staticmethod + def _build_context_section(plan_path: str = "") -> str: + """Build the content for the managed section between markers. + + *plan_path* is the project-relative path to the current plan + (e.g. ``"specs//plan.md"``). When empty, the section + contains only the generic directive without a concrete path. + """ + lines = [ + "For additional context about technologies to be used, project structure,", + "shell commands, and other important information, read the current plan", + ] + if plan_path: + lines.append(f"at {plan_path}") + return "\n".join(lines) + + def upsert_context_section( + self, + project_root: Path, + plan_path: str = "", + ) -> Path | None: + """Create or update the managed section in the agent context file. + + If the context file does not exist it is created with just the + managed section. If it exists, the content between + ```` and ```` markers + is replaced (or appended when no markers are found). + + Returns the path to the context file, or ``None`` when + ``context_file`` is not set. + """ + if not self.context_file: + return None + + ctx_path = project_root / self.context_file + section = ( + f"{self.CONTEXT_MARKER_START}\n" + f"{self._build_context_section(plan_path)}\n" + f"{self.CONTEXT_MARKER_END}\n" + ) + + if ctx_path.exists(): + content = ctx_path.read_text(encoding="utf-8") + start_idx = content.find(self.CONTEXT_MARKER_START) + end_idx = content.find( + self.CONTEXT_MARKER_END, + start_idx if start_idx != -1 else 0, + ) + + if start_idx != -1 and end_idx != -1 and end_idx > start_idx: + # Replace existing section (include the end marker + newline) + end_of_marker = end_idx + len(self.CONTEXT_MARKER_END) + # Consume trailing line ending (CRLF or LF) + if end_of_marker < len(content) and content[end_of_marker] == "\r": + end_of_marker += 1 + if end_of_marker < len(content) and content[end_of_marker] == "\n": + end_of_marker += 1 + new_content = content[:start_idx] + section + content[end_of_marker:] + elif start_idx != -1: + # Corrupted: start marker without end — replace from start through EOF + new_content = content[:start_idx] + section + elif end_idx != -1: + # Corrupted: end marker without start — replace BOF through end marker + end_of_marker = end_idx + len(self.CONTEXT_MARKER_END) + if end_of_marker < len(content) and content[end_of_marker] == "\r": + end_of_marker += 1 + if end_of_marker < len(content) and content[end_of_marker] == "\n": + end_of_marker += 1 + new_content = section + content[end_of_marker:] + else: + # No markers found — append + if content: + if not content.endswith("\n"): + content += "\n" + new_content = content + "\n" + section + else: + new_content = section + + # Ensure .mdc files have required YAML frontmatter + if ctx_path.suffix == ".mdc": + new_content = self._ensure_mdc_frontmatter(new_content) + else: + ctx_path.parent.mkdir(parents=True, exist_ok=True) + # Cursor .mdc files require YAML frontmatter to be loaded + if ctx_path.suffix == ".mdc": + new_content = self._ensure_mdc_frontmatter(section) + else: + new_content = section + + normalized = new_content.replace("\r\n", "\n").replace("\r", "\n") + ctx_path.write_bytes(normalized.encode("utf-8")) + return ctx_path + + def remove_context_section(self, project_root: Path) -> bool: + """Remove the managed section from the agent context file. + + Returns ``True`` if the section was found and removed. If the + file becomes empty (or whitespace-only) after removal it is + deleted. + """ + if not self.context_file: + return False + + ctx_path = project_root / self.context_file + if not ctx_path.exists(): + return False + + content = ctx_path.read_text(encoding="utf-8") + start_idx = content.find(self.CONTEXT_MARKER_START) + end_idx = content.find( + self.CONTEXT_MARKER_END, + start_idx if start_idx != -1 else 0, + ) + + # Only remove a complete, well-ordered managed section. If either + # marker is missing, leave the file unchanged to avoid deleting + # unrelated user-authored content. + if start_idx == -1 or end_idx == -1 or end_idx <= start_idx: + return False + + removal_start = start_idx + removal_end = end_idx + len(self.CONTEXT_MARKER_END) + + # Consume trailing line ending (CRLF or LF) + if removal_end < len(content) and content[removal_end] == "\r": + removal_end += 1 + if removal_end < len(content) and content[removal_end] == "\n": + removal_end += 1 + + # Also strip a blank line before the section if present + if removal_start > 0 and content[removal_start - 1] == "\n": + if removal_start > 1 and content[removal_start - 2] == "\n": + removal_start -= 1 + + new_content = content[:removal_start] + content[removal_end:] + + # Normalize line endings before comparisons + normalized = new_content.replace("\r\n", "\n").replace("\r", "\n") + + # For .mdc files, treat Speckit-generated frontmatter-only content as empty + if ctx_path.suffix == ".mdc": + import re + # Delete the file if only YAML frontmatter remains (no body content) + frontmatter_only = re.match( + r"^---\n.*?\n---\s*$", normalized, re.DOTALL + ) + if not normalized.strip() or frontmatter_only: + ctx_path.unlink() + return True + + if not normalized.strip(): + ctx_path.unlink() + else: + ctx_path.write_bytes(normalized.encode("utf-8")) + + return True + @staticmethod def process_template( content: str, agent_name: str, script_type: str, arg_placeholder: str = "$ARGUMENTS", + context_file: str = "", ) -> str: """Process a raw command template into agent-ready content. Performs the same transformations as the release script: 1. Extract ``scripts.`` value from YAML frontmatter 2. Replace ``{SCRIPT}`` with the extracted script command - 3. Extract ``agent_scripts.`` and replace ``{AGENT_SCRIPT}`` - 4. Strip ``scripts:`` and ``agent_scripts:`` sections from frontmatter - 5. Replace ``{ARGS}`` and ``$ARGUMENTS`` with *arg_placeholder* - 6. Replace ``__AGENT__`` with *agent_name* + 3. Strip ``scripts:`` section from frontmatter + 4. Replace ``{ARGS}`` and ``$ARGUMENTS`` with *arg_placeholder* + 5. Replace ``__AGENT__`` with *agent_name* + 6. Replace ``__CONTEXT_FILE__`` with *context_file* 7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc. """ # 1. Extract script command from frontmatter @@ -421,25 +639,7 @@ def process_template( if script_command: content = content.replace("{SCRIPT}", script_command) - # 3. Extract agent_script command - agent_script_command = "" - in_agent_scripts = False - for line in content.splitlines(): - if line.strip() == "agent_scripts:": - in_agent_scripts = True - continue - if in_agent_scripts and line and not line[0].isspace(): - in_agent_scripts = False - if in_agent_scripts: - m = script_pattern.match(line) - if m: - agent_script_command = m.group(1).strip() - break - - if agent_script_command: - content = content.replace("{AGENT_SCRIPT}", agent_script_command) - - # 4. Strip scripts: and agent_scripts: sections from frontmatter + # 3. Strip scripts: section from frontmatter lines = content.splitlines(keepends=True) output_lines: list[str] = [] in_frontmatter = False @@ -457,23 +657,26 @@ def process_template( output_lines.append(line) continue if in_frontmatter: - if stripped in ("scripts:", "agent_scripts:"): + if stripped == "scripts:": skip_section = True continue if skip_section: if line[0:1].isspace(): - continue # skip indented content under scripts/agent_scripts + continue # skip indented content under scripts skip_section = False output_lines.append(line) content = "".join(output_lines) - # 5. Replace {ARGS} and $ARGUMENTS + # 4. Replace {ARGS} and $ARGUMENTS content = content.replace("{ARGS}", arg_placeholder) content = content.replace("$ARGUMENTS", arg_placeholder) - # 6. Replace __AGENT__ + # 5. Replace __AGENT__ content = content.replace("__AGENT__", agent_name) + # 6. Replace __CONTEXT_FILE__ + content = content.replace("__CONTEXT_FILE__", context_file) + # 7. Rewrite paths — delegate to the shared implementation in # CommandRegistrar so extension-local paths are preserved and # boundary rules stay consistent across the codebase. @@ -526,6 +729,9 @@ def setup( self.record_file_in_manifest(dst_file, project_root, manifest) created.append(dst_file) + # Upsert managed context section into the agent context file + self.upsert_context_section(project_root) + return created def teardown( @@ -539,9 +745,11 @@ def teardown( Delegates to ``manifest.uninstall()`` which only removes files whose hash still matches the recorded value (unless *force*). + Also removes the managed context section from the agent file. Returns ``(removed, skipped)`` file lists. """ + self.remove_context_section(project_root) return manifest.uninstall(project_root, force=force) # -- Convenience helpers for subclasses ------------------------------- @@ -579,8 +787,8 @@ class MarkdownIntegration(IntegrationBase): (and optionally ``context_file``). Everything else is inherited. ``setup()`` processes command templates (replacing ``{SCRIPT}``, - ``{ARGS}``, ``__AGENT__``, rewriting paths) and installs - integration-specific scripts (``update-context.sh`` / ``.ps1``). + ``{ARGS}``, ``__AGENT__``, rewriting paths) and upserts the + managed context section into the agent context file. """ def build_exec_args( @@ -638,7 +846,8 @@ def setup( for src_file in templates: raw = src_file.read_text(encoding="utf-8") processed = self.process_template( - raw, self.key, script_type, arg_placeholder + raw, self.key, script_type, arg_placeholder, + context_file=self.context_file or "", ) dst_name = self.command_filename(src_file.stem) dst_file = self.write_file_and_record( @@ -646,7 +855,9 @@ def setup( ) created.append(dst_file) - created.extend(self.install_scripts(project_root, manifest)) + # Upsert managed context section into the agent context file + self.upsert_context_section(project_root) + return created @@ -841,7 +1052,8 @@ def setup( raw = src_file.read_text(encoding="utf-8") description = self._extract_description(raw) processed = self.process_template( - raw, self.key, script_type, arg_placeholder + raw, self.key, script_type, arg_placeholder, + context_file=self.context_file or "", ) _, body = self._split_frontmatter(processed) toml_content = self._render_toml(description, body) @@ -851,7 +1063,9 @@ def setup( ) created.append(dst_file) - created.extend(self.install_scripts(project_root, manifest)) + # Upsert managed context section into the agent context file + self.upsert_context_section(project_root) + return created @@ -1021,7 +1235,8 @@ def setup( title = self._human_title(src_file.stem) processed = self.process_template( - raw, self.key, script_type, arg_placeholder + raw, self.key, script_type, arg_placeholder, + context_file=self.context_file or "", ) _, body = self._split_frontmatter(processed) yaml_content = self._render_yaml( @@ -1033,7 +1248,9 @@ def setup( ) created.append(dst_file) - created.extend(self.install_scripts(project_root, manifest)) + # Upsert managed context section into the agent context file + self.upsert_context_section(project_root) + return created @@ -1176,7 +1393,8 @@ def setup( # Process body through the standard template pipeline processed_body = self.process_template( - raw, self.key, script_type, arg_placeholder + raw, self.key, script_type, arg_placeholder, + context_file=self.context_file or "", ) # Strip the processed frontmatter — we rebuild it for skills. # Preserve leading whitespace in the body to match release ZIP @@ -1220,5 +1438,7 @@ def _quote(v: str) -> str: ) created.append(dst) - created.extend(self.install_scripts(project_root, manifest)) + # Upsert managed context section into the agent context file + self.upsert_context_section(project_root) + return created diff --git a/src/specify_cli/integrations/bob/scripts/update-context.ps1 b/src/specify_cli/integrations/bob/scripts/update-context.ps1 deleted file mode 100644 index 188860899f..0000000000 --- a/src/specify_cli/integrations/bob/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — IBM Bob integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType bob diff --git a/src/specify_cli/integrations/bob/scripts/update-context.sh b/src/specify_cli/integrations/bob/scripts/update-context.sh deleted file mode 100755 index 0228603fea..0000000000 --- a/src/specify_cli/integrations/bob/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — IBM Bob integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" bob diff --git a/src/specify_cli/integrations/claude/scripts/update-context.ps1 b/src/specify_cli/integrations/claude/scripts/update-context.ps1 deleted file mode 100644 index 837974d47a..0000000000 --- a/src/specify_cli/integrations/claude/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Claude Code integration: create/update CLAUDE.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType claude diff --git a/src/specify_cli/integrations/claude/scripts/update-context.sh b/src/specify_cli/integrations/claude/scripts/update-context.sh deleted file mode 100755 index 4b83855a27..0000000000 --- a/src/specify_cli/integrations/claude/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Claude Code integration: create/update CLAUDE.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" claude diff --git a/src/specify_cli/integrations/codebuddy/scripts/update-context.ps1 b/src/specify_cli/integrations/codebuddy/scripts/update-context.ps1 deleted file mode 100644 index 0269392c09..0000000000 --- a/src/specify_cli/integrations/codebuddy/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — CodeBuddy integration: create/update CODEBUDDY.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codebuddy diff --git a/src/specify_cli/integrations/codebuddy/scripts/update-context.sh b/src/specify_cli/integrations/codebuddy/scripts/update-context.sh deleted file mode 100755 index d57ddc3560..0000000000 --- a/src/specify_cli/integrations/codebuddy/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — CodeBuddy integration: create/update CODEBUDDY.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codebuddy diff --git a/src/specify_cli/integrations/codex/scripts/update-context.ps1 b/src/specify_cli/integrations/codex/scripts/update-context.ps1 deleted file mode 100644 index d73a5a4d34..0000000000 --- a/src/specify_cli/integrations/codex/scripts/update-context.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -# update-context.ps1 — Codex CLI integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. - -$ErrorActionPreference = 'Stop' - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codex diff --git a/src/specify_cli/integrations/codex/scripts/update-context.sh b/src/specify_cli/integrations/codex/scripts/update-context.sh deleted file mode 100755 index 512d6e91d3..0000000000 --- a/src/specify_cli/integrations/codex/scripts/update-context.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Codex CLI integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. - -set -euo pipefail - -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codex diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index e389138a84..be7fc819f6 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -183,7 +183,10 @@ def setup( # 1. Process and write command files as .agent.md for src_file in templates: raw = src_file.read_text(encoding="utf-8") - processed = self.process_template(raw, self.key, script_type, arg_placeholder) + processed = self.process_template( + raw, self.key, script_type, arg_placeholder, + context_file=self.context_file or "", + ) dst_name = self.command_filename(src_file.stem) dst_file = self.write_file_and_record( processed, dest / dst_name, project_root, manifest @@ -217,8 +220,8 @@ def setup( self.record_file_in_manifest(dst_settings, project_root, manifest) created.append(dst_settings) - # 4. Install integration-specific update-context scripts - created.extend(self.install_scripts(project_root, manifest)) + # 4. Upsert managed context section into the agent context file + self.upsert_context_section(project_root) return created diff --git a/src/specify_cli/integrations/copilot/scripts/update-context.ps1 b/src/specify_cli/integrations/copilot/scripts/update-context.ps1 deleted file mode 100644 index 26e746a789..0000000000 --- a/src/specify_cli/integrations/copilot/scripts/update-context.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -# update-context.ps1 — Copilot integration: create/update .github/copilot-instructions.md -# -# This is the copilot-specific implementation that produces the GitHub -# Copilot instructions file. The shared dispatcher reads -# .specify/integration.json and calls this script. -# -# NOTE: This script is not yet active. It will be activated in Stage 7 -# when the shared update-agent-context.ps1 replaces its switch statement -# with integration.json-based dispatch. The shared script must also be -# refactored to support SPECKIT_SOURCE_ONLY (guard the Main call) before -# dot-sourcing will work. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -# Invoke shared update-agent-context script as a separate process. -# Dot-sourcing is unsafe until that script guards its Main call. -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType copilot diff --git a/src/specify_cli/integrations/copilot/scripts/update-context.sh b/src/specify_cli/integrations/copilot/scripts/update-context.sh deleted file mode 100644 index c7f3bc60b5..0000000000 --- a/src/specify_cli/integrations/copilot/scripts/update-context.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Copilot integration: create/update .github/copilot-instructions.md -# -# This is the copilot-specific implementation that produces the GitHub -# Copilot instructions file. The shared dispatcher reads -# .specify/integration.json and calls this script. -# -# NOTE: This script is not yet active. It will be activated in Stage 7 -# when the shared update-agent-context.sh replaces its case statement -# with integration.json-based dispatch. The shared script must also be -# refactored to support SPECKIT_SOURCE_ONLY (guard the main logic) -# before sourcing will work. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -# Invoke shared update-agent-context script as a separate process. -# Sourcing is unsafe until that script guards its main logic. -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" copilot diff --git a/src/specify_cli/integrations/cursor_agent/scripts/update-context.ps1 b/src/specify_cli/integrations/cursor_agent/scripts/update-context.ps1 deleted file mode 100644 index 4ce50a4873..0000000000 --- a/src/specify_cli/integrations/cursor_agent/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Cursor integration: create/update .cursor/rules/specify-rules.mdc -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType cursor-agent diff --git a/src/specify_cli/integrations/cursor_agent/scripts/update-context.sh b/src/specify_cli/integrations/cursor_agent/scripts/update-context.sh deleted file mode 100755 index 597ca2289c..0000000000 --- a/src/specify_cli/integrations/cursor_agent/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Cursor integration: create/update .cursor/rules/specify-rules.mdc -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" cursor-agent diff --git a/src/specify_cli/integrations/forge/__init__.py b/src/specify_cli/integrations/forge/__init__.py index e1c4d9da62..a941d4c331 100644 --- a/src/specify_cli/integrations/forge/__init__.py +++ b/src/specify_cli/integrations/forge/__init__.py @@ -130,7 +130,10 @@ def setup( for src_file in templates: raw = src_file.read_text(encoding="utf-8") # Process template with standard MarkdownIntegration logic - processed = self.process_template(raw, self.key, script_type, arg_placeholder) + processed = self.process_template( + raw, self.key, script_type, arg_placeholder, + context_file=self.context_file or "", + ) # FORGE-SPECIFIC: Ensure any remaining $ARGUMENTS placeholders are # converted to {{parameters}} @@ -145,8 +148,8 @@ def setup( ) created.append(dst_file) - # Install integration-specific update-context scripts - created.extend(self.install_scripts(project_root, manifest)) + # Upsert managed context section into the agent context file + self.upsert_context_section(project_root) return created diff --git a/src/specify_cli/integrations/forge/scripts/update-context.ps1 b/src/specify_cli/integrations/forge/scripts/update-context.ps1 deleted file mode 100644 index 474a9c6d0b..0000000000 --- a/src/specify_cli/integrations/forge/scripts/update-context.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -# update-context.ps1 — Forge integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -$sharedScript = "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" - -# Always delegate to the shared updater; fail clearly if it is unavailable. -if (-not (Test-Path $sharedScript)) { - Write-Error "Error: shared agent context updater not found: $sharedScript" - Write-Error "Forge integration requires support in scripts/powershell/update-agent-context.ps1." - exit 1 -} - -& $sharedScript -AgentType forge -exit $LASTEXITCODE diff --git a/src/specify_cli/integrations/forge/scripts/update-context.sh b/src/specify_cli/integrations/forge/scripts/update-context.sh deleted file mode 100755 index 2a5c46e1d1..0000000000 --- a/src/specify_cli/integrations/forge/scripts/update-context.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Forge integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -shared_script="$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" - -# Always delegate to the shared updater; fail clearly if it is unavailable. -if [ ! -x "$shared_script" ]; then - echo "Error: shared agent context updater not found or not executable:" >&2 - echo " $shared_script" >&2 - echo "Forge integration requires support in scripts/bash/update-agent-context.sh." >&2 - exit 1 -fi - -exec "$shared_script" forge diff --git a/src/specify_cli/integrations/gemini/scripts/update-context.ps1 b/src/specify_cli/integrations/gemini/scripts/update-context.ps1 deleted file mode 100644 index 51c9e0bc83..0000000000 --- a/src/specify_cli/integrations/gemini/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Gemini CLI integration: create/update GEMINI.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType gemini diff --git a/src/specify_cli/integrations/gemini/scripts/update-context.sh b/src/specify_cli/integrations/gemini/scripts/update-context.sh deleted file mode 100644 index c4e5003a55..0000000000 --- a/src/specify_cli/integrations/gemini/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Gemini CLI integration: create/update GEMINI.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" gemini diff --git a/src/specify_cli/integrations/generic/__init__.py b/src/specify_cli/integrations/generic/__init__.py index 4107c48690..fdaee4ed04 100644 --- a/src/specify_cli/integrations/generic/__init__.py +++ b/src/specify_cli/integrations/generic/__init__.py @@ -31,7 +31,7 @@ class GenericIntegration(MarkdownIntegration): "args": "$ARGUMENTS", "extension": ".md", } - context_file = None + context_file = "AGENTS.md" @classmethod def options(cls) -> list[IntegrationOption]: @@ -122,12 +122,17 @@ def setup( for src_file in templates: raw = src_file.read_text(encoding="utf-8") - processed = self.process_template(raw, self.key, script_type, arg_placeholder) + processed = self.process_template( + raw, self.key, script_type, arg_placeholder, + context_file=self.context_file or "", + ) dst_name = self.command_filename(src_file.stem) dst_file = self.write_file_and_record( processed, dest / dst_name, project_root, manifest ) created.append(dst_file) - created.extend(self.install_scripts(project_root, manifest)) + # Upsert managed context section into the agent context file + self.upsert_context_section(project_root) + return created diff --git a/src/specify_cli/integrations/generic/scripts/update-context.ps1 b/src/specify_cli/integrations/generic/scripts/update-context.ps1 deleted file mode 100644 index 2e9467f801..0000000000 --- a/src/specify_cli/integrations/generic/scripts/update-context.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -# update-context.ps1 — Generic integration: create/update context file -# -# Thin wrapper that delegates to the shared update-agent-context script. - -$ErrorActionPreference = 'Stop' - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType generic diff --git a/src/specify_cli/integrations/generic/scripts/update-context.sh b/src/specify_cli/integrations/generic/scripts/update-context.sh deleted file mode 100755 index d8ad30a7b8..0000000000 --- a/src/specify_cli/integrations/generic/scripts/update-context.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Generic integration: create/update context file -# -# Thin wrapper that delegates to the shared update-agent-context script. - -set -euo pipefail - -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" generic diff --git a/src/specify_cli/integrations/goose/scripts/update-context.ps1 b/src/specify_cli/integrations/goose/scripts/update-context.ps1 deleted file mode 100644 index eeb31f6296..0000000000 --- a/src/specify_cli/integrations/goose/scripts/update-context.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -# update-context.ps1 — Goose integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -$sharedScript = "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" - -# Always delegate to the shared updater; fail clearly if it is unavailable. -if (-not (Test-Path $sharedScript)) { - Write-Error "Error: shared agent context updater not found: $sharedScript" - Write-Error "Goose integration requires support in scripts/powershell/update-agent-context.ps1." - exit 1 -} - -& $sharedScript -AgentType goose -exit $LASTEXITCODE diff --git a/src/specify_cli/integrations/goose/scripts/update-context.sh b/src/specify_cli/integrations/goose/scripts/update-context.sh deleted file mode 100755 index 759ae3045a..0000000000 --- a/src/specify_cli/integrations/goose/scripts/update-context.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Goose integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -shared_script="$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" - -# Always delegate to the shared updater; fail clearly if it is unavailable. -if [ ! -x "$shared_script" ]; then - echo "Error: shared agent context updater not found or not executable:" >&2 - echo " $shared_script" >&2 - echo "Goose integration requires support in scripts/bash/update-agent-context.sh." >&2 - exit 1 -fi - -exec "$shared_script" goose diff --git a/src/specify_cli/integrations/iflow/scripts/update-context.ps1 b/src/specify_cli/integrations/iflow/scripts/update-context.ps1 deleted file mode 100644 index b502d4182a..0000000000 --- a/src/specify_cli/integrations/iflow/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — iFlow CLI integration: create/update IFLOW.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType iflow diff --git a/src/specify_cli/integrations/iflow/scripts/update-context.sh b/src/specify_cli/integrations/iflow/scripts/update-context.sh deleted file mode 100755 index 5080402071..0000000000 --- a/src/specify_cli/integrations/iflow/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — iFlow CLI integration: create/update IFLOW.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" iflow diff --git a/src/specify_cli/integrations/junie/scripts/update-context.ps1 b/src/specify_cli/integrations/junie/scripts/update-context.ps1 deleted file mode 100644 index 5a32432132..0000000000 --- a/src/specify_cli/integrations/junie/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Junie integration: create/update .junie/AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType junie diff --git a/src/specify_cli/integrations/junie/scripts/update-context.sh b/src/specify_cli/integrations/junie/scripts/update-context.sh deleted file mode 100755 index f4c8ba6c0e..0000000000 --- a/src/specify_cli/integrations/junie/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Junie integration: create/update .junie/AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" junie diff --git a/src/specify_cli/integrations/kilocode/scripts/update-context.ps1 b/src/specify_cli/integrations/kilocode/scripts/update-context.ps1 deleted file mode 100644 index d87e7ef59f..0000000000 --- a/src/specify_cli/integrations/kilocode/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Kilo Code integration: create/update .kilocode/rules/specify-rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kilocode diff --git a/src/specify_cli/integrations/kilocode/scripts/update-context.sh b/src/specify_cli/integrations/kilocode/scripts/update-context.sh deleted file mode 100755 index 132c0403f3..0000000000 --- a/src/specify_cli/integrations/kilocode/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Kilo Code integration: create/update .kilocode/rules/specify-rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kilocode diff --git a/src/specify_cli/integrations/kimi/scripts/update-context.ps1 b/src/specify_cli/integrations/kimi/scripts/update-context.ps1 deleted file mode 100644 index aa6678d052..0000000000 --- a/src/specify_cli/integrations/kimi/scripts/update-context.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -# update-context.ps1 — Kimi Code integration: create/update KIMI.md -# -# Thin wrapper that delegates to the shared update-agent-context script. - -$ErrorActionPreference = 'Stop' - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kimi diff --git a/src/specify_cli/integrations/kimi/scripts/update-context.sh b/src/specify_cli/integrations/kimi/scripts/update-context.sh deleted file mode 100755 index 2f81bc2a48..0000000000 --- a/src/specify_cli/integrations/kimi/scripts/update-context.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Kimi Code integration: create/update KIMI.md -# -# Thin wrapper that delegates to the shared update-agent-context script. - -set -euo pipefail - -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kimi diff --git a/src/specify_cli/integrations/kiro_cli/scripts/update-context.ps1 b/src/specify_cli/integrations/kiro_cli/scripts/update-context.ps1 deleted file mode 100644 index 7dd2b35fb7..0000000000 --- a/src/specify_cli/integrations/kiro_cli/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Kiro CLI integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kiro-cli diff --git a/src/specify_cli/integrations/kiro_cli/scripts/update-context.sh b/src/specify_cli/integrations/kiro_cli/scripts/update-context.sh deleted file mode 100755 index fa258edc75..0000000000 --- a/src/specify_cli/integrations/kiro_cli/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Kiro CLI integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kiro-cli diff --git a/src/specify_cli/integrations/opencode/scripts/update-context.ps1 b/src/specify_cli/integrations/opencode/scripts/update-context.ps1 deleted file mode 100644 index 4bba02b455..0000000000 --- a/src/specify_cli/integrations/opencode/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — opencode integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType opencode diff --git a/src/specify_cli/integrations/opencode/scripts/update-context.sh b/src/specify_cli/integrations/opencode/scripts/update-context.sh deleted file mode 100755 index 24c7e60251..0000000000 --- a/src/specify_cli/integrations/opencode/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — opencode integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" opencode diff --git a/src/specify_cli/integrations/pi/scripts/update-context.ps1 b/src/specify_cli/integrations/pi/scripts/update-context.ps1 deleted file mode 100644 index 6362118a5b..0000000000 --- a/src/specify_cli/integrations/pi/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Pi Coding Agent integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType pi diff --git a/src/specify_cli/integrations/pi/scripts/update-context.sh b/src/specify_cli/integrations/pi/scripts/update-context.sh deleted file mode 100755 index 1ad84c95a2..0000000000 --- a/src/specify_cli/integrations/pi/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Pi Coding Agent integration: create/update AGENTS.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" pi diff --git a/src/specify_cli/integrations/qodercli/scripts/update-context.ps1 b/src/specify_cli/integrations/qodercli/scripts/update-context.ps1 deleted file mode 100644 index 1fa007a168..0000000000 --- a/src/specify_cli/integrations/qodercli/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Qoder CLI integration: create/update QODER.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType qodercli diff --git a/src/specify_cli/integrations/qodercli/scripts/update-context.sh b/src/specify_cli/integrations/qodercli/scripts/update-context.sh deleted file mode 100755 index d371ad7952..0000000000 --- a/src/specify_cli/integrations/qodercli/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Qoder CLI integration: create/update QODER.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" qodercli diff --git a/src/specify_cli/integrations/qwen/scripts/update-context.ps1 b/src/specify_cli/integrations/qwen/scripts/update-context.ps1 deleted file mode 100644 index 24e4c90fab..0000000000 --- a/src/specify_cli/integrations/qwen/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Qwen Code integration: create/update QWEN.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType qwen diff --git a/src/specify_cli/integrations/qwen/scripts/update-context.sh b/src/specify_cli/integrations/qwen/scripts/update-context.sh deleted file mode 100755 index d1c62eb161..0000000000 --- a/src/specify_cli/integrations/qwen/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Qwen Code integration: create/update QWEN.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" qwen diff --git a/src/specify_cli/integrations/roo/scripts/update-context.ps1 b/src/specify_cli/integrations/roo/scripts/update-context.ps1 deleted file mode 100644 index d1dec923ed..0000000000 --- a/src/specify_cli/integrations/roo/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Roo Code integration: create/update .roo/rules/specify-rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType roo diff --git a/src/specify_cli/integrations/roo/scripts/update-context.sh b/src/specify_cli/integrations/roo/scripts/update-context.sh deleted file mode 100755 index 8fe255cb1b..0000000000 --- a/src/specify_cli/integrations/roo/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Roo Code integration: create/update .roo/rules/specify-rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" roo diff --git a/src/specify_cli/integrations/shai/scripts/update-context.ps1 b/src/specify_cli/integrations/shai/scripts/update-context.ps1 deleted file mode 100644 index 2c621c76ac..0000000000 --- a/src/specify_cli/integrations/shai/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — SHAI integration: create/update SHAI.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType shai diff --git a/src/specify_cli/integrations/shai/scripts/update-context.sh b/src/specify_cli/integrations/shai/scripts/update-context.sh deleted file mode 100755 index 093b9d1f76..0000000000 --- a/src/specify_cli/integrations/shai/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — SHAI integration: create/update SHAI.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" shai diff --git a/src/specify_cli/integrations/tabnine/scripts/update-context.ps1 b/src/specify_cli/integrations/tabnine/scripts/update-context.ps1 deleted file mode 100644 index 0ffb3a1649..0000000000 --- a/src/specify_cli/integrations/tabnine/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Tabnine CLI integration: create/update TABNINE.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType tabnine diff --git a/src/specify_cli/integrations/tabnine/scripts/update-context.sh b/src/specify_cli/integrations/tabnine/scripts/update-context.sh deleted file mode 100644 index fe5050b6e9..0000000000 --- a/src/specify_cli/integrations/tabnine/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Tabnine CLI integration: create/update TABNINE.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" tabnine diff --git a/src/specify_cli/integrations/trae/scripts/update-context.ps1 b/src/specify_cli/integrations/trae/scripts/update-context.ps1 deleted file mode 100644 index ae9a3d1cd0..0000000000 --- a/src/specify_cli/integrations/trae/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Trae integration: create/update .trae/rules/project_rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType trae diff --git a/src/specify_cli/integrations/trae/scripts/update-context.sh b/src/specify_cli/integrations/trae/scripts/update-context.sh deleted file mode 100755 index 32e5c16b29..0000000000 --- a/src/specify_cli/integrations/trae/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Trae integration: create/update .trae/rules/project_rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" trae diff --git a/src/specify_cli/integrations/vibe/scripts/update-context.ps1 b/src/specify_cli/integrations/vibe/scripts/update-context.ps1 deleted file mode 100644 index d82ce3389c..0000000000 --- a/src/specify_cli/integrations/vibe/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Mistral Vibe integration: create/update .vibe/agents/specify-agents.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType vibe diff --git a/src/specify_cli/integrations/vibe/scripts/update-context.sh b/src/specify_cli/integrations/vibe/scripts/update-context.sh deleted file mode 100755 index f924cdb896..0000000000 --- a/src/specify_cli/integrations/vibe/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Mistral Vibe integration: create/update .vibe/agents/specify-agents.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" vibe diff --git a/src/specify_cli/integrations/windsurf/scripts/update-context.ps1 b/src/specify_cli/integrations/windsurf/scripts/update-context.ps1 deleted file mode 100644 index b5fe1d0c0a..0000000000 --- a/src/specify_cli/integrations/windsurf/scripts/update-context.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -# update-context.ps1 — Windsurf integration: create/update .windsurf/rules/specify-rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -$ErrorActionPreference = 'Stop' - -# Derive repo root from script location (walks up to find .specify/) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition -$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } -# If git did not return a repo root, or the git root does not contain .specify, -# fall back to walking up from the script directory to find the initialized project root. -if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = $scriptDir - $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) - while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { - $repoRoot = Split-Path -Parent $repoRoot - } -} - -& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType windsurf diff --git a/src/specify_cli/integrations/windsurf/scripts/update-context.sh b/src/specify_cli/integrations/windsurf/scripts/update-context.sh deleted file mode 100755 index b9a78d320e..0000000000 --- a/src/specify_cli/integrations/windsurf/scripts/update-context.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -# update-context.sh — Windsurf integration: create/update .windsurf/rules/specify-rules.md -# -# Thin wrapper that delegates to the shared update-agent-context script. -# Activated in Stage 7 when the shared script uses integration.json dispatch. -# -# Until then, this delegates to the shared script as a subprocess. - -set -euo pipefail - -# Derive repo root from script location (walks up to find .specify/) -_script_dir="$(cd "$(dirname "$0")" && pwd)" -_root="$_script_dir" -while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done -if [ -z "${REPO_ROOT:-}" ]; then - if [ -d "$_root/.specify" ]; then - REPO_ROOT="$_root" - else - git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" - if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then - REPO_ROOT="$git_root" - else - REPO_ROOT="$_root" - fi - fi -fi - -exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" windsurf diff --git a/templates/agent-file-template.md b/templates/agent-file-template.md deleted file mode 100644 index 4cc7fd6678..0000000000 --- a/templates/agent-file-template.md +++ /dev/null @@ -1,28 +0,0 @@ -# [PROJECT NAME] Development Guidelines - -Auto-generated from all feature plans. Last updated: [DATE] - -## Active Technologies - -[EXTRACTED FROM ALL PLAN.MD FILES] - -## Project Structure - -```text -[ACTUAL STRUCTURE FROM PLANS] -``` - -## Commands - -[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] - -## Code Style - -[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE] - -## Recent Changes - -[LAST 3 FEATURES AND WHAT THEY ADDED] - - - diff --git a/templates/commands/plan.md b/templates/commands/plan.md index 4f1e9ed295..04db94ffaa 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -11,9 +11,6 @@ handoffs: scripts: sh: scripts/bash/setup-plan.sh --json ps: scripts/powershell/setup-plan.ps1 -Json -agent_scripts: - sh: scripts/bash/update-agent-context.sh __AGENT__ - ps: scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__ --- ## User Input @@ -145,15 +142,11 @@ You **MUST** consider the user input before proceeding (if not empty). - Skip if project is purely internal (build scripts, one-off tools, etc.) 3. **Agent context update**: - - Run `{AGENT_SCRIPT}` - - These scripts detect which AI agent is in use - - Update the appropriate agent-specific context file - - Add only new technology from current plan - - Preserve manual additions between markers + - Update the plan reference between the `` and `` markers in `__CONTEXT_FILE__` to point to the plan file created in step 1 (the IMPL_PLAN path) -**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file +**Output**: data-model.md, /contracts/*, quickstart.md, updated agent context file ## Key rules -- Use absolute paths +- Use absolute paths for filesystem operations; use project-relative paths for references in documentation and agent context files - ERROR on gate failures or unresolved clarifications diff --git a/tests/integrations/test_cli.py b/tests/integrations/test_cli.py index bd73ccd664..04a91682e8 100644 --- a/tests/integrations/test_cli.py +++ b/tests/integrations/test_cli.py @@ -56,14 +56,19 @@ def test_integration_copilot_creates_files(self, tmp_path): data = json.loads((project / ".specify" / "integration.json").read_text(encoding="utf-8")) assert data["integration"] == "copilot" - assert "scripts" in data - assert "update-context" in data["scripts"] opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8")) assert opts["integration"] == "copilot" + assert opts["context_file"] == ".github/copilot-instructions.md" assert (project / ".specify" / "integrations" / "copilot.manifest.json").exists() - assert (project / ".specify" / "integrations" / "copilot" / "scripts" / "update-context.sh").exists() + + # Context section should be upserted into the copilot instructions file + ctx_file = project / ".github" / "copilot-instructions.md" + assert ctx_file.exists() + ctx_content = ctx_file.read_text(encoding="utf-8") + assert "" in ctx_content + assert "" in ctx_content shared_manifest = project / ".specify" / "integrations" / "speckit.manifest.json" assert shared_manifest.exists() diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index 3700d35de5..f22bb298b4 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -99,7 +99,23 @@ def test_templates_are_processed(self, tmp_path): assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__" assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}" assert "\nscripts:\n" not in content, f"{f.name} has unstripped scripts: block" - assert "\nagent_scripts:\n" not in content, f"{f.name} has unstripped agent_scripts: block" + + def test_plan_references_correct_context_file(self, tmp_path): + """The generated plan command must reference this integration's context file.""" + i = get_integration(self.KEY) + if not i.context_file: + return + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m) + plan_file = i.commands_dest(tmp_path) / i.command_filename("plan") + assert plan_file.exists(), f"Plan file {plan_file} not created" + content = plan_file.read_text(encoding="utf-8") + assert i.context_file in content, ( + f"Plan command should reference {i.context_file!r} but it was not found in {plan_file.name}" + ) + assert "__CONTEXT_FILE__" not in content, ( + f"Plan command has unprocessed __CONTEXT_FILE__ placeholder in {plan_file.name}" + ) def test_all_files_tracked_in_manifest(self, tmp_path): i = get_integration(self.KEY) @@ -132,30 +148,35 @@ def test_modified_file_survives_uninstall(self, tmp_path): assert modified_file.exists() assert modified_file in skipped - # -- Scripts ---------------------------------------------------------- - - def test_setup_installs_update_context_scripts(self, tmp_path): - i = get_integration(self.KEY) - m = IntegrationManifest(self.KEY, tmp_path) - created = i.setup(tmp_path, m) - scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" - assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" - assert (scripts_dir / "update-context.sh").exists() - assert (scripts_dir / "update-context.ps1").exists() + # -- Context section --------------------------------------------------- - def test_scripts_tracked_in_manifest(self, tmp_path): + def test_setup_upserts_context_section(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) i.setup(tmp_path, m) - script_rels = [k for k in m.files if "update-context" in k] - assert len(script_rels) >= 2 - - def test_sh_script_is_executable(self, tmp_path): + if i.context_file: + ctx_path = tmp_path / i.context_file + assert ctx_path.exists(), f"Context file {i.context_file} not created for {self.KEY}" + content = ctx_path.read_text(encoding="utf-8") + assert "" in content + assert "" in content + assert "read the current plan" in content + + def test_teardown_removes_context_section(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) i.setup(tmp_path, m) - sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh" - assert os.access(sh, os.X_OK) + m.save() + if i.context_file: + ctx_path = tmp_path / i.context_file + # Add user content around the section + content = ctx_path.read_text(encoding="utf-8") + ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8") + i.teardown(tmp_path, m) + remaining = ctx_path.read_text(encoding="utf-8") + assert "" not in remaining + assert "" not in remaining + assert "# My Rules" in remaining # -- CLI auto-promote ------------------------------------------------- @@ -203,6 +224,30 @@ def test_integration_flag_creates_files(self, tmp_path): commands = sorted(cmd_dir.glob("speckit.*")) assert len(commands) > 0, f"No command files in {cmd_dir}" + def test_init_options_includes_context_file(self, tmp_path): + """init-options.json must include context_file for the active integration.""" + import json + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / f"opts-{self.KEY}" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + result = CliRunner().invoke(app, [ + "init", "--here", "--integration", self.KEY, "--script", "sh", + "--no-git", "--ignore-agent-tools", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0 + opts = json.loads((project / ".specify" / "init-options.json").read_text()) + i = get_integration(self.KEY) + assert opts.get("context_file") == i.context_file, ( + f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}" + ) + # -- Complete file inventory ------------------------------------------ COMMAND_STEMS = [ @@ -220,10 +265,6 @@ def _expected_files(self, script_variant: str) -> list[str]: for stem in self.COMMAND_STEMS: files.append(f"{cmd_dir}/speckit.{stem}.md") - # Integration scripts - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") - # Framework files files.append(f".specify/integration.json") files.append(f".specify/init-options.json") @@ -232,14 +273,14 @@ 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", "update-agent-context.sh"]: + "setup-plan.sh"]: files.append(f".specify/scripts/bash/{name}") else: for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1", - "setup-plan.ps1", "update-agent-context.ps1"]: + "setup-plan.ps1"]: files.append(f".specify/scripts/powershell/{name}") - for name in ["agent-file-template.md", "checklist-template.md", + for name in ["checklist-template.md", "constitution-template.md", "plan-template.md", "spec-template.md", "tasks-template.md"]: files.append(f".specify/templates/{name}") @@ -248,6 +289,11 @@ def _expected_files(self, script_variant: str) -> list[str]: # Bundled workflow files.append(".specify/workflows/speckit/workflow.yml") files.append(".specify/workflows/workflow-registry.json") + + # Agent context file (if set) + if i.context_file: + files.append(i.context_file) + return sorted(files) def test_complete_file_inventory_sh(self, tmp_path): diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 72d32278ba..c8c152a84b 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -173,6 +173,23 @@ def test_skill_body_has_content(self, tmp_path): body = parts[2].strip() if len(parts) >= 3 else "" assert len(body) > 0, f"{f} has empty body" + def test_plan_references_correct_context_file(self, tmp_path): + """The generated plan skill must reference this integration's context file.""" + i = get_integration(self.KEY) + if not i.context_file: + return + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m) + plan_file = i.skills_dest(tmp_path) / "speckit-plan" / "SKILL.md" + assert plan_file.exists(), f"Plan skill {plan_file} not created" + content = plan_file.read_text(encoding="utf-8") + assert i.context_file in content, ( + f"Plan skill should reference {i.context_file!r} but it was not found" + ) + assert "__CONTEXT_FILE__" not in content, ( + "Plan skill has unprocessed __CONTEXT_FILE__ placeholder" + ) + def test_all_files_tracked_in_manifest(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) @@ -217,30 +234,34 @@ def test_pre_existing_skills_not_removed(self, tmp_path): assert (foreign_dir / "SKILL.md").exists(), "Foreign skill was removed" - # -- Scripts ---------------------------------------------------------- + # -- Context section --------------------------------------------------- - def test_setup_installs_update_context_scripts(self, tmp_path): + def test_setup_upserts_context_section(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) i.setup(tmp_path, m) - scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" - assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" - assert (scripts_dir / "update-context.sh").exists() - assert (scripts_dir / "update-context.ps1").exists() - - def test_scripts_tracked_in_manifest(self, tmp_path): + if i.context_file: + ctx_path = tmp_path / i.context_file + assert ctx_path.exists(), f"Context file {i.context_file} not created for {self.KEY}" + content = ctx_path.read_text(encoding="utf-8") + assert "" in content + assert "" in content + assert "read the current plan" in content + + def test_teardown_removes_context_section(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) i.setup(tmp_path, m) - script_rels = [k for k in m.files if "update-context" in k] - assert len(script_rels) >= 2 - - def test_sh_script_is_executable(self, tmp_path): - i = get_integration(self.KEY) - m = IntegrationManifest(self.KEY, tmp_path) - i.setup(tmp_path, m) - sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh" - assert os.access(sh, os.X_OK) + m.save() + if i.context_file: + ctx_path = tmp_path / i.context_file + content = ctx_path.read_text(encoding="utf-8") + ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8") + i.teardown(tmp_path, m) + remaining = ctx_path.read_text(encoding="utf-8") + assert "" not in remaining + assert "" not in remaining + assert "# My Rules" in remaining # -- CLI auto-promote ------------------------------------------------- @@ -286,6 +307,30 @@ def test_integration_flag_creates_files(self, tmp_path): skills_dir = i.skills_dest(project) assert skills_dir.is_dir(), f"Skills directory {skills_dir} not created" + def test_init_options_includes_context_file(self, tmp_path): + """init-options.json must include context_file for the active integration.""" + import json + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / f"opts-{self.KEY}" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + result = CliRunner().invoke(app, [ + "init", "--here", "--integration", self.KEY, "--script", "sh", + "--no-git", "--ignore-agent-tools", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0 + opts = json.loads((project / ".specify" / "init-options.json").read_text()) + i = get_integration(self.KEY) + assert opts.get("context_file") == i.context_file, ( + f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}" + ) + # -- IntegrationOption ------------------------------------------------ def test_options_include_skills_flag(self): @@ -316,8 +361,6 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/init-options.json", ".specify/integration.json", f".specify/integrations/{self.KEY}.manifest.json", - f".specify/integrations/{self.KEY}/scripts/update-context.ps1", - f".specify/integrations/{self.KEY}/scripts/update-context.sh", ".specify/integrations/speckit.manifest.json", ".specify/memory/constitution.md", ] @@ -328,7 +371,6 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", ".specify/scripts/bash/setup-plan.sh", - ".specify/scripts/bash/update-agent-context.sh", ] else: files += [ @@ -336,11 +378,9 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/scripts/powershell/common.ps1", ".specify/scripts/powershell/create-new-feature.ps1", ".specify/scripts/powershell/setup-plan.ps1", - ".specify/scripts/powershell/update-agent-context.ps1", ] # Templates files += [ - ".specify/templates/agent-file-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -352,6 +392,9 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/workflows/speckit/workflow.yml", ".specify/workflows/workflow-registry.json", ] + # Agent context file (if set) + if i.context_file: + files.append(i.context_file) return sorted(files) def test_complete_file_inventory_sh(self, tmp_path): diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index e80f9abc10..ca66b2123a 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -310,6 +310,23 @@ def test_toml_is_valid(self, tmp_path): raise AssertionError(f"{f.name} is not valid TOML: {exc}") from exc assert "prompt" in parsed, f"{f.name} parsed TOML has no 'prompt' key" + def test_plan_references_correct_context_file(self, tmp_path): + """The generated plan command must reference this integration's context file.""" + i = get_integration(self.KEY) + if not i.context_file: + return + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m) + plan_file = i.commands_dest(tmp_path) / i.command_filename("plan") + assert plan_file.exists(), f"Plan file {plan_file} not created" + content = plan_file.read_text(encoding="utf-8") + assert i.context_file in content, ( + f"Plan command should reference {i.context_file!r} but it was not found in {plan_file.name}" + ) + assert "__CONTEXT_FILE__" not in content, ( + f"Plan command has unprocessed __CONTEXT_FILE__ placeholder in {plan_file.name}" + ) + def test_all_files_tracked_in_manifest(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) @@ -341,37 +358,34 @@ def test_modified_file_survives_uninstall(self, tmp_path): assert modified_file.exists() assert modified_file in skipped - # -- Scripts ---------------------------------------------------------- - - def test_setup_installs_update_context_scripts(self, tmp_path): - i = get_integration(self.KEY) - m = IntegrationManifest(self.KEY, tmp_path) - created = i.setup(tmp_path, m) - scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" - assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" - assert (scripts_dir / "update-context.sh").exists() - assert (scripts_dir / "update-context.ps1").exists() + # -- Context section --------------------------------------------------- - def test_scripts_tracked_in_manifest(self, tmp_path): + def test_setup_upserts_context_section(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) i.setup(tmp_path, m) - script_rels = [k for k in m.files if "update-context" in k] - assert len(script_rels) >= 2 - - def test_sh_script_is_executable(self, tmp_path): + if i.context_file: + ctx_path = tmp_path / i.context_file + assert ctx_path.exists(), f"Context file {i.context_file} not created for {self.KEY}" + content = ctx_path.read_text(encoding="utf-8") + assert "" in content + assert "" in content + assert "read the current plan" in content + + def test_teardown_removes_context_section(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) i.setup(tmp_path, m) - sh = ( - tmp_path - / ".specify" - / "integrations" - / self.KEY - / "scripts" - / "update-context.sh" - ) - assert os.access(sh, os.X_OK) + m.save() + if i.context_file: + ctx_path = tmp_path / i.context_file + content = ctx_path.read_text(encoding="utf-8") + ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8") + i.teardown(tmp_path, m) + remaining = ctx_path.read_text(encoding="utf-8") + assert "" not in remaining + assert "" not in remaining + assert "# My Rules" in remaining # -- CLI auto-promote ------------------------------------------------- @@ -441,6 +455,30 @@ def test_integration_flag_creates_files(self, tmp_path): commands = sorted(cmd_dir.glob("speckit.*.toml")) assert len(commands) > 0, f"No command files in {cmd_dir}" + def test_init_options_includes_context_file(self, tmp_path): + """init-options.json must include context_file for the active integration.""" + import json + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / f"opts-{self.KEY}" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + result = CliRunner().invoke(app, [ + "init", "--here", "--integration", self.KEY, "--script", "sh", + "--no-git", "--ignore-agent-tools", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0 + opts = json.loads((project / ".specify" / "init-options.json").read_text()) + i = get_integration(self.KEY) + assert opts.get("context_file") == i.context_file, ( + f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}" + ) + # -- Complete file inventory ------------------------------------------ COMMAND_STEMS = [ @@ -465,10 +503,6 @@ def _expected_files(self, script_variant: str) -> list[str]: for stem in self.COMMAND_STEMS: files.append(f"{cmd_dir}/speckit.{stem}.toml") - # Integration scripts - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") - # Framework files files.append(".specify/integration.json") files.append(".specify/init-options.json") @@ -481,7 +515,6 @@ def _expected_files(self, script_variant: str) -> list[str]: "common.sh", "create-new-feature.sh", "setup-plan.sh", - "update-agent-context.sh", ]: files.append(f".specify/scripts/bash/{name}") else: @@ -490,12 +523,10 @@ def _expected_files(self, script_variant: str) -> list[str]: "common.ps1", "create-new-feature.ps1", "setup-plan.ps1", - "update-agent-context.ps1", ]: files.append(f".specify/scripts/powershell/{name}") for name in [ - "agent-file-template.md", "checklist-template.md", "constitution-template.md", "plan-template.md", @@ -508,6 +539,11 @@ def _expected_files(self, script_variant: str) -> list[str]: # Bundled workflow files.append(".specify/workflows/speckit/workflow.yml") files.append(".specify/workflows/workflow-registry.json") + + # Agent context file (if set) + if i.context_file: + files.append(i.context_file) + return sorted(files) def test_complete_file_inventory_sh(self, tmp_path): diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index e4c31b3c88..08f088576c 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -189,6 +189,23 @@ def test_yaml_prompt_excludes_frontmatter(self, tmp_path, monkeypatch): assert "scripts:" not in parsed["prompt"] assert "---" not in parsed["prompt"] + def test_plan_references_correct_context_file(self, tmp_path): + """The generated plan command must reference this integration's context file.""" + i = get_integration(self.KEY) + if not i.context_file: + return + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m) + plan_file = i.commands_dest(tmp_path) / i.command_filename("plan") + assert plan_file.exists(), f"Plan file {plan_file} not created" + content = plan_file.read_text(encoding="utf-8") + assert i.context_file in content, ( + f"Plan command should reference {i.context_file!r} but it was not found in {plan_file.name}" + ) + assert "__CONTEXT_FILE__" not in content, ( + f"Plan command has unprocessed __CONTEXT_FILE__ placeholder in {plan_file.name}" + ) + def test_all_files_tracked_in_manifest(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) @@ -220,37 +237,34 @@ def test_modified_file_survives_uninstall(self, tmp_path): assert modified_file.exists() assert modified_file in skipped - # -- Scripts ---------------------------------------------------------- - - def test_setup_installs_update_context_scripts(self, tmp_path): - i = get_integration(self.KEY) - m = IntegrationManifest(self.KEY, tmp_path) - created = i.setup(tmp_path, m) - scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" - assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" - assert (scripts_dir / "update-context.sh").exists() - assert (scripts_dir / "update-context.ps1").exists() + # -- Context section --------------------------------------------------- - def test_scripts_tracked_in_manifest(self, tmp_path): + def test_setup_upserts_context_section(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) i.setup(tmp_path, m) - script_rels = [k for k in m.files if "update-context" in k] - assert len(script_rels) >= 2 - - def test_sh_script_is_executable(self, tmp_path): + if i.context_file: + ctx_path = tmp_path / i.context_file + assert ctx_path.exists(), f"Context file {i.context_file} not created for {self.KEY}" + content = ctx_path.read_text(encoding="utf-8") + assert "" in content + assert "" in content + assert "read the current plan" in content + + def test_teardown_removes_context_section(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) i.setup(tmp_path, m) - sh = ( - tmp_path - / ".specify" - / "integrations" - / self.KEY - / "scripts" - / "update-context.sh" - ) - assert os.access(sh, os.X_OK) + m.save() + if i.context_file: + ctx_path = tmp_path / i.context_file + content = ctx_path.read_text(encoding="utf-8") + ctx_path.write_text("# My Rules\n\n" + content + "\n# Footer\n", encoding="utf-8") + i.teardown(tmp_path, m) + remaining = ctx_path.read_text(encoding="utf-8") + assert "" not in remaining + assert "" not in remaining + assert "# My Rules" in remaining # -- CLI auto-promote ------------------------------------------------- @@ -320,6 +334,30 @@ def test_integration_flag_creates_files(self, tmp_path): commands = sorted(cmd_dir.glob("speckit.*.yaml")) assert len(commands) > 0, f"No command files in {cmd_dir}" + def test_init_options_includes_context_file(self, tmp_path): + """init-options.json must include context_file for the active integration.""" + import json + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / f"opts-{self.KEY}" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + result = CliRunner().invoke(app, [ + "init", "--here", "--integration", self.KEY, "--script", "sh", + "--no-git", "--ignore-agent-tools", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0 + opts = json.loads((project / ".specify" / "init-options.json").read_text()) + i = get_integration(self.KEY) + assert opts.get("context_file") == i.context_file, ( + f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}" + ) + # -- Complete file inventory ------------------------------------------ COMMAND_STEMS = [ @@ -344,10 +382,6 @@ def _expected_files(self, script_variant: str) -> list[str]: for stem in self.COMMAND_STEMS: files.append(f"{cmd_dir}/speckit.{stem}.yaml") - # Integration scripts - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") - # Framework files files.append(".specify/integration.json") files.append(".specify/init-options.json") @@ -360,7 +394,6 @@ def _expected_files(self, script_variant: str) -> list[str]: "common.sh", "create-new-feature.sh", "setup-plan.sh", - "update-agent-context.sh", ]: files.append(f".specify/scripts/bash/{name}") else: @@ -369,12 +402,10 @@ def _expected_files(self, script_variant: str) -> list[str]: "common.ps1", "create-new-feature.ps1", "setup-plan.ps1", - "update-agent-context.ps1", ]: files.append(f".specify/scripts/powershell/{name}") for name in [ - "agent-file-template.md", "checklist-template.md", "constitution-template.md", "plan-template.md", @@ -387,6 +418,11 @@ def _expected_files(self, script_variant: str) -> list[str]: # Bundled workflow files.append(".specify/workflows/speckit/workflow.yml") files.append(".specify/workflows/workflow-registry.json") + + # Agent context file (if set) + if i.context_file: + files.append(i.context_file) + return sorted(files) def test_complete_file_inventory_sh(self, tmp_path): diff --git a/tests/integrations/test_integration_catalog.py b/tests/integrations/test_integration_catalog.py index 3d0a14acdc..6d82a6c390 100644 --- a/tests/integrations/test_integration_catalog.py +++ b/tests/integrations/test_integration_catalog.py @@ -285,7 +285,7 @@ def test_clear_cache(self, tmp_path): "commands": [ {"name": "speckit.specify", "file": "templates/speckit.specify.md"}, ], - "scripts": ["update-context.sh"], + "scripts": [], }, } @@ -305,7 +305,7 @@ def test_valid_descriptor(self, tmp_path): assert desc.description == "Integration for My Agent" assert desc.requires_speckit_version == ">=0.6.0" assert len(desc.commands) == 1 - assert desc.scripts == ["update-context.sh"] + assert desc.scripts == [] def test_missing_schema_version(self, tmp_path): data = {**VALID_DESCRIPTOR} diff --git a/tests/integrations/test_integration_claude.py b/tests/integrations/test_integration_claude.py index d3b01097fc..153983dcf4 100644 --- a/tests/integrations/test_integration_claude.py +++ b/tests/integrations/test_integration_claude.py @@ -62,19 +62,17 @@ def test_setup_creates_skill_files(self, tmp_path): assert parsed["disable-model-invocation"] is False assert parsed["metadata"]["source"] == "templates/commands/plan.md" - def test_setup_installs_update_context_scripts(self, tmp_path): + def test_setup_upserts_context_section(self, tmp_path): integration = get_integration("claude") manifest = IntegrationManifest("claude", tmp_path) - created = integration.setup(tmp_path, manifest, script_type="sh") - - scripts_dir = tmp_path / ".specify" / "integrations" / "claude" / "scripts" - assert scripts_dir.is_dir() - assert (scripts_dir / "update-context.sh").exists() - assert (scripts_dir / "update-context.ps1").exists() - - tracked = {path.resolve().relative_to(tmp_path.resolve()).as_posix() for path in created} - assert ".specify/integrations/claude/scripts/update-context.sh" in tracked - assert ".specify/integrations/claude/scripts/update-context.ps1" in tracked + integration.setup(tmp_path, manifest, script_type="sh") + + ctx_path = tmp_path / integration.context_file + assert ctx_path.exists() + content = ctx_path.read_text(encoding="utf-8") + assert "" in content + assert "" in content + assert "read the current plan" in content def test_ai_flag_auto_promotes_and_enables_skills(self, tmp_path): from typer.testing import CliRunner diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index 34a9d54945..642b1e5300 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -143,7 +143,20 @@ def test_templates_are_processed(self, tmp_path): assert "__AGENT__" not in content, f"{agent_file.name} has unprocessed __AGENT__" assert "{ARGS}" not in content, f"{agent_file.name} has unprocessed {{ARGS}}" assert "\nscripts:\n" not in content - assert "\nagent_scripts:\n" not in content + + def test_plan_references_correct_context_file(self, tmp_path): + """The generated plan command must reference copilot's context file.""" + from specify_cli.integrations.copilot import CopilotIntegration + copilot = CopilotIntegration() + m = IntegrationManifest("copilot", tmp_path) + copilot.setup(tmp_path, m) + plan_file = tmp_path / ".github" / "agents" / "speckit.plan.agent.md" + assert plan_file.exists() + content = plan_file.read_text(encoding="utf-8") + assert copilot.context_file in content, ( + f"Plan command should reference {copilot.context_file!r}" + ) + assert "__CONTEXT_FILE__" not in content def test_complete_file_inventory_sh(self, tmp_path): """Every file produced by specify init --integration copilot --script sh.""" @@ -181,18 +194,15 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/prompts/speckit.tasks.prompt.md", ".github/prompts/speckit.taskstoissues.prompt.md", ".vscode/settings.json", + ".github/copilot-instructions.md", ".specify/integration.json", ".specify/init-options.json", ".specify/integrations/copilot.manifest.json", ".specify/integrations/speckit.manifest.json", - ".specify/integrations/copilot/scripts/update-context.ps1", - ".specify/integrations/copilot/scripts/update-context.sh", ".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/update-agent-context.sh", - ".specify/templates/agent-file-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -243,18 +253,15 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/prompts/speckit.tasks.prompt.md", ".github/prompts/speckit.taskstoissues.prompt.md", ".vscode/settings.json", + ".github/copilot-instructions.md", ".specify/integration.json", ".specify/init-options.json", ".specify/integrations/copilot.manifest.json", ".specify/integrations/speckit.manifest.json", - ".specify/integrations/copilot/scripts/update-context.ps1", - ".specify/integrations/copilot/scripts/update-context.sh", ".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/update-agent-context.ps1", - ".specify/templates/agent-file-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", diff --git a/tests/integrations/test_integration_cursor_agent.py b/tests/integrations/test_integration_cursor_agent.py index 3384fdc14f..352a0475b5 100644 --- a/tests/integrations/test_integration_cursor_agent.py +++ b/tests/integrations/test_integration_cursor_agent.py @@ -1,5 +1,10 @@ """Tests for CursorAgentIntegration.""" +from pathlib import Path + +from specify_cli.integrations import get_integration +from specify_cli.integrations.manifest import IntegrationManifest + from .test_integration_base_skills import SkillsIntegrationTests @@ -11,6 +16,81 @@ class TestCursorAgentIntegration(SkillsIntegrationTests): CONTEXT_FILE = ".cursor/rules/specify-rules.mdc" +class TestCursorMdcFrontmatter: + """Verify .mdc frontmatter handling in upsert/remove context section.""" + + def _setup(self, tmp_path: Path): + i = get_integration("cursor-agent") + m = IntegrationManifest("cursor-agent", tmp_path) + return i, m + + def test_new_mdc_gets_frontmatter(self, tmp_path): + """A freshly created .mdc file includes alwaysApply: true.""" + i, m = self._setup(tmp_path) + i.setup(tmp_path, m) + ctx = (tmp_path / i.context_file).read_text(encoding="utf-8") + assert ctx.startswith("---\n") + assert "alwaysApply: true" in ctx + + def test_existing_mdc_without_frontmatter_gets_it(self, tmp_path): + """An existing .mdc without frontmatter gets it added.""" + i, m = self._setup(tmp_path) + ctx_path = tmp_path / i.context_file + ctx_path.parent.mkdir(parents=True, exist_ok=True) + ctx_path.write_text("# User rules\n", encoding="utf-8") + i.upsert_context_section(tmp_path) + content = ctx_path.read_text(encoding="utf-8") + assert content.lstrip().startswith("---") + assert "alwaysApply: true" in content + assert "# User rules" in content + + def test_existing_mdc_with_frontmatter_preserves_it(self, tmp_path): + """An existing .mdc with custom frontmatter is preserved.""" + i, m = self._setup(tmp_path) + ctx_path = tmp_path / i.context_file + ctx_path.parent.mkdir(parents=True, exist_ok=True) + ctx_path.write_text( + "---\nalwaysApply: true\ncustomKey: hello\n---\n\n# Rules\n", + encoding="utf-8", + ) + i.upsert_context_section(tmp_path) + content = ctx_path.read_text(encoding="utf-8") + assert "alwaysApply: true" in content + assert "customKey: hello" in content + assert "" in content + + def test_existing_mdc_wrong_alwaysapply_fixed(self, tmp_path): + """An .mdc with alwaysApply: false gets corrected.""" + i, m = self._setup(tmp_path) + ctx_path = tmp_path / i.context_file + ctx_path.parent.mkdir(parents=True, exist_ok=True) + ctx_path.write_text( + "---\nalwaysApply: false\n---\n\n# Rules\n", + encoding="utf-8", + ) + i.upsert_context_section(tmp_path) + content = ctx_path.read_text(encoding="utf-8") + assert "alwaysApply: true" in content + assert "alwaysApply: false" not in content + + def test_upsert_idempotent_no_duplicate_frontmatter(self, tmp_path): + """Repeated upserts don't duplicate frontmatter.""" + i, m = self._setup(tmp_path) + i.upsert_context_section(tmp_path) + i.upsert_context_section(tmp_path) + content = (tmp_path / i.context_file).read_text(encoding="utf-8") + assert content.count("alwaysApply") == 1 + + def test_remove_deletes_mdc_with_only_frontmatter(self, tmp_path): + """Removing the section from a Speckit-only .mdc deletes the file.""" + i, m = self._setup(tmp_path) + i.upsert_context_section(tmp_path) + ctx_path = tmp_path / i.context_file + assert ctx_path.exists() + i.remove_context_section(tmp_path) + assert not ctx_path.exists() + + class TestCursorAgentAutoPromote: """--ai cursor-agent auto-promotes to integration path.""" diff --git a/tests/integrations/test_integration_forge.py b/tests/integrations/test_integration_forge.py index 7affd0d160..613ede91c0 100644 --- a/tests/integrations/test_integration_forge.py +++ b/tests/integrations/test_integration_forge.py @@ -73,19 +73,16 @@ def test_setup_creates_md_files(self, tmp_path): for f in command_files: assert f.name.endswith(".md") - def test_setup_installs_update_scripts(self, tmp_path): + def test_setup_upserts_context_section(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) - created = forge.setup(tmp_path, m) - script_files = [f for f in created if "scripts" in f.parts] - assert len(script_files) > 0 - sh_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.sh" - ps_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.ps1" - assert sh_script in created - assert ps_script in created - assert sh_script.exists() - assert ps_script.exists() + forge.setup(tmp_path, m) + ctx_path = tmp_path / forge.context_file + assert ctx_path.exists() + content = ctx_path.read_text(encoding="utf-8") + assert "" in content + assert "" in content def test_all_created_files_tracked_in_manifest(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration @@ -159,7 +156,20 @@ def test_templates_are_processed(self, tmp_path): assert "$ARGUMENTS" not in content, f"{cmd_file.name} has unprocessed $ARGUMENTS" # Frontmatter sections should be stripped assert "\nscripts:\n" not in content - assert "\nagent_scripts:\n" not in content + + def test_plan_references_correct_context_file(self, tmp_path): + """The generated plan command must reference forge's context file.""" + from specify_cli.integrations.forge import ForgeIntegration + forge = ForgeIntegration() + m = IntegrationManifest("forge", tmp_path) + forge.setup(tmp_path, m) + plan_file = tmp_path / ".forge" / "commands" / "speckit.plan.md" + assert plan_file.exists() + content = plan_file.read_text(encoding="utf-8") + assert forge.context_file in content, ( + f"Plan command should reference {forge.context_file!r}" + ) + assert "__CONTEXT_FILE__" not in content def test_forge_specific_transformations(self, tmp_path): """Test Forge-specific processing: name injection and handoffs stripping.""" diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index 74034ef105..8ca32078b5 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -31,9 +31,9 @@ def test_config_requires_cli_false(self): i = get_integration("generic") assert i.config["requires_cli"] is False - def test_context_file_is_none(self): + def test_context_file_is_agents_md(self): i = get_integration("generic") - assert i.context_file is None + assert i.context_file == "AGENTS.md" # -- Options ---------------------------------------------------------- @@ -158,30 +158,31 @@ def test_different_commands_dirs(self, tmp_path): cmd_files = [f for f in created if "scripts" not in f.parts] assert len(cmd_files) > 0 - # -- Scripts ---------------------------------------------------------- + # -- Context section --------------------------------------------------- - def test_setup_installs_update_context_scripts(self, tmp_path): + def test_setup_upserts_context_section(self, tmp_path): i = get_integration("generic") m = IntegrationManifest("generic", tmp_path) i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"}) - scripts_dir = tmp_path / ".specify" / "integrations" / "generic" / "scripts" - assert scripts_dir.is_dir(), "Scripts directory not created for generic" - assert (scripts_dir / "update-context.sh").exists() - assert (scripts_dir / "update-context.ps1").exists() - - def test_scripts_tracked_in_manifest(self, tmp_path): - i = get_integration("generic") - m = IntegrationManifest("generic", tmp_path) - i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"}) - script_rels = [k for k in m.files if "update-context" in k] - assert len(script_rels) >= 2 - - def test_sh_script_is_executable(self, tmp_path): + if i.context_file: + ctx_path = tmp_path / i.context_file + assert ctx_path.exists() + content = ctx_path.read_text(encoding="utf-8") + assert "" in content + assert "" in content + + def test_plan_references_correct_context_file(self, tmp_path): + """The generated plan command must reference generic's context file.""" i = get_integration("generic") m = IntegrationManifest("generic", tmp_path) i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"}) - sh = tmp_path / ".specify" / "integrations" / "generic" / "scripts" / "update-context.sh" - assert os.access(sh, os.X_OK) + plan_file = tmp_path / ".custom" / "cmds" / "speckit.plan.md" + assert plan_file.exists() + content = plan_file.read_text(encoding="utf-8") + assert i.context_file in content, ( + f"Plan command should reference {i.context_file!r}" + ) + assert "__CONTEXT_FILE__" not in content # -- CLI -------------------------------------------------------------- @@ -198,6 +199,28 @@ def test_cli_generic_without_commands_dir_fails(self, tmp_path): # The integration path validates via setup() assert result.exit_code != 0 + def test_init_options_includes_context_file(self, tmp_path): + """init-options.json must include context_file for the generic integration.""" + import json + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / "opts-generic" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + result = CliRunner().invoke(app, [ + "init", "--here", "--integration", "generic", + "--ai-commands-dir", ".myagent/commands", + "--script", "sh", "--no-git", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0 + opts = json.loads((project / ".specify" / "init-options.json").read_text()) + assert opts.get("context_file") == "AGENTS.md" + def test_complete_file_inventory_sh(self, tmp_path): """Every file produced by specify init --integration generic --ai-commands-dir ... --script sh.""" from typer.testing import CliRunner @@ -221,6 +244,7 @@ def test_complete_file_inventory_sh(self, tmp_path): for p in project.rglob("*") if p.is_file() ) expected = sorted([ + "AGENTS.md", ".myagent/commands/speckit.analyze.md", ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", @@ -233,16 +257,12 @@ def test_complete_file_inventory_sh(self, tmp_path): ".specify/init-options.json", ".specify/integration.json", ".specify/integrations/generic.manifest.json", - ".specify/integrations/generic/scripts/update-context.ps1", - ".specify/integrations/generic/scripts/update-context.sh", ".specify/integrations/speckit.manifest.json", ".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/update-agent-context.sh", - ".specify/templates/agent-file-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -279,6 +299,7 @@ def test_complete_file_inventory_ps(self, tmp_path): for p in project.rglob("*") if p.is_file() ) expected = sorted([ + "AGENTS.md", ".myagent/commands/speckit.analyze.md", ".myagent/commands/speckit.checklist.md", ".myagent/commands/speckit.clarify.md", @@ -291,16 +312,12 @@ def test_complete_file_inventory_ps(self, tmp_path): ".specify/init-options.json", ".specify/integration.json", ".specify/integrations/generic.manifest.json", - ".specify/integrations/generic/scripts/update-context.ps1", - ".specify/integrations/generic/scripts/update-context.sh", ".specify/integrations/speckit.manifest.json", ".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/update-agent-context.ps1", - ".specify/templates/agent-file-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", diff --git a/tests/test_agent_config_consistency.py b/tests/test_agent_config_consistency.py index 9cfe1ddbc9..75e80fdf33 100644 --- a/tests/test_agent_config_consistency.py +++ b/tests/test_agent_config_consistency.py @@ -1,6 +1,5 @@ """Consistency checks for agent configuration across runtime surfaces.""" -import re from pathlib import Path from specify_cli import AGENT_CONFIG, AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP @@ -61,20 +60,6 @@ def test_devcontainer_kiro_installer_uses_pinned_checksum(self): assert "sha256sum -c -" in post_create_text assert "KIRO_SKIP_KIRO_INSTALLER_VERIFY" not in post_create_text - def test_agent_context_scripts_use_kiro_cli(self): - """Agent context scripts should advertise kiro-cli and not legacy q agent key.""" - bash_text = ( - REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh" - ).read_text(encoding="utf-8") - pwsh_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - assert "kiro-cli" in bash_text - assert "kiro-cli" in pwsh_text - assert "Amazon Q Developer CLI" not in bash_text - assert "Amazon Q Developer CLI" not in pwsh_text - # --- Tabnine CLI consistency checks --- def test_runtime_config_includes_tabnine(self): @@ -96,20 +81,6 @@ def test_extension_registrar_includes_tabnine(self): assert cfg["args"] == "{{args}}" assert cfg["extension"] == ".toml" - def test_agent_context_scripts_include_tabnine(self): - """Agent context scripts should support tabnine agent type.""" - bash_text = ( - REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh" - ).read_text(encoding="utf-8") - pwsh_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - assert "tabnine" in bash_text - assert "TABNINE_FILE" in bash_text - assert "tabnine" in pwsh_text - assert "TABNINE_FILE" in pwsh_text - def test_ai_help_includes_tabnine(self): """CLI help text for --ai should include tabnine.""" assert "tabnine" in AI_ASSISTANT_HELP @@ -132,18 +103,6 @@ def test_kimi_in_extension_registrar(self): assert kimi_cfg["dir"] == ".kimi/skills" assert kimi_cfg["extension"] == "/SKILL.md" - def test_kimi_in_powershell_validate_set(self): - """PowerShell update-agent-context script should include 'kimi' in ValidateSet.""" - ps_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text) - assert validate_set_match is not None - validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1)) - - assert "kimi" in validate_set_values - def test_ai_help_includes_kimi(self): """CLI help text for --ai should include kimi.""" assert "kimi" in AI_ASSISTANT_HELP @@ -168,32 +127,6 @@ def test_trae_in_extension_registrar(self): assert trae_cfg["args"] == "$ARGUMENTS" assert trae_cfg["extension"] == "/SKILL.md" - def test_trae_in_agent_context_scripts(self): - """Agent context scripts should support trae agent type.""" - bash_text = ( - REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh" - ).read_text(encoding="utf-8") - pwsh_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - assert "trae" in bash_text - assert "TRAE_FILE" in bash_text - assert "trae" in pwsh_text - assert "TRAE_FILE" in pwsh_text - - def test_trae_in_powershell_validate_set(self): - """PowerShell update-agent-context script should include 'trae' in ValidateSet.""" - ps_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text) - assert validate_set_match is not None - validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1)) - - assert "trae" in validate_set_values - def test_ai_help_includes_trae(self): """CLI help text for --ai should include trae.""" assert "trae" in AI_ASSISTANT_HELP @@ -219,32 +152,6 @@ def test_pi_in_extension_registrar(self): assert pi_cfg["args"] == "$ARGUMENTS" assert pi_cfg["extension"] == ".md" - def test_pi_in_powershell_validate_set(self): - """PowerShell update-agent-context script should include 'pi' in ValidateSet.""" - ps_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - validate_set_match = re.search(r"\[ValidateSet\(([^)]*)\)\]", ps_text) - assert validate_set_match is not None - validate_set_values = re.findall(r"'([^']+)'", validate_set_match.group(1)) - - assert "pi" in validate_set_values - - def test_agent_context_scripts_include_pi(self): - """Agent context scripts should support pi agent type.""" - bash_text = ( - REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh" - ).read_text(encoding="utf-8") - pwsh_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - assert "pi" in bash_text - assert "Pi Coding Agent" in bash_text - assert "pi" in pwsh_text - assert "Pi Coding Agent" in pwsh_text - def test_ai_help_includes_pi(self): """CLI help text for --ai should include pi.""" assert "pi" in AI_ASSISTANT_HELP @@ -267,20 +174,6 @@ def test_iflow_in_extension_registrar(self): assert cfg["iflow"]["format"] == "markdown" assert cfg["iflow"]["args"] == "$ARGUMENTS" - def test_iflow_in_agent_context_scripts(self): - """Agent context scripts should support iflow agent type.""" - bash_text = ( - REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh" - ).read_text(encoding="utf-8") - pwsh_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - assert "iflow" in bash_text - assert "IFLOW_FILE" in bash_text - assert "iflow" in pwsh_text - assert "IFLOW_FILE" in pwsh_text - def test_ai_help_includes_iflow(self): """CLI help text for --ai should include iflow.""" assert "iflow" in AI_ASSISTANT_HELP @@ -303,18 +196,6 @@ def test_goose_in_extension_registrar(self): assert cfg["goose"]["format"] == "yaml" assert cfg["goose"]["args"] == "{{args}}" - def test_goose_in_agent_context_scripts(self): - """Agent context scripts should support goose agent type.""" - bash_text = ( - REPO_ROOT / "scripts" / "bash" / "update-agent-context.sh" - ).read_text(encoding="utf-8") - pwsh_text = ( - REPO_ROOT / "scripts" / "powershell" / "update-agent-context.ps1" - ).read_text(encoding="utf-8") - - assert "goose" in bash_text - assert "goose" in pwsh_text - def test_ai_help_includes_goose(self): """CLI help text for --ai should include goose.""" assert "goose" in AI_ASSISTANT_HELP diff --git a/tests/test_cursor_frontmatter.py b/tests/test_cursor_frontmatter.py deleted file mode 100644 index 9f8c31ce10..0000000000 --- a/tests/test_cursor_frontmatter.py +++ /dev/null @@ -1,266 +0,0 @@ -""" -Tests for Cursor .mdc frontmatter generation (issue #669). - -Verifies that update-agent-context.sh properly prepends YAML frontmatter -to .mdc files so that Cursor IDE auto-includes the rules. -""" - -import os -import shutil -import subprocess -import textwrap - -import pytest - -from tests.conftest import requires_bash - -SCRIPT_PATH = os.path.join( - os.path.dirname(__file__), - os.pardir, - "scripts", - "bash", - "update-agent-context.sh", -) - -EXPECTED_FRONTMATTER_LINES = [ - "---", - "description: Project Development Guidelines", - 'globs: ["**/*"]', - "alwaysApply: true", - "---", -] - -requires_git = pytest.mark.skipif( - shutil.which("git") is None, - reason="git is not installed", -) - - -class TestScriptFrontmatterPattern: - """Static analysis — no git required.""" - - def test_create_new_has_mdc_frontmatter_logic(self): - """create_new_agent_file() must contain .mdc frontmatter logic.""" - with open(SCRIPT_PATH, encoding="utf-8") as f: - content = f.read() - assert 'if [[ "$target_file" == *.mdc ]]' in content - assert "alwaysApply: true" in content - - def test_update_existing_has_mdc_frontmatter_logic(self): - """update_existing_agent_file() must also handle .mdc frontmatter.""" - with open(SCRIPT_PATH, encoding="utf-8") as f: - content = f.read() - # There should be two occurrences of the .mdc check — one per function - occurrences = content.count('if [[ "$target_file" == *.mdc ]]') - assert occurrences >= 2, ( - f"Expected at least 2 .mdc frontmatter checks, found {occurrences}" - ) - - def test_powershell_script_has_mdc_frontmatter_logic(self): - """PowerShell script must also handle .mdc frontmatter.""" - ps_path = os.path.join( - os.path.dirname(__file__), - os.pardir, - "scripts", - "powershell", - "update-agent-context.ps1", - ) - with open(ps_path, encoding="utf-8") as f: - content = f.read() - assert "alwaysApply: true" in content - occurrences = content.count(r"\.mdc$") - assert occurrences >= 2, ( - f"Expected at least 2 .mdc frontmatter checks in PS script, found {occurrences}" - ) - - -@requires_git -@requires_bash -class TestCursorFrontmatterIntegration: - """Integration tests using a real git repo.""" - - @pytest.fixture - def git_repo(self, tmp_path): - """Create a minimal git repo with the spec-kit structure.""" - repo = tmp_path / "repo" - repo.mkdir() - - # Init git repo - subprocess.run( - ["git", "init"], cwd=str(repo), capture_output=True, check=True - ) - subprocess.run( - ["git", "config", "user.email", "test@test.com"], - cwd=str(repo), - capture_output=True, - check=True, - ) - subprocess.run( - ["git", "config", "user.name", "Test"], - cwd=str(repo), - capture_output=True, - check=True, - ) - - # Create .specify dir with config - specify_dir = repo / ".specify" - specify_dir.mkdir() - (specify_dir / "config.yaml").write_text( - textwrap.dedent("""\ - project_type: webapp - language: python - framework: fastapi - database: N/A - """) - ) - - # Create template - templates_dir = specify_dir / "templates" - templates_dir.mkdir() - (templates_dir / "agent-file-template.md").write_text( - "# [PROJECT NAME] Development Guidelines\n\n" - "Auto-generated from all feature plans. Last updated: [DATE]\n\n" - "## Active Technologies\n\n" - "[EXTRACTED FROM ALL PLAN.MD FILES]\n\n" - "## Project Structure\n\n" - "[ACTUAL STRUCTURE FROM PLANS]\n\n" - "## Development Commands\n\n" - "[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]\n\n" - "## Coding Conventions\n\n" - "[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]\n\n" - "## Recent Changes\n\n" - "[LAST 3 FEATURES AND WHAT THEY ADDED]\n" - ) - - # Create initial commit - subprocess.run( - ["git", "add", "-A"], cwd=str(repo), capture_output=True, check=True - ) - subprocess.run( - ["git", "commit", "-m", "init"], - cwd=str(repo), - capture_output=True, - check=True, - ) - - # Create a feature branch so CURRENT_BRANCH detection works - subprocess.run( - ["git", "checkout", "-b", "001-test-feature"], - cwd=str(repo), - capture_output=True, - check=True, - ) - - # Create a spec so the script detects the feature - spec_dir = repo / "specs" / "001-test-feature" - spec_dir.mkdir(parents=True) - (spec_dir / "plan.md").write_text( - "# Test Feature Plan\n\n" - "## Technology Stack\n\n" - "- Language: Python\n" - "- Framework: FastAPI\n" - ) - - return repo - - def _run_update(self, repo, agent_type="cursor-agent"): - """Run update-agent-context.sh for a specific agent type.""" - script = os.path.abspath(SCRIPT_PATH) - result = subprocess.run( - ["bash", script, agent_type], - cwd=str(repo), - capture_output=True, - text=True, - timeout=30, - ) - return result - - def test_new_mdc_file_has_frontmatter(self, git_repo): - """Creating a new .mdc file must include YAML frontmatter.""" - result = self._run_update(git_repo) - assert result.returncode == 0, f"Script failed: {result.stderr}" - - mdc_file = git_repo / ".cursor" / "rules" / "specify-rules.mdc" - assert mdc_file.exists(), "Cursor .mdc file was not created" - - content = mdc_file.read_text() - lines = content.splitlines() - - # First line must be the opening --- - assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}" - - # Check all frontmatter lines are present - for expected in EXPECTED_FRONTMATTER_LINES: - assert expected in content, f"Missing frontmatter line: {expected}" - - # Content after frontmatter should be the template content - assert "Development Guidelines" in content - - def test_existing_mdc_without_frontmatter_gets_it_added(self, git_repo): - """Updating an existing .mdc file that lacks frontmatter must add it.""" - # First, create the file WITHOUT frontmatter (simulating pre-fix state) - cursor_dir = git_repo / ".cursor" / "rules" - cursor_dir.mkdir(parents=True, exist_ok=True) - mdc_file = cursor_dir / "specify-rules.mdc" - mdc_file.write_text( - "# repo Development Guidelines\n\n" - "Auto-generated from all feature plans. Last updated: 2025-01-01\n\n" - "## Active Technologies\n\n" - "- Python + FastAPI (main)\n\n" - "## Recent Changes\n\n" - "- main: Added Python + FastAPI\n" - ) - - result = self._run_update(git_repo) - assert result.returncode == 0, f"Script failed: {result.stderr}" - - content = mdc_file.read_text() - lines = content.splitlines() - - assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}" - for expected in EXPECTED_FRONTMATTER_LINES: - assert expected in content, f"Missing frontmatter line: {expected}" - - def test_existing_mdc_with_frontmatter_not_duplicated(self, git_repo): - """Updating an .mdc file that already has frontmatter must not duplicate it.""" - cursor_dir = git_repo / ".cursor" / "rules" - cursor_dir.mkdir(parents=True, exist_ok=True) - mdc_file = cursor_dir / "specify-rules.mdc" - - frontmatter = ( - "---\n" - "description: Project Development Guidelines\n" - 'globs: ["**/*"]\n' - "alwaysApply: true\n" - "---\n\n" - ) - body = ( - "# repo Development Guidelines\n\n" - "Auto-generated from all feature plans. Last updated: 2025-01-01\n\n" - "## Active Technologies\n\n" - "- Python + FastAPI (main)\n\n" - "## Recent Changes\n\n" - "- main: Added Python + FastAPI\n" - ) - mdc_file.write_text(frontmatter + body) - - result = self._run_update(git_repo) - assert result.returncode == 0, f"Script failed: {result.stderr}" - - content = mdc_file.read_text() - # Count occurrences of the frontmatter delimiter - assert content.count("alwaysApply: true") == 1, ( - "Frontmatter was duplicated" - ) - - def test_non_mdc_file_has_no_frontmatter(self, git_repo): - """Non-.mdc agent files (e.g., Claude) must NOT get frontmatter.""" - result = self._run_update(git_repo, agent_type="claude") - assert result.returncode == 0, f"Script failed: {result.stderr}" - - claude_file = git_repo / ".claude" / "CLAUDE.md" - if claude_file.exists(): - content = claude_file.read_text() - assert not content.startswith("---"), ( - "Non-mdc file should not have frontmatter" - ) diff --git a/tests/test_extension_skills.py b/tests/test_extension_skills.py index c9d13382ab..89e8b4a8b8 100644 --- a/tests/test_extension_skills.py +++ b/tests/test_extension_skills.py @@ -396,11 +396,8 @@ def test_skill_registration_resolves_script_placeholders(self, project_dir, temp "description: Scripted plan command\n" "scripts:\n" " sh: ../../scripts/bash/setup-plan.sh --json \"{ARGS}\"\n" - "agent_scripts:\n" - " sh: ../../scripts/bash/update-agent-context.sh __AGENT__\n" "---\n\n" "Run {SCRIPT}\n" - "Then {AGENT_SCRIPT}\n" "Review templates/checklist.md and memory/constitution.md for __AGENT__.\n" ) @@ -409,11 +406,9 @@ def test_skill_registration_resolves_script_placeholders(self, project_dir, temp content = (skills_dir / "speckit-scripted-ext-plan" / "SKILL.md").read_text() assert "{SCRIPT}" not in content - assert "{AGENT_SCRIPT}" not in content assert "{ARGS}" not in content assert "__AGENT__" not in content assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content - assert ".specify/scripts/bash/update-agent-context.sh claude" in content assert ".specify/templates/checklist.md" in content assert ".specify/memory/constitution.md" in content diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 460404d597..5379178afe 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -1334,13 +1334,9 @@ def test_codex_skill_registration_resolves_script_placeholders(self, project_dir scripts: sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}" ps: ../../scripts/powershell/setup-plan.ps1 -Json -agent_scripts: - sh: ../../scripts/bash/update-agent-context.sh __AGENT__ - ps: ../../scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__ --- Run {SCRIPT} -Then {AGENT_SCRIPT} Agent __AGENT__ """ ) @@ -1361,11 +1357,9 @@ def test_codex_skill_registration_resolves_script_placeholders(self, project_dir content = skill_file.read_text() assert "{SCRIPT}" not in content - assert "{AGENT_SCRIPT}" not in content assert "__AGENT__" not in content assert "{ARGS}" not in content assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content - assert ".specify/scripts/bash/update-agent-context.sh codex" in content def test_codex_skill_alias_frontmatter_matches_alias_name(self, project_dir, temp_dir): """Codex alias skills should render their own matching `name:` frontmatter.""" @@ -1451,13 +1445,9 @@ def test_codex_skill_registration_uses_fallback_script_variant_without_init_opti scripts: sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}" ps: ../../scripts/powershell/setup-plan.ps1 -Json -agent_scripts: - sh: ../../scripts/bash/update-agent-context.sh __AGENT__ - ps: ../../scripts/powershell/update-agent-context.ps1 __AGENT__ --- Run {SCRIPT} -Then {AGENT_SCRIPT} """ ) @@ -1474,13 +1464,10 @@ def test_codex_skill_registration_uses_fallback_script_variant_without_init_opti content = skill_file.read_text() assert "{SCRIPT}" not in content - assert "{AGENT_SCRIPT}" not in content if platform.system().lower().startswith("win"): assert ".specify/scripts/powershell/setup-plan.ps1 -Json" in content - assert ".specify/scripts/powershell/update-agent-context.ps1 codex" in content else: assert '.specify/scripts/bash/setup-plan.sh --json "$ARGUMENTS"' in content - assert ".specify/scripts/bash/update-agent-context.sh codex" in content def test_codex_skill_registration_handles_non_dict_init_options( self, project_dir, temp_dir @@ -1577,13 +1564,9 @@ def test_codex_skill_registration_fallback_prefers_powershell_on_windows( scripts: sh: ../../scripts/bash/setup-plan.sh --json "{ARGS}" ps: ../../scripts/powershell/setup-plan.ps1 -Json -agent_scripts: - sh: ../../scripts/bash/update-agent-context.sh __AGENT__ - ps: ../../scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__ --- Run {SCRIPT} -Then {AGENT_SCRIPT} """ ) @@ -1599,7 +1582,6 @@ def test_codex_skill_registration_fallback_prefers_powershell_on_windows( content = skill_file.read_text() assert ".specify/scripts/powershell/setup-plan.ps1 -Json" in content - assert ".specify/scripts/powershell/update-agent-context.ps1 -AgentType codex" in content assert ".specify/scripts/bash/setup-plan.sh" not in content def test_register_commands_for_copilot(self, extension_dir, project_dir): diff --git a/tests/test_presets.py b/tests/test_presets.py index b883d554b0..35c19bdd7f 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -1648,7 +1648,6 @@ def test_url_cache_expired(self, project_dir): "tasks-template", "checklist-template", "constitution-template", - "agent-file-template", ] @@ -2911,7 +2910,7 @@ def test_lean_command_files_exist(self): assert tmpl_path.exists(), f"Missing command file: {tmpl['file']}" def test_lean_commands_have_no_scripts(self): - """Verify lean commands have no scripts or agent_scripts in frontmatter.""" + """Verify lean commands have no scripts in frontmatter.""" from specify_cli.agents import CommandRegistrar for name in LEAN_COMMAND_NAMES: @@ -2919,7 +2918,6 @@ def test_lean_commands_have_no_scripts(self): content = cmd_path.read_text() frontmatter, _ = CommandRegistrar.parse_frontmatter(content) assert "scripts" not in frontmatter, f"{name} should not have scripts in frontmatter" - assert "agent_scripts" not in frontmatter, f"{name} should not have agent_scripts in frontmatter" def test_lean_commands_have_no_hooks(self): """Verify lean commands do not contain extension hook boilerplate."""