diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 675ee3b67..0bede837f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,33 +25,13 @@ jobs: - name: Get latest tag id: get_tag run: | - # Get the latest tag, or use v0.0.0 if no tags exist - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") - echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT - - # Extract version number and increment - VERSION=$(echo $LATEST_TAG | sed 's/v//') - IFS='.' read -ra VERSION_PARTS <<< "$VERSION" - MAJOR=${VERSION_PARTS[0]:-0} - MINOR=${VERSION_PARTS[1]:-0} - PATCH=${VERSION_PARTS[2]:-0} - - # Increment patch version - PATCH=$((PATCH + 1)) - NEW_VERSION="v$MAJOR.$MINOR.$PATCH" - - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "New version will be: $NEW_VERSION" + chmod +x .github/workflows/scripts/get-next-version.sh + .github/workflows/scripts/get-next-version.sh - name: Check if release already exists id: check_release run: | - if gh release view ${{ steps.get_tag.outputs.new_version }} >/dev/null 2>&1; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "Release ${{ steps.get_tag.outputs.new_version }} already exists, skipping..." - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "Release ${{ steps.get_tag.outputs.new_version }} does not exist, proceeding..." - fi + chmod +x .github/workflows/scripts/check-release-exists.sh + .github/workflows/scripts/check-release-exists.sh ${{ steps.get_tag.outputs.new_version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release package variants @@ -63,85 +43,17 @@ jobs: if: steps.check_release.outputs.exists == 'false' id: release_notes run: | - # Get commits since last tag - LAST_TAG=${{ steps.get_tag.outputs.latest_tag }} - if [ "$LAST_TAG" = "v0.0.0" ]; then - # Check how many commits we have and use that as the limit - COMMIT_COUNT=$(git rev-list --count HEAD) - if [ "$COMMIT_COUNT" -gt 10 ]; then - COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~10..HEAD) - else - COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~$COMMIT_COUNT..HEAD 2>/dev/null || git log --oneline --pretty=format:"- %s") - fi - else - COMMITS=$(git log --oneline --pretty=format:"- %s" $LAST_TAG..HEAD) - fi - - # Create release notes - cat > release_notes.md << EOF - Template release ${{ steps.get_tag.outputs.new_version }} - - Updated specification-driven development templates for GitHub Copilot, Claude Code, Gemini CLI, Cursor, Qwen, opencode, Windsurf, and Codex. - - Now includes per-script variants for POSIX shell (sh) and PowerShell (ps). - - Download the template for your preferred AI assistant + script type: - - spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-cursor-sh-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-cursor-ps-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-opencode-sh-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-opencode-ps-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-qwen-sh-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-qwen-ps-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-windsurf-sh-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-windsurf-ps-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-codex-sh-${{ steps.get_tag.outputs.new_version }}.zip - - spec-kit-template-codex-ps-${{ steps.get_tag.outputs.new_version }}.zip - EOF - - echo "Generated release notes:" - cat release_notes.md + chmod +x .github/workflows/scripts/generate-release-notes.sh + .github/workflows/scripts/generate-release-notes.sh ${{ steps.get_tag.outputs.new_version }} ${{ steps.get_tag.outputs.latest_tag }} - name: Create GitHub Release if: steps.check_release.outputs.exists == 'false' run: | - # Remove 'v' prefix from version for release title - VERSION_NO_V=${{ steps.get_tag.outputs.new_version }} - VERSION_NO_V=${VERSION_NO_V#v} - - gh release create ${{ steps.get_tag.outputs.new_version }} \ - spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-cursor-sh-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-cursor-ps-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-opencode-sh-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-opencode-ps-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-qwen-sh-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-qwen-ps-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-windsurf-sh-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-windsurf-ps-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-codex-sh-${{ steps.get_tag.outputs.new_version }}.zip \ - spec-kit-template-codex-ps-${{ steps.get_tag.outputs.new_version }}.zip \ - --title "Spec Kit Templates - $VERSION_NO_V" \ - --notes-file release_notes.md + chmod +x .github/workflows/scripts/create-github-release.sh + .github/workflows/scripts/create-github-release.sh ${{ steps.get_tag.outputs.new_version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Update version in pyproject.toml (for release artifacts only) if: steps.check_release.outputs.exists == 'false' run: | - # Update version in pyproject.toml (remove 'v' prefix for Python versioning) - VERSION=${{ steps.get_tag.outputs.new_version }} - PYTHON_VERSION=${VERSION#v} - - if [ -f "pyproject.toml" ]; then - sed -i "s/version = \".*\"/version = \"$PYTHON_VERSION\"/" pyproject.toml - echo "Updated pyproject.toml version to $PYTHON_VERSION (for release artifacts only)" - fi + chmod +x .github/workflows/scripts/update-version.sh + .github/workflows/scripts/update-version.sh ${{ steps.get_tag.outputs.new_version }} diff --git a/.github/workflows/scripts/check-release-exists.sh b/.github/workflows/scripts/check-release-exists.sh new file mode 100644 index 000000000..161bf208c --- /dev/null +++ b/.github/workflows/scripts/check-release-exists.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +# check-release-exists.sh +# Check if a GitHub release already exists for the given version +# Usage: check-release-exists.sh + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +VERSION="$1" + +if gh release view "$VERSION" >/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Release $VERSION already exists, skipping..." +else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Release $VERSION does not exist, proceeding..." +fi \ No newline at end of file diff --git a/.github/workflows/scripts/create-github-release.sh b/.github/workflows/scripts/create-github-release.sh new file mode 100644 index 000000000..86ce3f73a --- /dev/null +++ b/.github/workflows/scripts/create-github-release.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +# create-github-release.sh +# Create a GitHub release with all template zip files +# Usage: create-github-release.sh + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +VERSION="$1" + +# Remove 'v' prefix from version for release title +VERSION_NO_V=${VERSION#v} + +gh release create "$VERSION" \ + .genreleases/spec-kit-template-copilot-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-copilot-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-claude-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-claude-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-gemini-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-gemini-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-cursor-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-cursor-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-opencode-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-opencode-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-qwen-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-qwen-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-windsurf-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-windsurf-ps-"$VERSION".zip \ + .genreleases/spec-kit-template-codex-sh-"$VERSION".zip \ + .genreleases/spec-kit-template-codex-ps-"$VERSION".zip \ + --title "Spec Kit Templates - $VERSION_NO_V" \ + --notes-file release_notes.md \ No newline at end of file diff --git a/.github/workflows/scripts/create-release-packages.sh b/.github/workflows/scripts/create-release-packages.sh index 05b5cce3f..0021c5601 100644 --- a/.github/workflows/scripts/create-release-packages.sh +++ b/.github/workflows/scripts/create-release-packages.sh @@ -25,7 +25,10 @@ fi echo "Building release packages for $NEW_VERSION" -rm -rf sdd-package-base* sdd-*-package-* spec-kit-template-*-"${NEW_VERSION}".zip || true +# Create and use .genreleases directory for all build artifacts +GENRELEASES_DIR=".genreleases" +mkdir -p "$GENRELEASES_DIR" +rm -rf "$GENRELEASES_DIR"/* || true rewrite_paths() { sed -E \ @@ -82,7 +85,7 @@ generate_commands() { build_variant() { local agent=$1 script=$2 - local base_dir="sdd-${agent}-package-${script}" + local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}" echo "Building $agent ($script) package..." mkdir -p "$base_dir" @@ -158,11 +161,11 @@ build_variant() { mkdir -p "$base_dir/.windsurf/workflows" generate_commands windsurf md "\$ARGUMENTS" "$base_dir/.windsurf/workflows" "$script" ;; codex) - mkdir -p "$base_dir/.codex/commands" - generate_commands codex md "\$ARGUMENTS" "$base_dir/.codex/commands" "$script" ;; + mkdir -p "$base_dir/.codex/prompts" + generate_commands codex md "\$ARGUMENTS" "$base_dir/.codex/prompts" "$script" ;; esac ( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . ) - echo "Created spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" + echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" } # Determine agent list @@ -212,5 +215,5 @@ for agent in "${AGENT_LIST[@]}"; do done done -echo "Archives:" -ls -1 spec-kit-template-*-"${NEW_VERSION}".zip +echo "Archives in $GENRELEASES_DIR:" +ls -1 "$GENRELEASES_DIR"/spec-kit-template-*-"${NEW_VERSION}".zip diff --git a/.github/workflows/scripts/generate-release-notes.sh b/.github/workflows/scripts/generate-release-notes.sh new file mode 100644 index 000000000..f65992d45 --- /dev/null +++ b/.github/workflows/scripts/generate-release-notes.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +# generate-release-notes.sh +# Generate release notes from git history +# Usage: generate-release-notes.sh + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +NEW_VERSION="$1" +LAST_TAG="$2" + +# Get commits since last tag +if [ "$LAST_TAG" = "v0.0.0" ]; then + # Check how many commits we have and use that as the limit + COMMIT_COUNT=$(git rev-list --count HEAD) + if [ "$COMMIT_COUNT" -gt 10 ]; then + COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~10..HEAD) + else + COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~$COMMIT_COUNT..HEAD 2>/dev/null || git log --oneline --pretty=format:"- %s") + fi +else + COMMITS=$(git log --oneline --pretty=format:"- %s" $LAST_TAG..HEAD) +fi + +# Create release notes +cat > release_notes.md << EOF +Template release $NEW_VERSION + +Updated specification-driven development templates for GitHub Copilot, Claude Code, Gemini CLI, Cursor, Qwen, opencode, Windsurf, and Codex. + +Now includes per-script variants for POSIX shell (sh) and PowerShell (ps). + +Download the template for your preferred AI assistant + script type: +- spec-kit-template-copilot-sh-$NEW_VERSION.zip +- spec-kit-template-copilot-ps-$NEW_VERSION.zip +- spec-kit-template-claude-sh-$NEW_VERSION.zip +- spec-kit-template-claude-ps-$NEW_VERSION.zip +- spec-kit-template-gemini-sh-$NEW_VERSION.zip +- spec-kit-template-gemini-ps-$NEW_VERSION.zip +- spec-kit-template-cursor-sh-$NEW_VERSION.zip +- spec-kit-template-cursor-ps-$NEW_VERSION.zip +- spec-kit-template-opencode-sh-$NEW_VERSION.zip +- spec-kit-template-opencode-ps-$NEW_VERSION.zip +- spec-kit-template-qwen-sh-$NEW_VERSION.zip +- spec-kit-template-qwen-ps-$NEW_VERSION.zip +- spec-kit-template-windsurf-sh-$NEW_VERSION.zip +- spec-kit-template-windsurf-ps-$NEW_VERSION.zip +- spec-kit-template-codex-sh-$NEW_VERSION.zip +- spec-kit-template-codex-ps-$NEW_VERSION.zip +EOF + +echo "Generated release notes:" +cat release_notes.md \ No newline at end of file diff --git a/.github/workflows/scripts/get-next-version.sh b/.github/workflows/scripts/get-next-version.sh new file mode 100644 index 000000000..2be0b6cf8 --- /dev/null +++ b/.github/workflows/scripts/get-next-version.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# get-next-version.sh +# Calculate the next version based on the latest git tag and output GitHub Actions variables +# Usage: get-next-version.sh + +# Get the latest tag, or use v0.0.0 if no tags exist +LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") +echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT + +# Extract version number and increment +VERSION=$(echo $LATEST_TAG | sed 's/v//') +IFS='.' read -ra VERSION_PARTS <<< "$VERSION" +MAJOR=${VERSION_PARTS[0]:-0} +MINOR=${VERSION_PARTS[1]:-0} +PATCH=${VERSION_PARTS[2]:-0} + +# Increment patch version +PATCH=$((PATCH + 1)) +NEW_VERSION="v$MAJOR.$MINOR.$PATCH" + +echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT +echo "New version will be: $NEW_VERSION" \ No newline at end of file diff --git a/.github/workflows/scripts/update-version.sh b/.github/workflows/scripts/update-version.sh new file mode 100644 index 000000000..b0dc0e672 --- /dev/null +++ b/.github/workflows/scripts/update-version.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +# update-version.sh +# Update version in pyproject.toml (for release artifacts only) +# Usage: update-version.sh + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +VERSION="$1" + +# Remove 'v' prefix for Python versioning +PYTHON_VERSION=${VERSION#v} + +if [ -f "pyproject.toml" ]; then + sed -i "s/version = \".*\"/version = \"$PYTHON_VERSION\"/" pyproject.toml + echo "Updated pyproject.toml version to $PYTHON_VERSION (for release artifacts only)" +else + echo "Warning: pyproject.toml not found, skipping version update" +fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index 21c7cd017..42a1fbbfa 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ env/ .env .env.local *.lock + +# Spec Kit-specific files +.genreleases/ +*.zip +sdd-*/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f494345e..07e6a2986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to the Specify CLI will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.12] - 2025-09-21 + +### Changed + +- Added additional context for OpenAI Codex users - they need to set an additional environment variable, as described in [#417](https://github.com/github/spec-kit/issues/417). + ## [0.0.11] - 2025-09-20 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9853fd110..06461ee76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,6 +45,33 @@ When working on spec-kit: 3. Test script functionality in the `scripts/` directory 4. Ensure memory files (`memory/constitution.md`) are updated if major process changes are made +## AI contributions in Spec Kit + +We welcome and encourage the use of AI tools to help improve Spec Kit! Many valuable contributions have been enhanced with AI assistance for code generation, issue detection, and feature definition. + +### What we're looking for + +When submitting AI-assisted contributions, please ensure they include: + +- **Human understanding and testing** - You've personally tested the changes and understand what they do +- **Clear rationale** - You can explain why the change is needed and how it fits within Spec Kit's goals +- **Concrete evidence** - Include test cases, scenarios, or examples that demonstrate the improvement +- **Your own analysis** - Share your thoughts on the end-to-end developer experience + +### What we'll close + +We reserve the right to close contributions that appear to be: + +- Untested changes submitted without verification +- Generic suggestions that don't address specific Spec Kit needs +- Bulk submissions that show no human review or understanding + +### Guidelines for success + +The key is demonstrating that you understand and have validated your proposed changes. If a maintainer can easily tell that a contribution was generated entirely by AI without human input or testing, it likely needs more work before submission. + +Contributors who consistently submit low-effort AI-generated changes may be restricted from further contributions at the maintainers' discretion. + ## Resources - [Spec-Driven Development Methodology](./spec-driven.md) diff --git a/README.md b/README.md index b2dbca540..272458097 100644 --- a/README.md +++ b/README.md @@ -319,10 +319,9 @@ At this stage, your project folder contents should resemble the following: │ ├── constitution.md │ └── constitution_update_checklist.md ├── scripts -│ ├── check-task-prerequisites.sh +│ ├── check-prerequisites.sh │ ├── common.sh │ ├── create-new-feature.sh -│ ├── get-feature-paths.sh │ ├── setup-plan.sh │ └── update-claude-md.sh ├── specs @@ -371,10 +370,9 @@ The output of this step will include a number of implementation detail documents │ ├── constitution.md │ └── constitution_update_checklist.md ├── scripts -│ ├── check-task-prerequisites.sh +│ ├── check-prerequisites.sh │ ├── common.sh │ ├── create-new-feature.sh -│ ├── get-feature-paths.sh │ ├── setup-plan.sh │ └── update-claude-md.sh ├── specs diff --git a/pyproject.toml b/pyproject.toml index bac191094..e44820b7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "specify-cli" -version = "0.0.10" +version = "0.0.12" description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)." requires-python = ">=3.11" dependencies = [ diff --git a/scripts/bash/check-implementation-prerequisites.sh b/scripts/bash/check-implementation-prerequisites.sh deleted file mode 100644 index c660ffa78..000000000 --- a/scripts/bash/check-implementation-prerequisites.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -e -JSON_MODE=false -for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; esac; done -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" -eval $(get_feature_paths) -check_feature_branch "$CURRENT_BRANCH" || exit 1 -if [[ ! -d "$FEATURE_DIR" ]]; then echo "ERROR: Feature directory not found: $FEATURE_DIR"; echo "Run /specify first."; exit 1; fi -if [[ ! -f "$IMPL_PLAN" ]]; then echo "ERROR: plan.md not found in $FEATURE_DIR"; echo "Run /plan first."; exit 1; fi -if [[ ! -f "$TASKS" ]]; then echo "ERROR: tasks.md not found in $FEATURE_DIR"; echo "Run /tasks first."; exit 1; fi -if $JSON_MODE; then - docs=(); [[ -f "$RESEARCH" ]] && docs+=("research.md"); [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md"); ([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/"); [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md"); [[ -f "$TASKS" ]] && docs+=("tasks.md"); - json_docs=$(printf '"%s",' "${docs[@]}"); json_docs="[${json_docs%,}]"; printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" -else - echo "FEATURE_DIR:$FEATURE_DIR"; echo "AVAILABLE_DOCS:"; check_file "$RESEARCH" "research.md"; check_file "$DATA_MODEL" "data-model.md"; check_dir "$CONTRACTS_DIR" "contracts/"; check_file "$QUICKSTART" "quickstart.md"; check_file "$TASKS" "tasks.md"; fi \ No newline at end of file diff --git a/scripts/bash/check-prerequisites.sh b/scripts/bash/check-prerequisites.sh new file mode 100644 index 000000000..d8e79301b --- /dev/null +++ b/scripts/bash/check-prerequisites.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +# Consolidated prerequisite checking script +# +# This script provides unified prerequisite checking for Spec-Driven Development workflow. +# It replaces the functionality previously spread across multiple scripts. +# +# Usage: ./check-prerequisites.sh [OPTIONS] +# +# OPTIONS: +# --json Output in JSON format +# --require-tasks Require tasks.md to exist (for implementation phase) +# --include-tasks Include tasks.md in AVAILABLE_DOCS list +# --paths-only Only output path variables (no validation) +# --help, -h Show help message +# +# OUTPUTS: +# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]} +# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md +# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc. + +set -e + +# Parse command line arguments +JSON_MODE=false +REQUIRE_TASKS=false +INCLUDE_TASKS=false +PATHS_ONLY=false + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --require-tasks) + REQUIRE_TASKS=true + ;; + --include-tasks) + INCLUDE_TASKS=true + ;; + --paths-only) + PATHS_ONLY=true + ;; + --help|-h) + cat << 'EOF' +Usage: check-prerequisites.sh [OPTIONS] + +Consolidated prerequisite checking for Spec-Driven Development workflow. + +OPTIONS: + --json Output in JSON format + --require-tasks Require tasks.md to exist (for implementation phase) + --include-tasks Include tasks.md in AVAILABLE_DOCS list + --paths-only Only output path variables (no prerequisite validation) + --help, -h Show this help message + +EXAMPLES: + # Check task prerequisites (plan.md required) + ./check-prerequisites.sh --json + + # Check implementation prerequisites (plan.md + tasks.md required) + ./check-prerequisites.sh --json --require-tasks --include-tasks + + # Get feature paths only (no validation) + ./check-prerequisites.sh --paths-only + +EOF + exit 0 + ;; + *) + echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2 + exit 1 + ;; + esac +done + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get feature paths and validate branch +eval $(get_feature_paths) +check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 + +# If paths-only mode, output paths and exit +if $PATHS_ONLY; then + echo "REPO_ROOT: $REPO_ROOT" + echo "BRANCH: $CURRENT_BRANCH" + echo "FEATURE_DIR: $FEATURE_DIR" + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "TASKS: $TASKS" + exit 0 +fi + +# Validate required directories and files +if [[ ! -d "$FEATURE_DIR" ]]; then + echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 + echo "Run /specify first to create the feature structure." >&2 + exit 1 +fi + +if [[ ! -f "$IMPL_PLAN" ]]; then + echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 + echo "Run /plan first to create the implementation plan." >&2 + exit 1 +fi + +# Check for tasks.md if required +if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then + echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 + echo "Run /tasks first to create the task list." >&2 + exit 1 +fi + +# Build list of available documents +docs=() + +# Always check these optional docs +[[ -f "$RESEARCH" ]] && docs+=("research.md") +[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") + +# Check contracts directory (only if it exists and has files) +if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then + docs+=("contracts/") +fi + +[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") + +# Include tasks.md if requested and it exists +if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then + docs+=("tasks.md") +fi + +# Output results +if $JSON_MODE; then + # Build JSON array of documents + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + json_docs=$(printf '"%s",' "${docs[@]}") + json_docs="[${json_docs%,}]" + fi + + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" +else + # Text output + echo "FEATURE_DIR:$FEATURE_DIR" + echo "AVAILABLE_DOCS:" + + # Show status of each potential document + check_file "$RESEARCH" "research.md" + check_file "$DATA_MODEL" "data-model.md" + check_dir "$CONTRACTS_DIR" "contracts/" + check_file "$QUICKSTART" "quickstart.md" + + if $INCLUDE_TASKS; then + check_file "$TASKS" "tasks.md" + fi +fi \ No newline at end of file diff --git a/scripts/bash/check-task-prerequisites.sh b/scripts/bash/check-task-prerequisites.sh deleted file mode 100644 index e578f8646..000000000 --- a/scripts/bash/check-task-prerequisites.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -e -JSON_MODE=false -for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; esac; done -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" -eval $(get_feature_paths) -check_feature_branch "$CURRENT_BRANCH" || exit 1 -if [[ ! -d "$FEATURE_DIR" ]]; then echo "ERROR: Feature directory not found: $FEATURE_DIR"; echo "Run /specify first."; exit 1; fi -if [[ ! -f "$IMPL_PLAN" ]]; then echo "ERROR: plan.md not found in $FEATURE_DIR"; echo "Run /plan first."; exit 1; fi -if $JSON_MODE; then - docs=(); [[ -f "$RESEARCH" ]] && docs+=("research.md"); [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md"); ([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/"); [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md"); - json_docs=$(printf '"%s",' "${docs[@]}"); json_docs="[${json_docs%,}]"; printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" -else - echo "FEATURE_DIR:$FEATURE_DIR"; echo "AVAILABLE_DOCS:"; check_file "$RESEARCH" "research.md"; check_file "$DATA_MODEL" "data-model.md"; check_dir "$CONTRACTS_DIR" "contracts/"; check_file "$QUICKSTART" "quickstart.md"; fi diff --git a/scripts/bash/common.sh b/scripts/bash/common.sh index 582d940de..34e5d4bb7 100644 --- a/scripts/bash/common.sh +++ b/scripts/bash/common.sh @@ -1,16 +1,84 @@ #!/usr/bin/env bash -# (Moved to scripts/bash/) Common functions and variables for all scripts +# Common functions and variables for all scripts -get_repo_root() { git rev-parse --show-toplevel; } -get_current_branch() { git rev-parse --abbrev-ref HEAD; } +# Get repository root, with fallback for non-git repositories +get_repo_root() { + if git rev-parse --show-toplevel >/dev/null 2>&1; then + git rev-parse --show-toplevel + else + # Fall back to script location for non-git repos + local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../../.." && pwd) + fi +} + +# Get current branch, with fallback for non-git repositories +get_current_branch() { + # First check if SPECIFY_FEATURE environment variable is set + if [[ -n "${SPECIFY_FEATURE:-}" ]]; then + echo "$SPECIFY_FEATURE" + return + fi + + # Then check git if available + if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then + git rev-parse --abbrev-ref HEAD + return + fi + + # For non-git repos, try to find the latest feature directory + local repo_root=$(get_repo_root) + local specs_dir="$repo_root/specs" + + if [[ -d "$specs_dir" ]]; then + local latest_feature="" + local highest=0 + + for dir in "$specs_dir"/*; do + if [[ -d "$dir" ]]; then + local dirname=$(basename "$dir") + if [[ "$dirname" =~ ^([0-9]{3})- ]]; then + local number=${BASH_REMATCH[1]} + number=$((10#$number)) + if [[ "$number" -gt "$highest" ]]; then + highest=$number + latest_feature=$dirname + fi + fi + fi + done + + if [[ -n "$latest_feature" ]]; then + echo "$latest_feature" + return + fi + fi + + echo "main" # Final fallback +} + +# Check if we have git available +has_git() { + git rev-parse --show-toplevel >/dev/null 2>&1 +} check_feature_branch() { local branch="$1" + local has_git_repo="$2" + + # For non-git repos, we can't enforce branch naming but still provide output + if [[ "$has_git_repo" != "true" ]]; then + echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 + return 0 + fi + if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 echo "Feature branches should be named like: 001-feature-name" >&2 return 1 - fi; return 0 + fi + + return 0 } get_feature_dir() { echo "$1/specs/$2"; } @@ -18,10 +86,18 @@ get_feature_dir() { echo "$1/specs/$2"; } get_feature_paths() { local repo_root=$(get_repo_root) local current_branch=$(get_current_branch) + local has_git_repo="false" + + if has_git; then + has_git_repo="true" + fi + local feature_dir=$(get_feature_dir "$repo_root" "$current_branch") + cat </dev/null 2>&1; then REPO_ROOT=$(git rev-parse --show-toplevel) HAS_GIT=true else - REPO_ROOT="$FALLBACK_ROOT" + REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")" + if [ -z "$REPO_ROOT" ]; then + echo "Error: Could not determine repository root. Please run this script from within the repository." >&2 + exit 1 + fi HAS_GIT=false fi @@ -67,10 +84,14 @@ TEMPLATE="$REPO_ROOT/templates/spec-template.md" SPEC_FILE="$FEATURE_DIR/spec.md" if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi +# Set the SPECIFY_FEATURE environment variable for the current session +export SPECIFY_FEATURE="$BRANCH_NAME" + if $JSON_MODE; then printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" else echo "BRANCH_NAME: $BRANCH_NAME" echo "SPEC_FILE: $SPEC_FILE" echo "FEATURE_NUM: $FEATURE_NUM" + echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" fi diff --git a/scripts/bash/get-feature-paths.sh b/scripts/bash/get-feature-paths.sh deleted file mode 100644 index 016727dbd..000000000 --- a/scripts/bash/get-feature-paths.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -e -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" -eval $(get_feature_paths) -check_feature_branch "$CURRENT_BRANCH" || exit 1 -echo "REPO_ROOT: $REPO_ROOT"; echo "BRANCH: $CURRENT_BRANCH"; echo "FEATURE_DIR: $FEATURE_DIR"; echo "FEATURE_SPEC: $FEATURE_SPEC"; echo "IMPL_PLAN: $IMPL_PLAN"; echo "TASKS: $TASKS" diff --git a/scripts/bash/setup-plan.sh b/scripts/bash/setup-plan.sh index 1da426573..654ba50d7 100644 --- a/scripts/bash/setup-plan.sh +++ b/scripts/bash/setup-plan.sh @@ -1,17 +1,60 @@ #!/usr/bin/env bash + set -e + +# Parse command line arguments JSON_MODE=false -for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; esac; done +ARGS=() + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --help|-h) + echo "Usage: $0 [--json]" + echo " --json Output results in JSON format" + echo " --help Show this help message" + exit 0 + ;; + *) + ARGS+=("$arg") + ;; + esac +done + +# Get script directory and load common functions SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" + +# Get all paths and variables from common functions eval $(get_feature_paths) -check_feature_branch "$CURRENT_BRANCH" || exit 1 + +# Check if we're on a proper feature branch (only for git repos) +check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 + +# Ensure the feature directory exists mkdir -p "$FEATURE_DIR" + +# Copy plan template if it exists TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" -[[ -f "$TEMPLATE" ]] && cp "$TEMPLATE" "$IMPL_PLAN" -if $JSON_MODE; then - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \ - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" +if [[ -f "$TEMPLATE" ]]; then + cp "$TEMPLATE" "$IMPL_PLAN" + echo "Copied plan template to $IMPL_PLAN" else - echo "FEATURE_SPEC: $FEATURE_SPEC"; echo "IMPL_PLAN: $IMPL_PLAN"; echo "SPECS_DIR: $FEATURE_DIR"; echo "BRANCH: $CURRENT_BRANCH" + echo "Warning: Plan template not found at $TEMPLATE" + # Create a basic plan file if template doesn't exist + touch "$IMPL_PLAN" +fi + +# Output results +if $JSON_MODE; then + printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ + "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" +else + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "SPECS_DIR: $FEATURE_DIR" + echo "BRANCH: $CURRENT_BRANCH" + echo "HAS_GIT: $HAS_GIT" fi diff --git a/scripts/bash/update-agent-context.sh b/scripts/bash/update-agent-context.sh index 4f9e6e31c..96131f5a4 100644 --- a/scripts/bash/update-agent-context.sh +++ b/scripts/bash/update-agent-context.sh @@ -48,13 +48,17 @@ set -o pipefail # Configuration and Global Variables #============================================================================== -REPO_ROOT=$(git rev-parse --show-toplevel) -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH" -NEW_PLAN="$FEATURE_DIR/plan.md" +# Get script directory and load common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths and variables from common functions +eval $(get_feature_paths) + +NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code AGENT_TYPE="${1:-}" -# Agent-specific file paths +# Agent-specific file paths CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" GEMINI_FILE="$REPO_ROOT/GEMINI.md" COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md" @@ -108,22 +112,24 @@ trap cleanup EXIT INT TERM #============================================================================== validate_environment() { - # Check if we're in a git repository - if ! git rev-parse --show-toplevel >/dev/null 2>&1; then - log_error "Not in a git repository" - exit 1 - fi - - # Check if we have a current branch + # Check if we have a current branch/feature (git or non-git) if [[ -z "$CURRENT_BRANCH" ]]; then - log_error "Unable to determine current git branch" + 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 on a feature branch with a corresponding spec directory" + 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 @@ -142,9 +148,10 @@ extract_plan_field() { local field_pattern="$1" local plan_file="$2" - grep "^**${field_pattern}**: " "$plan_file" 2>/dev/null | \ + grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ head -1 | \ - sed "s/^**${field_pattern}**: //" | \ + sed "s|^\*\*${field_pattern}\*\*: ||" | \ + sed 's/^[ \t]*//;s/[ \t]*$//' | \ grep -v "NEEDS CLARIFICATION" | \ grep -v "^N/A$" || echo "" } @@ -188,6 +195,31 @@ parse_plan_data() { 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 #============================================================================== @@ -196,12 +228,9 @@ get_project_structure() { local project_type="$1" if [[ "$project_type" == *"web"* ]]; then - echo "backend/ -frontend/ -tests/" + echo "backend/\\nfrontend/\\ntests/" else - echo "src/ -tests/" + echo "src/\\ntests/" fi } @@ -262,169 +291,65 @@ create_new_agent_file() { local language_conventions language_conventions=$(get_language_conventions "$NEW_LANG") - # Perform substitutions with error checking + # Perform substitutions with error checking using safer approach + # Escape special characters for sed by using a different delimiter or escaping + local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g') + local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g') + local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g') + + # 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\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" - "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|" + "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\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" + "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" ) for substitution in "${substitutions[@]}"; do - if ! sed -i.bak "$substitution" "$temp_file"; then + 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 \n sequences to actual newlines + newline=$(printf '\n') + sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" + # Clean up backup files - rm -f "$temp_file.bak" + rm -f "$temp_file.bak" "$temp_file.bak2" return 0 } -update_active_technologies() { - local target_file="$1" - local temp_file="$2" - - # Find the Active Technologies section and add new entries - local tech_section_start - tech_section_start=$(grep -n "## Active Technologies" "$target_file" | cut -d: -f1) - - if [[ -z "$tech_section_start" ]]; then - return 0 # No Active Technologies section found - fi - - # Find the end of the Active Technologies section (next ## heading or empty line) - local tech_section_end - tech_section_end=$(tail -n +$((tech_section_start + 1)) "$target_file" | grep -n "^## \|^$" | head -1 | cut -d: -f1) - - if [[ -n "$tech_section_end" ]]; then - tech_section_end=$((tech_section_start + tech_section_end)) - else - tech_section_end=$(wc -l < "$target_file") - fi - - # Extract existing technologies section - local existing_tech - existing_tech=$(sed -n "${tech_section_start},${tech_section_end}p" "$target_file") - - # Build list of new additions - local additions=() - if [[ -n "$NEW_LANG" ]] && ! echo "$existing_tech" | grep -q "$NEW_LANG"; then - additions+=("- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)") - fi - - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && ! echo "$existing_tech" | grep -q "$NEW_DB"; then - additions+=("- $NEW_DB ($CURRENT_BRANCH)") - fi - - # If we have additions, update the section - if [[ ${#additions[@]} -gt 0 ]]; then - { - # Copy everything before the Active Technologies section - head -n $((tech_section_start)) "$target_file" - - # Copy existing tech section content - sed -n "$((tech_section_start + 1)),$((tech_section_end - 1))p" "$target_file" - - # Add new technologies - printf '%s\n' "${additions[@]}" - echo - - # Copy everything after the Active Technologies section - tail -n +$((tech_section_end + 1)) "$target_file" - } > "$temp_file" - else - cp "$target_file" "$temp_file" - fi -} -update_recent_changes() { - local temp_file="$1" - local temp_file2="$2" - - # Find Recent Changes section - local changes_section_start - changes_section_start=$(grep -n "## Recent Changes" "$temp_file" | cut -d: -f1) - - if [[ -z "$changes_section_start" ]]; then - return 0 # No Recent Changes section found - fi - - # Find the end of the Recent Changes section - local changes_section_end - changes_section_end=$(tail -n +$((changes_section_start + 1)) "$temp_file" | grep -n "^## \|^$" | head -1 | cut -d: -f1) - - if [[ -n "$changes_section_end" ]]; then - changes_section_end=$((changes_section_start + changes_section_end)) - else - changes_section_end=$(wc -l < "$temp_file") - fi - - # Extract existing changes, keep only non-empty lines, and limit to 2 (so we can add 1 new one) - local existing_changes=() - while IFS= read -r line; do - if [[ -n "$line" ]] && [[ "$line" == "- "* ]]; then - existing_changes+=("$line") - fi - done < <(sed -n "$((changes_section_start + 1)),$((changes_section_end - 1))p" "$temp_file") - - # Keep only the first 2 existing changes - existing_changes=("${existing_changes[@]:0:2}") - - # Create updated Recent Changes section - { - # Copy everything before Recent Changes - head -n "$changes_section_start" "$temp_file" - - # Add new change at the top - echo "- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK" - - # Add existing changes (up to 2) - printf '%s\n' "${existing_changes[@]}" - echo - - # Copy everything after Recent Changes section - tail -n +$((changes_section_end + 1)) "$temp_file" - } > "$temp_file2" -} -update_last_updated() { - local temp_file="$1" - local current_date="$2" - - # Update the "Last updated" timestamp - sed -i.bak "s/Last updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/Last updated: $current_date/" "$temp_file" - rm -f "$temp_file.bak" -} -preserve_manual_additions() { - local target_file="$1" - local temp_file="$2" - - # Check if there are manual additions to preserve - local manual_start manual_end - manual_start=$(grep -n "" "$target_file" 2>/dev/null | cut -d: -f1 || echo "") - manual_end=$(grep -n "" "$target_file" 2>/dev/null | cut -d: -f1 || echo "") - - if [[ -n "$manual_start" ]] && [[ -n "$manual_end" ]]; then - # Extract manual additions - local manual_file="/tmp/manual_additions_$$" - sed -n "${manual_start},${manual_end}p" "$target_file" > "$manual_file" - - # Remove any existing manual additions from temp file - sed -i.bak '//,//d' "$temp_file" - rm -f "$temp_file.bak" - - # Append preserved manual additions - cat "$manual_file" >> "$temp_file" - rm -f "$manual_file" - fi -} update_existing_agent_file() { local target_file="$1" @@ -432,26 +357,110 @@ update_existing_agent_file() { log_info "Updating existing agent context file..." - local temp_file1="/tmp/agent_update_1_$$" - local temp_file2="/tmp/agent_update_2_$$" + # Use a single temporary file for atomic update + local temp_file + temp_file=$(mktemp) || { + log_error "Failed to create temporary file" + return 1 + } - # Step 1: Update Active Technologies section - update_active_technologies "$target_file" "$temp_file1" + # Process the file in one pass + local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") + local new_tech_entries=() + local new_change_entry="" - # Step 2: Update Recent Changes section - update_recent_changes "$temp_file1" "$temp_file2" + # Prepare new technology entries + if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then + new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") + fi - # Step 3: Update timestamp - update_last_updated "$temp_file2" "$current_date" + 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 - # Step 4: Preserve manual additions - preserve_manual_additions "$target_file" "$temp_file2" + # 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 - # Move the final result to target - mv "$temp_file2" "$target_file" + # 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 - # Cleanup - rm -f "$temp_file1" + 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" + 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 diff --git a/scripts/powershell/check-implementation-prerequisites.ps1 b/scripts/powershell/check-implementation-prerequisites.ps1 deleted file mode 100644 index 312a3b1f4..000000000 --- a/scripts/powershell/check-implementation-prerequisites.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env pwsh -[CmdletBinding()] -param([switch]$Json) -$ErrorActionPreference = 'Stop' -. "$PSScriptRoot/common.ps1" - -$paths = Get-FeaturePathsEnv -if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 } - -if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) { - Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)" - Write-Output "Run /specify first to create the feature structure." - exit 1 -} -if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) { - Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)" - Write-Output "Run /plan first to create the plan." - exit 1 -} -if (-not (Test-Path $paths.TASKS -PathType Leaf)) { - Write-Output "ERROR: tasks.md not found in $($paths.FEATURE_DIR)" - Write-Output "Run /tasks first to create the task list." - exit 1 -} - -if ($Json) { - $docs = @() - if (Test-Path $paths.RESEARCH) { $docs += 'research.md' } - if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' } - if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) { $docs += 'contracts/' } - if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } - if (Test-Path $paths.TASKS) { $docs += 'tasks.md' } - [PSCustomObject]@{ FEATURE_DIR=$paths.FEATURE_DIR; AVAILABLE_DOCS=$docs } | ConvertTo-Json -Compress -} else { - Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)" - Write-Output "AVAILABLE_DOCS:" - Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null - Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null - Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null - Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null - Test-FileExists -Path $paths.TASKS -Description 'tasks.md' | Out-Null -} \ No newline at end of file diff --git a/scripts/powershell/check-prerequisites.ps1 b/scripts/powershell/check-prerequisites.ps1 new file mode 100644 index 000000000..f039419f7 --- /dev/null +++ b/scripts/powershell/check-prerequisites.ps1 @@ -0,0 +1,137 @@ +#!/usr/bin/env pwsh + +# Consolidated prerequisite checking script (PowerShell) +# +# This script provides unified prerequisite checking for Spec-Driven Development workflow. +# It replaces the functionality previously spread across multiple scripts. +# +# Usage: ./check-prerequisites.ps1 [OPTIONS] +# +# OPTIONS: +# -Json Output in JSON format +# -RequireTasks Require tasks.md to exist (for implementation phase) +# -IncludeTasks Include tasks.md in AVAILABLE_DOCS list +# -PathsOnly Only output path variables (no validation) +# -Help, -h Show help message + +[CmdletBinding()] +param( + [switch]$Json, + [switch]$RequireTasks, + [switch]$IncludeTasks, + [switch]$PathsOnly, + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +# Show help if requested +if ($Help) { + Write-Output @" +Usage: check-prerequisites.ps1 [OPTIONS] + +Consolidated prerequisite checking for Spec-Driven Development workflow. + +OPTIONS: + -Json Output in JSON format + -RequireTasks Require tasks.md to exist (for implementation phase) + -IncludeTasks Include tasks.md in AVAILABLE_DOCS list + -PathsOnly Only output path variables (no prerequisite validation) + -Help, -h Show this help message + +EXAMPLES: + # Check task prerequisites (plan.md required) + .\check-prerequisites.ps1 -Json + + # Check implementation prerequisites (plan.md + tasks.md required) + .\check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks + + # Get feature paths only (no validation) + .\check-prerequisites.ps1 -PathsOnly + +"@ + exit 0 +} + +# Source common functions +. "$PSScriptRoot/common.ps1" + +# Get feature paths and validate branch +$paths = Get-FeaturePathsEnv + +if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GIT)) { + exit 1 +} + +# If paths-only mode, output paths and exit +if ($PathsOnly) { + Write-Output "REPO_ROOT: $($paths.REPO_ROOT)" + Write-Output "BRANCH: $($paths.CURRENT_BRANCH)" + Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)" + Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)" + Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)" + Write-Output "TASKS: $($paths.TASKS)" + exit 0 +} + +# Validate required directories and files +if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) { + Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)" + Write-Output "Run /specify first to create the feature structure." + exit 1 +} + +if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) { + Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)" + Write-Output "Run /plan first to create the implementation plan." + exit 1 +} + +# Check for tasks.md if required +if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) { + Write-Output "ERROR: tasks.md not found in $($paths.FEATURE_DIR)" + Write-Output "Run /tasks first to create the task list." + exit 1 +} + +# Build list of available documents +$docs = @() + +# Always check these optional docs +if (Test-Path $paths.RESEARCH) { $docs += 'research.md' } +if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' } + +# Check contracts directory (only if it exists and has files) +if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) { + $docs += 'contracts/' +} + +if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } + +# Include tasks.md if requested and it exists +if ($IncludeTasks -and (Test-Path $paths.TASKS)) { + $docs += 'tasks.md' +} + +# Output results +if ($Json) { + # JSON output + [PSCustomObject]@{ + FEATURE_DIR = $paths.FEATURE_DIR + AVAILABLE_DOCS = $docs + } | ConvertTo-Json -Compress +} else { + # Text output + Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)" + Write-Output "AVAILABLE_DOCS:" + + # Show status of each potential document + Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null + Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null + Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null + Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null + + if ($IncludeTasks) { + Test-FileExists -Path $paths.TASKS -Description 'tasks.md' | Out-Null + } +} \ No newline at end of file diff --git a/scripts/powershell/check-task-prerequisites.ps1 b/scripts/powershell/check-task-prerequisites.ps1 deleted file mode 100644 index 3be870f31..000000000 --- a/scripts/powershell/check-task-prerequisites.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env pwsh -[CmdletBinding()] -param([switch]$Json) -$ErrorActionPreference = 'Stop' -. "$PSScriptRoot/common.ps1" - -$paths = Get-FeaturePathsEnv -if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 } - -if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) { - Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)" - Write-Output "Run /specify first to create the feature structure." - exit 1 -} -if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) { - Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)" - Write-Output "Run /plan first to create the plan." - exit 1 -} - -if ($Json) { - $docs = @() - if (Test-Path $paths.RESEARCH) { $docs += 'research.md' } - if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' } - if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) { $docs += 'contracts/' } - if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } - [PSCustomObject]@{ FEATURE_DIR=$paths.FEATURE_DIR; AVAILABLE_DOCS=$docs } | ConvertTo-Json -Compress -} else { - Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)" - Write-Output "AVAILABLE_DOCS:" - Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null - Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null - Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null - Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null -} diff --git a/scripts/powershell/common.ps1 b/scripts/powershell/common.ps1 index 3e04a1ece..c8e34b26b 100644 --- a/scripts/powershell/common.ps1 +++ b/scripts/powershell/common.ps1 @@ -1,16 +1,84 @@ #!/usr/bin/env pwsh -# Common PowerShell functions analogous to common.sh (moved to powershell/) +# Common PowerShell functions analogous to common.sh function Get-RepoRoot { - git rev-parse --show-toplevel + try { + $result = git rev-parse --show-toplevel 2>$null + if ($LASTEXITCODE -eq 0) { + return $result + } + } catch { + # Git command failed + } + + # Fall back to script location for non-git repos + return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path } function Get-CurrentBranch { - git rev-parse --abbrev-ref HEAD + # First check if SPECIFY_FEATURE environment variable is set + if ($env:SPECIFY_FEATURE) { + return $env:SPECIFY_FEATURE + } + + # Then check git if available + try { + $result = git rev-parse --abbrev-ref HEAD 2>$null + if ($LASTEXITCODE -eq 0) { + return $result + } + } catch { + # Git command failed + } + + # For non-git repos, try to find the latest feature directory + $repoRoot = Get-RepoRoot + $specsDir = Join-Path $repoRoot "specs" + + if (Test-Path $specsDir) { + $latestFeature = "" + $highest = 0 + + Get-ChildItem -Path $specsDir -Directory | ForEach-Object { + if ($_.Name -match '^(\d{3})-') { + $num = [int]$matches[1] + if ($num -gt $highest) { + $highest = $num + $latestFeature = $_.Name + } + } + } + + if ($latestFeature) { + return $latestFeature + } + } + + # Final fallback + return "main" +} + +function Test-HasGit { + try { + git rev-parse --show-toplevel 2>$null | Out-Null + return ($LASTEXITCODE -eq 0) + } catch { + return $false + } } function Test-FeatureBranch { - param([string]$Branch) + param( + [string]$Branch, + [bool]$HasGit = $true + ) + + # For non-git repos, we can't enforce branch naming but still provide output + if (-not $HasGit) { + Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation" + return $true + } + if ($Branch -notmatch '^[0-9]{3}-') { Write-Output "ERROR: Not on a feature branch. Current branch: $Branch" Write-Output "Feature branches should be named like: 001-feature-name" @@ -27,17 +95,20 @@ function Get-FeatureDir { function Get-FeaturePathsEnv { $repoRoot = Get-RepoRoot $currentBranch = Get-CurrentBranch + $hasGit = Test-HasGit $featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch + [PSCustomObject]@{ - REPO_ROOT = $repoRoot + REPO_ROOT = $repoRoot CURRENT_BRANCH = $currentBranch - FEATURE_DIR = $featureDir - FEATURE_SPEC = Join-Path $featureDir 'spec.md' - IMPL_PLAN = Join-Path $featureDir 'plan.md' - TASKS = Join-Path $featureDir 'tasks.md' - RESEARCH = Join-Path $featureDir 'research.md' - DATA_MODEL = Join-Path $featureDir 'data-model.md' - QUICKSTART = Join-Path $featureDir 'quickstart.md' + HAS_GIT = $hasGit + FEATURE_DIR = $featureDir + FEATURE_SPEC = Join-Path $featureDir 'spec.md' + IMPL_PLAN = Join-Path $featureDir 'plan.md' + TASKS = Join-Path $featureDir 'tasks.md' + RESEARCH = Join-Path $featureDir 'research.md' + DATA_MODEL = Join-Path $featureDir 'data-model.md' + QUICKSTART = Join-Path $featureDir 'quickstart.md' CONTRACTS_DIR = Join-Path $featureDir 'contracts' } } diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index b99f08898..f1c8e04e3 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -1,5 +1,5 @@ #!/usr/bin/env pwsh -# Create a new feature (moved to powershell/) +# Create a new feature [CmdletBinding()] param( [switch]$Json, @@ -9,11 +9,54 @@ param( $ErrorActionPreference = 'Stop' if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) { - Write-Error "Usage: ./create-new-feature.ps1 [-Json] "; exit 1 + Write-Error "Usage: ./create-new-feature.ps1 [-Json] " + exit 1 } $featureDesc = ($FeatureDescription -join ' ').Trim() -$repoRoot = git rev-parse --show-toplevel +# Resolve repository root. Prefer git information when available, but fall back +# to searching for repository markers so the workflow still functions in repositories that +# were initialised with --no-git. +function Find-RepositoryRoot { + param( + [string]$StartDir, + [string[]]$Markers = @('.git', '.specify') + ) + $current = Resolve-Path $StartDir + while ($true) { + foreach ($marker in $Markers) { + if (Test-Path (Join-Path $current $marker)) { + return $current + } + } + $parent = Split-Path $current -Parent + if ($parent -eq $current) { + # Reached filesystem root without finding markers + return $null + } + $current = $parent + } +} +$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot) +if (-not $fallbackRoot) { + Write-Error "Error: Could not determine repository root. Please run this script from within the repository." + exit 1 +} + +try { + $repoRoot = git rev-parse --show-toplevel 2>$null + if ($LASTEXITCODE -eq 0) { + $hasGit = $true + } else { + throw "Git not available" + } +} catch { + $repoRoot = $fallbackRoot + $hasGit = $false +} + +Set-Location $repoRoot + $specsDir = Join-Path $repoRoot 'specs' New-Item -ItemType Directory -Path $specsDir -Force | Out-Null @@ -33,20 +76,42 @@ $branchName = $featureDesc.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', $words = ($branchName -split '-') | Where-Object { $_ } | Select-Object -First 3 $branchName = "$featureNum-$([string]::Join('-', $words))" -git checkout -b $branchName | Out-Null +if ($hasGit) { + try { + git checkout -b $branchName | Out-Null + } catch { + Write-Warning "Failed to create git branch: $branchName" + } +} else { + Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName" +} $featureDir = Join-Path $specsDir $branchName New-Item -ItemType Directory -Path $featureDir -Force | Out-Null $template = Join-Path $repoRoot 'templates/spec-template.md' $specFile = Join-Path $featureDir 'spec.md' -if (Test-Path $template) { Copy-Item $template $specFile -Force } else { New-Item -ItemType File -Path $specFile | Out-Null } +if (Test-Path $template) { + Copy-Item $template $specFile -Force +} else { + New-Item -ItemType File -Path $specFile | Out-Null +} + +# Set the SPECIFY_FEATURE environment variable for the current session +$env:SPECIFY_FEATURE = $branchName if ($Json) { - $obj = [PSCustomObject]@{ BRANCH_NAME = $branchName; SPEC_FILE = $specFile; FEATURE_NUM = $featureNum } + $obj = [PSCustomObject]@{ + BRANCH_NAME = $branchName + SPEC_FILE = $specFile + FEATURE_NUM = $featureNum + HAS_GIT = $hasGit + } $obj | ConvertTo-Json -Compress } else { Write-Output "BRANCH_NAME: $branchName" Write-Output "SPEC_FILE: $specFile" Write-Output "FEATURE_NUM: $featureNum" + Write-Output "HAS_GIT: $hasGit" + Write-Output "SPECIFY_FEATURE environment variable set to: $branchName" } diff --git a/scripts/powershell/get-feature-paths.ps1 b/scripts/powershell/get-feature-paths.ps1 deleted file mode 100644 index fc0958579..000000000 --- a/scripts/powershell/get-feature-paths.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env pwsh -param() -$ErrorActionPreference = 'Stop' - -. "$PSScriptRoot/common.ps1" - -$paths = Get-FeaturePathsEnv -if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 } - -Write-Output "REPO_ROOT: $($paths.REPO_ROOT)" -Write-Output "BRANCH: $($paths.CURRENT_BRANCH)" -Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)" -Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)" -Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)" -Write-Output "TASKS: $($paths.TASKS)" diff --git a/scripts/powershell/setup-plan.ps1 b/scripts/powershell/setup-plan.ps1 index b0264405b..d0ed582fa 100644 --- a/scripts/powershell/setup-plan.ps1 +++ b/scripts/powershell/setup-plan.ps1 @@ -1,21 +1,61 @@ #!/usr/bin/env pwsh +# Setup implementation plan for a feature + [CmdletBinding()] -param([switch]$Json) +param( + [switch]$Json, + [switch]$Help +) + $ErrorActionPreference = 'Stop' + +# Show help if requested +if ($Help) { + Write-Output "Usage: ./setup-plan.ps1 [-Json] [-Help]" + Write-Output " -Json Output results in JSON format" + Write-Output " -Help Show this help message" + exit 0 +} + +# Load common functions . "$PSScriptRoot/common.ps1" +# Get all paths and variables from common functions $paths = Get-FeaturePathsEnv -if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 } +# Check if we're on a proper feature branch (only for git repos) +if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) { + exit 1 +} + +# Ensure the feature directory exists New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null -$template = Join-Path $paths.REPO_ROOT 'templates/plan-template.md' -if (Test-Path $template) { Copy-Item $template $paths.IMPL_PLAN -Force } +# Copy plan template if it exists, otherwise note it or create empty file +$template = Join-Path $paths.REPO_ROOT '.specify/templates/plan-template.md' +if (Test-Path $template) { + Copy-Item $template $paths.IMPL_PLAN -Force + Write-Output "Copied plan template to $($paths.IMPL_PLAN)" +} else { + Write-Warning "Plan template not found at $template" + # Create a basic plan file if template doesn't exist + New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null +} + +# Output results if ($Json) { - [PSCustomObject]@{ FEATURE_SPEC=$paths.FEATURE_SPEC; IMPL_PLAN=$paths.IMPL_PLAN; SPECS_DIR=$paths.FEATURE_DIR; BRANCH=$paths.CURRENT_BRANCH } | ConvertTo-Json -Compress + $result = [PSCustomObject]@{ + FEATURE_SPEC = $paths.FEATURE_SPEC + IMPL_PLAN = $paths.IMPL_PLAN + SPECS_DIR = $paths.FEATURE_DIR + BRANCH = $paths.CURRENT_BRANCH + HAS_GIT = $paths.HAS_GIT + } + $result | ConvertTo-Json -Compress } else { Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)" Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)" Write-Output "SPECS_DIR: $($paths.FEATURE_DIR)" Write-Output "BRANCH: $($paths.CURRENT_BRANCH)" + Write-Output "HAS_GIT: $($paths.HAS_GIT)" } diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 index 1e35b25a6..5204b4715 100644 --- a/scripts/powershell/update-agent-context.ps1 +++ b/scripts/powershell/update-agent-context.ps1 @@ -1,109 +1,421 @@ #!/usr/bin/env pwsh -[CmdletBinding()] -param([string]$AgentType) +<#! +.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, qwen, opencode, codex, windsurf) + +.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','qwen','opencode','codex','windsurf')] + [string]$AgentType +) + $ErrorActionPreference = 'Stop' -$repoRoot = git rev-parse --show-toplevel -$currentBranch = git rev-parse --abbrev-ref HEAD -$featureDir = Join-Path $repoRoot "specs/$currentBranch" -$newPlan = Join-Path $featureDir 'plan.md' -if (-not (Test-Path $newPlan)) { Write-Error "ERROR: No plan.md found at $newPlan"; exit 1 } +# Import common helpers +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. (Join-Path $ScriptDir 'common.ps1') -$claudeFile = Join-Path $repoRoot 'CLAUDE.md' -$geminiFile = Join-Path $repoRoot 'GEMINI.md' -$copilotFile = Join-Path $repoRoot '.github/copilot-instructions.md' -$cursorFile = Join-Path $repoRoot '.cursor/rules/specify-rules.mdc' -$qwenFile = Join-Path $repoRoot 'QWEN.md' -$agentsFile = Join-Path $repoRoot 'AGENTS.md' -$windsurfFile = Join-Path $repoRoot '.windsurf/rules/specify-rules.md' +# 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 -Write-Output "=== Updating agent context files for feature $currentBranch ===" +# 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' -function Get-PlanValue($pattern) { - if (-not (Test-Path $newPlan)) { return '' } - $line = Select-String -Path $newPlan -Pattern $pattern | Select-Object -First 1 - if ($line) { return ($line.Line -replace "^\*\*$pattern\*\*: ", '') } - return '' +$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" } -$newLang = Get-PlanValue 'Language/Version' -$newFramework = Get-PlanValue 'Primary Dependencies' -$newTesting = Get-PlanValue 'Testing' -$newDb = Get-PlanValue 'Storage' -$newProjectType = Get-PlanValue 'Project Type' - -function Initialize-AgentFile($targetFile, $agentName) { - if (Test-Path $targetFile) { return } - $template = Join-Path $repoRoot '.specify/templates/agent-file-template.md' - if (-not (Test-Path $template)) { Write-Error "Template not found: $template"; return } - $content = Get-Content $template -Raw - $content = $content.Replace('[PROJECT NAME]', (Split-Path $repoRoot -Leaf)) - $content = $content.Replace('[DATE]', (Get-Date -Format 'yyyy-MM-dd')) - $content = $content.Replace('[EXTRACTED FROM ALL PLAN.MD FILES]', "- $newLang + $newFramework ($currentBranch)") - if ($newProjectType -match 'web') { $structure = "backend/`nfrontend/`ntests/" } else { $structure = "src/`ntests/" } - $content = $content.Replace('[ACTUAL STRUCTURE FROM PLANS]', $structure) - if ($newLang -match 'Python') { $commands = 'cd src && pytest && ruff check .' } - elseif ($newLang -match 'Rust') { $commands = 'cargo test && cargo clippy' } - elseif ($newLang -match 'JavaScript|TypeScript') { $commands = 'npm test && npm run lint' } - else { $commands = "# Add commands for $newLang" } - $content = $content.Replace('[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]', $commands) - $content = $content.Replace('[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]', "${newLang}: Follow standard conventions") - $content = $content.Replace('[LAST 3 FEATURES AND WHAT THEY ADDED]', "- ${currentBranch}: Added ${newLang} + ${newFramework}") - $content | Set-Content $targetFile -Encoding UTF8 +function Write-Success { + param( + [Parameter(Mandatory=$true)] + [string]$Message + ) + Write-Host "$([char]0x2713) $Message" } -function Update-AgentFile($targetFile, $agentName) { - if (-not (Test-Path $targetFile)) { Initialize-AgentFile $targetFile $agentName; return } - $content = Get-Content $targetFile -Raw - if ($newLang -and ($content -notmatch [regex]::Escape($newLang))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newLang + $newFramework ($currentBranch)`n" } - if ($newDb -and $newDb -ne 'N/A' -and ($content -notmatch [regex]::Escape($newDb))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newDb ($currentBranch)`n" } - if ($content -match '## Recent Changes\n([\s\S]*?)(\n\n|$)') { - $changesBlock = $matches[1].Trim().Split("`n") - $changesBlock = ,"- ${currentBranch}: Added ${newLang} + ${newFramework}" + $changesBlock - $changesBlock = $changesBlock | Where-Object { $_ } | Select-Object -First 3 - $joined = ($changesBlock -join "`n") - $content = [regex]::Replace($content, '## Recent Changes\n([\s\S]*?)(\n\n|$)', "## Recent Changes`n$joined`n`n") +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 } - $content = [regex]::Replace($content, 'Last updated: \d{4}-\d{2}-\d{2}', "Last updated: $(Get-Date -Format 'yyyy-MM-dd')") - $content | Set-Content $targetFile -Encoding UTF8 - Write-Output "✓ $agentName context file updated successfully" } -switch ($AgentType) { - 'claude' { Update-AgentFile $claudeFile 'Claude Code' } - 'gemini' { Update-AgentFile $geminiFile 'Gemini CLI' } - 'copilot' { Update-AgentFile $copilotFile 'GitHub Copilot' } - 'cursor' { Update-AgentFile $cursorFile 'Cursor IDE' } - 'qwen' { Update-AgentFile $qwenFile 'Qwen Code' } - 'opencode' { Update-AgentFile $agentsFile 'opencode' } - 'windsurf' { Update-AgentFile $windsurfFile 'Windsurf' } - 'codex' { Update-AgentFile $agentsFile 'Codex CLI' } - '' { - foreach ($pair in @( - @{file=$claudeFile; name='Claude Code'}, - @{file=$geminiFile; name='Gemini CLI'}, - @{file=$copilotFile; name='GitHub Copilot'}, - @{file=$cursorFile; name='Cursor IDE'}, - @{file=$qwenFile; name='Qwen Code'}, - @{file=$agentsFile; name='opencode'}, - @{file=$windsurfFile; name='Windsurf'}, - @{file=$agentsFile; name='Codex CLI'} - )) { - if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name } +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 | ForEach-Object { + if ($_ -match $regex) { + $val = $Matches[1].Trim() + if ($val -notin @('NEEDS CLARIFICATION','N/A')) { return $val } } - if (-not (Test-Path $claudeFile) -and -not (Test-Path $geminiFile) -and -not (Test-Path $copilotFile) -and -not (Test-Path $cursorFile) -and -not (Test-Path $qwenFile) -and -not (Test-Path $agentsFile) -and -not (Test-Path $windsurfFile)) { - Write-Output 'No agent context files found. Creating Claude Code context file by default.' - Update-AgentFile $claudeFile 'Claude Code' + } | 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 + $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 + + $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 + 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)" } } - Default { Write-Error "ERROR: Unknown agent type '$AgentType'. Use: claude, gemini, copilot, cursor, qwen, opencode, windsurf, codex or leave empty for all."; exit 1 } + 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 + $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($_) } + } + + Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) + return $true } -Write-Output '' -Write-Output 'Summary of changes:' -if ($newLang) { Write-Output "- Added language: $newLang" } -if ($newFramework) { Write-Output "- Added framework: $newFramework" } -if ($newDb -and $newDb -ne 'N/A') { Write-Output "- Added database: $newDb" } +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 -Write-Output '' -Write-Output 'Usage: ./update-agent-context.ps1 [claude|gemini|copilot|cursor|qwen|opencode|windsurf|codex]' + $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' { 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' } + default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf'; return $false } + } +} + +function Update-AllExistingAgents { + $found = $false + $ok = $true + if (Test-Path $CLAUDE_FILE) { if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }; $found = $true } + if (Test-Path $GEMINI_FILE) { if (-not (Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }; $found = $true } + if (Test-Path $COPILOT_FILE) { if (-not (Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }; $found = $true } + if (Test-Path $CURSOR_FILE) { if (-not (Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE')) { $ok = $false }; $found = $true } + if (Test-Path $QWEN_FILE) { if (-not (Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }; $found = $true } + if (Test-Path $AGENTS_FILE) { if (-not (Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex/opencode')) { $ok = $false }; $found = $true } + if (Test-Path $WINDSURF_FILE) { if (-not (Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false }; $found = $true } + 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|qwen|opencode|codex|windsurf]' +} + +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 4eb36fbdd..4e1d111d6 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -28,6 +28,7 @@ import sys import zipfile import tempfile import shutil +import shlex import json from pathlib import Path from typing import Optional, Tuple @@ -983,12 +984,24 @@ def init( # Boxed "Next steps" section steps_lines = [] if not here: - steps_lines.append(f"1. [bold green]cd {project_name}[/bold green]") + steps_lines.append(f"1. Go to the project folder: [cyan]cd {project_name}[/cyan]") step_num = 2 else: steps_lines.append("1. You're already in the project directory!") step_num = 2 + # Add Codex-specific setup step if needed + if selected_ai == "codex": + codex_path = project_path / ".codex" + quoted_path = shlex.quote(str(codex_path)) + if os.name == "nt": # Windows + cmd = f"setx CODEX_HOME {quoted_path}" + else: # Unix-like systems + cmd = f"export CODEX_HOME={quoted_path}" + + steps_lines.append(f"{step_num}. Set [cyan]CODEX_HOME[/cyan] environment variable before running Codex: [cyan]{cmd}[/cyan]") + step_num += 1 + steps_lines.append(f"{step_num}. Start using slash commands with your AI agent:") steps_lines.append(" 2.1 [cyan]/constitution[/] - Establish project principles") steps_lines.append(" 2.2 [cyan]/specify[/] - Create specifications") @@ -1000,6 +1013,20 @@ def init( console.print() console.print(steps_panel) + # Add Codex warning if using Codex + if selected_ai == "codex": + warning_text = """[bold yellow]Important Note:[/bold yellow] + +Custom prompts do not yet support arguments in Codex. You may need to manually +specify additional project instructions directly in prompt files located in +[cyan].codex/prompts/[/cyan]. + +For more information, see: [cyan]https://github.com/openai/codex/issues/2890[/cyan]""" + + warning_panel = Panel(warning_text, title="Slash Commands in Codex", border_style="yellow", padding=(1,2)) + console.print() + console.print(warning_panel) + @app.command() def check(): """Check that all required tools are installed.""" diff --git a/templates/commands/constitution.md b/templates/commands/constitution.md index e1b15cd42..671d2c9ab 100644 --- a/templates/commands/constitution.md +++ b/templates/commands/constitution.md @@ -1,8 +1,13 @@ --- description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync. -# (No scripts section: constitution edits are manual authoring assisted by the agent) --- +The user input to you can be provided directly by the agent or in `$ARGUMENTS` - you **MUST** consider it before proceeding with the prompt (if not empty). + +User input: + +$ARGUMENTS + You are updating the project constitution at `/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts. Follow this execution flow: diff --git a/templates/commands/implement.md b/templates/commands/implement.md index a14bd0d4b..19e45f7fe 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -1,11 +1,15 @@ --- description: Execute the implementation plan by processing and executing all tasks defined in tasks.md scripts: - sh: scripts/bash/check-implementation-prerequisites.sh --json - ps: scripts/powershell/check-implementation-prerequisites.ps1 -Json + sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks + ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks --- -Given the current feature context, do this: +The user input to you can be provided directly by the agent or in `$ARGUMENTS` - you **MUST** consider it before proceeding with the prompt (if not empty). + +User input: + +$ARGUMENTS 1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. diff --git a/templates/commands/plan.md b/templates/commands/plan.md index 18a0b5c64..afb9c900b 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -5,6 +5,12 @@ scripts: ps: scripts/powershell/setup-plan.ps1 -Json --- +The user input to you can be provided directly by the agent or in `$ARGUMENTS` - you **MUST** consider it before proceeding with the prompt (if not empty). + +User input: + +$ARGUMENTS + Given the implementation details provided as an argument, do this: 1. Run `{SCRIPT}` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 9a6a3b361..474f7b026 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -5,6 +5,12 @@ scripts: ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}" --- +The user input to you can be provided directly by the agent or in `$ARGUMENTS` - you **MUST** consider it before proceeding with the prompt (if not empty). + +User input: + +$ARGUMENTS + The text the user typed after `/specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided an empty command. Given that feature description, do this: diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 29b4cd25e..792dd2393 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -1,11 +1,15 @@ --- description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts. scripts: - sh: scripts/bash/check-task-prerequisites.sh --json - ps: scripts/powershell/check-task-prerequisites.ps1 -Json + sh: scripts/bash/check-prerequisites.sh --json + ps: scripts/powershell/check-prerequisites.ps1 -Json --- -Given the context provided as an argument, do this: +The user input to you can be provided directly by the agent or in `$ARGUMENTS` - you **MUST** consider it before proceeding with the prompt (if not empty). + +User input: + +$ARGUMENTS 1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 2. Load and analyze available design documents: diff --git a/templates/plan-template.md b/templates/plan-template.md index cb3eca1f9..e812b4126 100644 --- a/templates/plan-template.md +++ b/templates/plan-template.md @@ -151,7 +151,8 @@ ios/ or android/ - Quickstart test = story validation steps 5. **Update agent file incrementally** (O(1) operation): - - Run `{SCRIPT}` for your AI assistant + - Run `{SCRIPT}` + **IMPORTANT**: Execute it exactly as specified above. Do not add or remove any arguments. - If exists: Add only NEW tech from current plan - Preserve manual additions between markers - Update recent changes (keep last 3)