From 45eb22f90744a2ec7c2dd4ae0215154507a7b31c Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Wed, 22 Apr 2026 18:34:12 +0500 Subject: [PATCH 01/19] fix: honor template overrides for tasks-template (#2278) - Add scripts/bash/setup-tasks.sh mirroring setup-plan.sh pattern - Add scripts/powershell/setup-tasks.ps1 mirroring setup-plan.ps1 pattern - Update tasks.md frontmatter to use dedicated setup-tasks scripts - Resolve tasks template via override stack and emit path as TASKS_TEMPLATE in JSON output - Reference resolved TASKS_TEMPLATE path in generate step instead of hardcoded path --- scripts/bash/setup-tasks.sh | 94 ++++++++++++++++++++++++++++++ scripts/powershell/setup-tasks.ps1 | 71 ++++++++++++++++++++++ templates/commands/tasks.md | 8 +-- 3 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 scripts/bash/setup-tasks.sh create mode 100644 scripts/powershell/setup-tasks.ps1 diff --git a/scripts/bash/setup-tasks.sh b/scripts/bash/setup-tasks.sh new file mode 100644 index 0000000000..2d5ef972c3 --- /dev/null +++ b/scripts/bash/setup-tasks.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -e + +# Parse command line arguments +JSON_MODE=false + +for arg in "$@"; do + case "$arg" in + --json) JSON_MODE=true ;; + --help|-h) + echo "Usage: $0 [--json]" + echo " --json Output results in JSON format" + echo " --help Show this help message" + exit 0 + ;; + *) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;; + esac +done + +# Source common functions +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get feature paths +_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +eval "$_paths_output" +unset _paths_output + +# Validate branch +check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 + +# Validate prerequisites +if [[ ! -d "$FEATURE_DIR" ]]; then + echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 + echo "Run /speckit.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 /speckit.plan first to create the implementation plan." >&2 + exit 1 +fi + +# Build available docs list +docs=() +[[ -f "$RESEARCH" ]] && docs+=("research.md") +[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") +if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then + docs+=("contracts/") +fi +[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") + +# Resolve tasks template through override stack +TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true +if [[ -z "$TASKS_TEMPLATE" ]] || [[ ! -f "$TASKS_TEMPLATE" ]]; then + echo "Error: could not resolve required tasks-template in $REPO_ROOT" >&2 + exit 1 +fi + +# Output results +if $JSON_MODE; then + if has_jq; then + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .) + fi + jq -cn \ + --arg feature_dir "$FEATURE_DIR" \ + --argjson docs "$json_docs" \ + --arg tasks_template "${TASKS_TEMPLATE:-}" \ + '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TASKS_TEMPLATE:$tasks_template}' + else + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done) + json_docs="[${json_docs%,}]" + fi + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TASKS_TEMPLATE":"%s"}\n' \ + "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "${TASKS_TEMPLATE:-}")" + fi +else + echo "FEATURE_DIR: $FEATURE_DIR" + echo "TASKS_TEMPLATE: ${TASKS_TEMPLATE:-not found}" + 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 +EOF diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 new file mode 100644 index 0000000000..0cf83bfc0d --- /dev/null +++ b/scripts/powershell/setup-tasks.ps1 @@ -0,0 +1,71 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param( + [switch]$Json, + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +if ($Help) { + Write-Output "Usage: setup-tasks.ps1 [-Json] [-Help]" + 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 +} + +# Validate prerequisites +if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) { + Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)" + Write-Output "Run /speckit.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 /speckit.plan first to create the implementation plan." + exit 1 +} + +# Build available docs list +$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' } + +# Resolve tasks template through override stack +$tasksTemplate = Resolve-Template -TemplateName 'tasks-template' -RepoRoot $paths.REPO_ROOT +if (-not $tasksTemplate) { + Write-Error "Tasks template not found in $($paths.REPO_ROOT)" + exit 1 +} + +# Output results +if ($Json) { + [PSCustomObject]@{ + FEATURE_DIR = $paths.FEATURE_DIR + AVAILABLE_DOCS = $docs + TASKS_TEMPLATE = $tasksTemplate + } | ConvertTo-Json -Compress +} else { + Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)" + Write-Output "TASKS_TEMPLATE: $(if ($tasksTemplate) { $tasksTemplate } else { 'not found' })" + 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 +} +EOF \ No newline at end of file diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 4e204abc1b..05269a4369 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -10,8 +10,8 @@ handoffs: prompt: Start the implementation in phases send: true scripts: - sh: scripts/bash/check-prerequisites.sh --json - ps: scripts/powershell/check-prerequisites.ps1 -Json + sh: scripts/bash/setup-tasks.sh --json + ps: scripts/powershell/setup-tasks.ps1 -Json --- ## User Input @@ -58,7 +58,7 @@ You **MUST** consider the user input before proceeding (if not empty). ## Outline -1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). +1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR, TASKS_TEMPLATE, and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). 2. **Load design documents**: Read from FEATURE_DIR: - **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities) @@ -76,7 +76,7 @@ You **MUST** consider the user input before proceeding (if not empty). - Create parallel execution examples per user story - Validate task completeness (each user story has all needed tasks, independently testable) -4. **Generate tasks.md**: Use `templates/tasks-template.md` as structure, fill with: +4. **Generate tasks.md**: Read the tasks template from TASKS_TEMPLATE (from the JSON output above) and use it as structure. If TASKS_TEMPLATE is empty, fall back to `.specify/templates/tasks-template.md`. Fill with: - Correct feature name from plan.md - Phase 1: Setup tasks (project initialization) - Phase 2: Foundational tasks (blocking prerequisites for all user stories) From b856a51249c5b2810f2682d89d4a438b9adc587a Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Wed, 22 Apr 2026 23:09:38 +0500 Subject: [PATCH 02/19] fix: remove stray EOF tokens from setup-tasks scripts --- scripts/bash/setup-tasks.sh | 1 - scripts/powershell/setup-tasks.ps1 | 1 - 2 files changed, 2 deletions(-) diff --git a/scripts/bash/setup-tasks.sh b/scripts/bash/setup-tasks.sh index 2d5ef972c3..58de52c047 100644 --- a/scripts/bash/setup-tasks.sh +++ b/scripts/bash/setup-tasks.sh @@ -91,4 +91,3 @@ else check_dir "$CONTRACTS_DIR" "contracts/" check_file "$QUICKSTART" "quickstart.md" fi -EOF diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index 0cf83bfc0d..505b6ab0be 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -68,4 +68,3 @@ if ($Json) { Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null } -EOF \ No newline at end of file From 41889f1a94186381755f95c5d5a9af3abc877a9a Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Wed, 22 Apr 2026 23:51:33 +0500 Subject: [PATCH 03/19] fix: improve error messages for unresolved tasks-template --- scripts/bash/setup-tasks.sh | 3 ++- scripts/powershell/setup-tasks.ps1 | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/bash/setup-tasks.sh b/scripts/bash/setup-tasks.sh index 58de52c047..d0d92c1d0f 100644 --- a/scripts/bash/setup-tasks.sh +++ b/scripts/bash/setup-tasks.sh @@ -55,7 +55,8 @@ fi # Resolve tasks template through override stack TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true if [[ -z "$TASKS_TEMPLATE" ]] || [[ ! -f "$TASKS_TEMPLATE" ]]; then - echo "Error: could not resolve required tasks-template in $REPO_ROOT" >&2 + echo "ERROR: Could not resolve required tasks-template in $REPO_ROOT" >&2 + echo "Expected shared core template at .specify/templates/tasks-template.md; run 'specify init' or reinstall shared infra to restore it." >&2 exit 1 fi diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index 505b6ab0be..ce2ace9883 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -48,7 +48,8 @@ if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } # Resolve tasks template through override stack $tasksTemplate = Resolve-Template -TemplateName 'tasks-template' -RepoRoot $paths.REPO_ROOT if (-not $tasksTemplate) { - Write-Error "Tasks template not found in $($paths.REPO_ROOT)" + $expectedCoreTemplate = Join-Path $paths.REPO_ROOT '.specify/templates/tasks-template.md' + Write-Error "Tasks template not found for repository root: $($paths.REPO_ROOT)`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, restore the shared templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists, or add a valid tasks-template override." exit 1 } From 6685b92fd59a9e43ca53c044a19b87bae5a630ea Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Thu, 23 Apr 2026 13:33:27 +0500 Subject: [PATCH 04/19] test: update file inventory tests to include setup-tasks scripts --- tests/integrations/test_integration_base_markdown.py | 4 ++-- tests/integrations/test_integration_base_skills.py | 2 ++ tests/integrations/test_integration_base_toml.py | 2 ++ tests/integrations/test_integration_base_yaml.py | 2 ++ tests/integrations/test_integration_copilot.py | 2 ++ tests/integrations/test_integration_generic.py | 2 ++ 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index 82d7b8cfb3..0b74a6f1a9 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -274,11 +274,11 @@ def _expected_files(self, script_variant: str) -> list[str]: if script_variant == "sh": for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.sh", - "setup-plan.sh"]: + "setup-plan.sh", "setup-tasks.sh"]: files.append(f".specify/scripts/bash/{name}") else: for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1", - "setup-plan.ps1"]: + "setup-plan.ps1", "setup-tasks.ps1"]: files.append(f".specify/scripts/powershell/{name}") for name in ["checklist-template.md", diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 98a65fcff4..89140de1c3 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -387,6 +387,7 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", ".specify/scripts/bash/setup-plan.sh", + ".specify/scripts/bash/setup-tasks.sh", ] else: files += [ @@ -394,6 +395,7 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/scripts/powershell/common.ps1", ".specify/scripts/powershell/create-new-feature.ps1", ".specify/scripts/powershell/setup-plan.ps1", + ".specify/scripts/powershell/setup-tasks.ps1", ] # Templates files += [ diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 78273b560e..56862e534c 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -516,6 +516,7 @@ def _expected_files(self, script_variant: str) -> list[str]: "common.sh", "create-new-feature.sh", "setup-plan.sh", + "setup-tasks.sh", ]: files.append(f".specify/scripts/bash/{name}") else: @@ -524,6 +525,7 @@ def _expected_files(self, script_variant: str) -> list[str]: "common.ps1", "create-new-feature.ps1", "setup-plan.ps1", + "setup-tasks.ps1", ]: files.append(f".specify/scripts/powershell/{name}") diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index e1dee3bad7..956c7a796f 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -395,6 +395,7 @@ def _expected_files(self, script_variant: str) -> list[str]: "common.sh", "create-new-feature.sh", "setup-plan.sh", + "setup-tasks.sh", ]: files.append(f".specify/scripts/bash/{name}") else: @@ -403,6 +404,7 @@ def _expected_files(self, script_variant: str) -> list[str]: "common.ps1", "create-new-feature.ps1", "setup-plan.ps1", + "setup-tasks.ps1", ]: files.append(f".specify/scripts/powershell/{name}") diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index 2df4d2d7df..c5ee11bba7 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -206,6 +206,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", ".specify/scripts/bash/setup-plan.sh", + ".specify/scripts/bash/setup-tasks.sh", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -265,6 +266,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".specify/scripts/powershell/common.ps1", ".specify/scripts/powershell/create-new-feature.ps1", ".specify/scripts/powershell/setup-plan.ps1", + ".specify/scripts/powershell/setup-tasks.ps1", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index f0272afa8d..290a36419e 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -264,6 +264,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", ".specify/scripts/bash/setup-plan.sh", + ".specify/scripts/bash/setup-tasks.sh", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", @@ -319,6 +320,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".specify/scripts/powershell/common.ps1", ".specify/scripts/powershell/create-new-feature.ps1", ".specify/scripts/powershell/setup-plan.ps1", + ".specify/scripts/powershell/setup-tasks.ps1", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", From 759cc477fc4a5f3ba8cbf3f89765f8736be2d67b Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Thu, 23 Apr 2026 18:39:05 +0500 Subject: [PATCH 05/19] fix: use Console::Error.WriteLine instead of Write-Error in setup-tasks.ps1 --- scripts/powershell/setup-tasks.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index ce2ace9883..84da620ce8 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -49,7 +49,7 @@ if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } $tasksTemplate = Resolve-Template -TemplateName 'tasks-template' -RepoRoot $paths.REPO_ROOT if (-not $tasksTemplate) { $expectedCoreTemplate = Join-Path $paths.REPO_ROOT '.specify/templates/tasks-template.md' - Write-Error "Tasks template not found for repository root: $($paths.REPO_ROOT)`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, restore the shared templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists, or add a valid tasks-template override." + [Console]::Error.WriteLine("Tasks template not found for repository root: $($paths.REPO_ROOT)`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, restore the shared templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists, or add a valid tasks-template override.") exit 1 } From 8a525831744aae6e7b62060769700e23c8ee4104 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Thu, 23 Apr 2026 23:33:32 +0500 Subject: [PATCH 06/19] fix: write prerequisite error messages to stderr in setup-tasks.ps1 --- scripts/powershell/setup-tasks.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index 84da620ce8..c6e8626ac4 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -25,14 +25,14 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GI # Validate prerequisites if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) { - Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)" - Write-Output "Run /speckit.specify first to create the feature structure." + [Console]::Error.WriteLine("ERROR: Feature directory not found: $($paths.FEATURE_DIR)") + [Console]::Error.WriteLine("Run /speckit.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 /speckit.plan first to create the implementation plan." + [Console]::Error.WriteLine("ERROR: plan.md not found in $($paths.FEATURE_DIR)") + [Console]::Error.WriteLine("Run /speckit.plan first to create the implementation plan.") exit 1 } From ab511bf32e6560814af97fce00f9b14909783328 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Fri, 24 Apr 2026 18:26:11 +0500 Subject: [PATCH 07/19] fix: validate tasks template is a file and normalize path in setup-tasks.ps1 --- scripts/powershell/setup-tasks.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index c6e8626ac4..2a50447ce5 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -47,11 +47,12 @@ if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } # Resolve tasks template through override stack $tasksTemplate = Resolve-Template -TemplateName 'tasks-template' -RepoRoot $paths.REPO_ROOT -if (-not $tasksTemplate) { +if (-not $tasksTemplate -or -not (Test-Path -LiteralPath $tasksTemplate -PathType Leaf)) { $expectedCoreTemplate = Join-Path $paths.REPO_ROOT '.specify/templates/tasks-template.md' [Console]::Error.WriteLine("Tasks template not found for repository root: $($paths.REPO_ROOT)`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, restore the shared templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists, or add a valid tasks-template override.") exit 1 } +$tasksTemplate = (Resolve-Path -LiteralPath $tasksTemplate).Path # Output results if ($Json) { From d14439b8926b81af28e67b6ce43be9e9e7bb37f0 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Sat, 25 Apr 2026 01:39:56 +0500 Subject: [PATCH 08/19] fix: improve tasks-template error message to mention full override stack --- scripts/bash/setup-tasks.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/bash/setup-tasks.sh b/scripts/bash/setup-tasks.sh index d0d92c1d0f..8e0ad41fa3 100644 --- a/scripts/bash/setup-tasks.sh +++ b/scripts/bash/setup-tasks.sh @@ -55,8 +55,8 @@ fi # Resolve tasks template through override stack TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true if [[ -z "$TASKS_TEMPLATE" ]] || [[ ! -f "$TASKS_TEMPLATE" ]]; then - echo "ERROR: Could not resolve required tasks-template in $REPO_ROOT" >&2 - echo "Expected shared core template at .specify/templates/tasks-template.md; run 'specify init' or reinstall shared infra to restore it." >&2 + echo "ERROR: Could not resolve required tasks-template from the template override stack for $REPO_ROOT" >&2 + echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .specify/templates/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the shared core template." >&2 exit 1 fi From 5e7723ba486cbdc3bdac3e6515fa869caec32ef6 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Sat, 25 Apr 2026 02:56:38 +0500 Subject: [PATCH 09/19] test: add setup-tasks.sh to TestCopilotSkillsMode file inventory --- tests/integrations/test_integration_copilot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index c5ee11bba7..c6e9259b09 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -616,6 +616,7 @@ def test_complete_file_inventory_skills_sh(self, tmp_path): ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", ".specify/scripts/bash/setup-plan.sh", + ".specify/scripts/bash/setup-tasks.sh", # Templates ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", From 2b56c7d7130e26dd02928aa6dab76a288dffd2a3 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Sat, 25 Apr 2026 03:17:55 +0500 Subject: [PATCH 10/19] fix: skip feature-branch validation when feature.json pins FEATURE_DIR --- scripts/bash/setup-tasks.sh | 5 ++++- scripts/powershell/setup-tasks.ps1 | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/bash/setup-tasks.sh b/scripts/bash/setup-tasks.sh index 8e0ad41fa3..96f83601f4 100644 --- a/scripts/bash/setup-tasks.sh +++ b/scripts/bash/setup-tasks.sh @@ -28,7 +28,10 @@ eval "$_paths_output" unset _paths_output # Validate branch -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 +# If feature.json pins an existing feature directory, branch naming is not required. +if ! feature_json_matches_feature_dir "$REPO_ROOT" "$FEATURE_DIR"; then + check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 +fi # Validate prerequisites if [[ ! -d "$FEATURE_DIR" ]]; then diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index 2a50447ce5..f7c0091d84 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -19,8 +19,11 @@ if ($Help) { # Get feature paths and validate branch $paths = Get-FeaturePathsEnv -if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GIT)) { - exit 1 +# If feature.json pins an existing feature directory, branch naming is not required. +if (-not (Test-FeatureJsonMatchesFeatureDir -RepoRoot $paths.REPO_ROOT -ActiveFeatureDir $paths.FEATURE_DIR)) { + if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) { + exit 1 + } } # Validate prerequisites From 860363bc519dc5be824eeed1c30566a88dc74930 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Mon, 27 Apr 2026 23:02:36 +0500 Subject: [PATCH 11/19] fix: correct override path in tasks-template error messages --- scripts/bash/setup-tasks.sh | 2 +- scripts/powershell/setup-tasks.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/bash/setup-tasks.sh b/scripts/bash/setup-tasks.sh index 96f83601f4..c6830dec2b 100644 --- a/scripts/bash/setup-tasks.sh +++ b/scripts/bash/setup-tasks.sh @@ -59,7 +59,7 @@ fi TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true if [[ -z "$TASKS_TEMPLATE" ]] || [[ ! -f "$TASKS_TEMPLATE" ]]; then echo "ERROR: Could not resolve required tasks-template from the template override stack for $REPO_ROOT" >&2 - echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .specify/templates/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the shared core template." >&2 + echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .specify/templates/overrides/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the core .specify/templates/tasks-template.md template." >&2 exit 1 fi diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index f7c0091d84..02202fc4fa 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -52,7 +52,7 @@ if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } $tasksTemplate = Resolve-Template -TemplateName 'tasks-template' -RepoRoot $paths.REPO_ROOT if (-not $tasksTemplate -or -not (Test-Path -LiteralPath $tasksTemplate -PathType Leaf)) { $expectedCoreTemplate = Join-Path $paths.REPO_ROOT '.specify/templates/tasks-template.md' - [Console]::Error.WriteLine("Tasks template not found for repository root: $($paths.REPO_ROOT)`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, restore the shared templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists, or add a valid tasks-template override.") + [Console]::Error.WriteLine("Tasks template not found for repository root: $($paths.REPO_ROOT)`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, restore the shared templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists, or add an override at '.specify/templates/overrides/tasks-template.md'.") exit 1 } $tasksTemplate = (Resolve-Path -LiteralPath $tasksTemplate).Path From f1b733a023f22ceb90da241a5fab12adbcd19bcb Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Thu, 30 Apr 2026 22:41:34 +0500 Subject: [PATCH 12/19] test: add integration tests for setup-tasks template resolution and branch validation --- tests/test_setup_tasks.py | 441 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 tests/test_setup_tasks.py diff --git a/tests/test_setup_tasks.py b/tests/test_setup_tasks.py new file mode 100644 index 0000000000..785ed64d07 --- /dev/null +++ b/tests/test_setup_tasks.py @@ -0,0 +1,441 @@ +"""Tests for setup-tasks.{sh,ps1} template resolution and branch validation.""" + +import json +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + +from tests.conftest import requires_bash + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +COMMON_SH = PROJECT_ROOT / "scripts" / "bash" / "common.sh" +SETUP_TASKS_SH = PROJECT_ROOT / "scripts" / "bash" / "setup-tasks.sh" +COMMON_PS = PROJECT_ROOT / "scripts" / "powershell" / "common.ps1" +SETUP_TASKS_PS = PROJECT_ROOT / "scripts" / "powershell" / "setup-tasks.ps1" +TASKS_TEMPLATE = PROJECT_ROOT / "templates" / "tasks-template.md" + +HAS_PWSH = shutil.which("pwsh") is not None +_POWERSHELL = shutil.which("powershell.exe") or shutil.which("powershell") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _install_bash_scripts(repo: Path) -> None: + d = repo / ".specify" / "scripts" / "bash" + d.mkdir(parents=True, exist_ok=True) + shutil.copy(COMMON_SH, d / "common.sh") + shutil.copy(SETUP_TASKS_SH, d / "setup-tasks.sh") + + +def _install_ps_scripts(repo: Path) -> None: + d = repo / ".specify" / "scripts" / "powershell" + d.mkdir(parents=True, exist_ok=True) + shutil.copy(COMMON_PS, d / "common.ps1") + shutil.copy(SETUP_TASKS_PS, d / "setup-tasks.ps1") + + +def _install_core_tasks_template(repo: Path) -> None: + """Copy the real tasks-template.md into the core template location.""" + tdir = repo / ".specify" / "templates" + tdir.mkdir(parents=True, exist_ok=True) + shutil.copy(TASKS_TEMPLATE, tdir / "tasks-template.md") + + +def _minimal_feature(repo: Path) -> Path: + """ + Create a numbered branch-style feature directory with spec.md and plan.md + so all prerequisite checks in setup-tasks pass. + Returns the feature directory path. + """ + feat = repo / "specs" / "001-my-feature" + feat.mkdir(parents=True, exist_ok=True) + (feat / "spec.md").write_text("# spec\n", encoding="utf-8") + (feat / "plan.md").write_text("# plan\n", encoding="utf-8") + return feat + + +def _clean_env() -> dict[str, str]: + """ + Return os.environ with all SPECIFY_* variables stripped so the scripts + rely purely on git branch + feature.json state set up by each fixture. + """ + env = os.environ.copy() + for key in list(env): + if key.startswith("SPECIFY_"): + env.pop(key) + return env + + +def _git_init(repo: Path) -> None: + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run( + ["git", "config", "user.email", "test@example.com"], cwd=repo, check=True + ) + subprocess.run(["git", "config", "user.name", "Test User"], cwd=repo, check=True) + subprocess.run( + ["git", "commit", "--allow-empty", "-m", "init", "-q"], cwd=repo, check=True + ) + + +# --------------------------------------------------------------------------- +# Shared fixture +# --------------------------------------------------------------------------- + +@pytest.fixture +def tasks_repo(tmp_path: Path) -> Path: + """ + A minimal repo with: + - git initialised on a numbered branch (001-my-feature) + - core tasks-template.md in place + - both bash and PowerShell scripts installed + """ + repo = tmp_path / "proj" + repo.mkdir() + _git_init(repo) + + # Switch to a numbered branch so branch validation passes without feature.json + subprocess.run( + ["git", "checkout", "-q", "-b", "001-my-feature"], + cwd=repo, + check=True, + ) + + (repo / ".specify").mkdir() + _install_core_tasks_template(repo) + _install_bash_scripts(repo) + _install_ps_scripts(repo) + return repo + + +# =========================================================================== +# BASH TESTS +# =========================================================================== + +@requires_bash +def test_setup_tasks_bash_core_template_resolved(tasks_repo: Path) -> None: + """ + When the core tasks-template.md is present and all prerequisites are met, + setup-tasks.sh --json should exit 0 and return an absolute, existing + TASKS_TEMPLATE path pointing to the core template. + """ + feat = _minimal_feature(tasks_repo) + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" + + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + data = json.loads(result.stdout) + tasks_tmpl = Path(data["TASKS_TEMPLATE"]) + assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" + assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" + assert tasks_tmpl.name == "tasks-template.md" + + +@requires_bash +def test_setup_tasks_bash_override_wins(tasks_repo: Path) -> None: + """ + When an override exists at .specify/templates/overrides/tasks-template.md, + setup-tasks.sh --json must return the override path, not the core path. + """ + feat = _minimal_feature(tasks_repo) + + # Create the override + overrides_dir = tasks_repo / ".specify" / "templates" / "overrides" + overrides_dir.mkdir(parents=True, exist_ok=True) + override_file = overrides_dir / "tasks-template.md" + override_file.write_text("# override tasks template\n", encoding="utf-8") + + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" + + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + data = json.loads(result.stdout) + tasks_tmpl = Path(data["TASKS_TEMPLATE"]) + assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" + assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" + # The resolved path must be inside the overrides directory + assert "overrides" in tasks_tmpl.parts, ( + f"Expected override path but got: {tasks_tmpl}" + ) + + +@requires_bash +def test_setup_tasks_bash_missing_template_errors(tasks_repo: Path) -> None: + """ + When tasks-template.md is absent from all locations, setup-tasks.sh must + exit non-zero and print a helpful ERROR message to stderr. + """ + feat = _minimal_feature(tasks_repo) + + # Remove the core template so no template exists anywhere + core = tasks_repo / ".specify" / "templates" / "tasks-template.md" + core.unlink() + + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" + + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode != 0 + assert "ERROR" in result.stderr + assert "tasks-template" in result.stderr + + +@requires_bash +def test_setup_tasks_bash_passes_custom_branch_when_feature_json_valid( + tasks_repo: Path, +) -> None: + """ + On a non-standard branch, setup-tasks.sh must succeed when feature.json + pins a valid FEATURE_DIR (branch validation should be skipped). + """ + subprocess.run( + ["git", "checkout", "-q", "-b", "feature/custom-branch"], + cwd=tasks_repo, + check=True, + ) + + feat = tasks_repo / "specs" / "001-my-feature" + feat.mkdir(parents=True, exist_ok=True) + (feat / "spec.md").write_text("# spec\n", encoding="utf-8") + (feat / "plan.md").write_text("# plan\n", encoding="utf-8") + + (tasks_repo / ".specify" / "feature.json").write_text( + json.dumps({"feature_directory": "specs/001-my-feature"}), + encoding="utf-8", + ) + + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" + + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + +@requires_bash +def test_setup_tasks_bash_fails_custom_branch_without_feature_json( + tasks_repo: Path, +) -> None: + """ + On a non-standard branch with no feature.json, setup-tasks.sh must fail + and report that we are not on a feature branch. + """ + subprocess.run( + ["git", "checkout", "-q", "-b", "feature/custom-branch"], + cwd=tasks_repo, + check=True, + ) + + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" + + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode != 0 + assert "Not on a feature branch" in result.stderr + + +# =========================================================================== +# POWERSHELL TESTS +# =========================================================================== + +@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") +def test_setup_tasks_ps_core_template_resolved(tasks_repo: Path) -> None: + """ + When the core tasks-template.md is present and all prerequisites are met, + setup-tasks.ps1 -Json should exit 0 and return an absolute, existing + TASKS_TEMPLATE path. + """ + feat = _minimal_feature(tasks_repo) + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" + exe = "pwsh" if HAS_PWSH else _POWERSHELL + + result = subprocess.run( + [exe, "-NoProfile", "-File", str(script), "-Json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + data = json.loads(result.stdout) + tasks_tmpl = Path(data["TASKS_TEMPLATE"]) + assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" + assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" + assert tasks_tmpl.name == "tasks-template.md" + + +@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") +def test_setup_tasks_ps_override_wins(tasks_repo: Path) -> None: + """ + When an override exists at .specify/templates/overrides/tasks-template.md, + setup-tasks.ps1 -Json must return the override path, not the core path. + """ + feat = _minimal_feature(tasks_repo) + + overrides_dir = tasks_repo / ".specify" / "templates" / "overrides" + overrides_dir.mkdir(parents=True, exist_ok=True) + override_file = overrides_dir / "tasks-template.md" + override_file.write_text("# override tasks template\n", encoding="utf-8") + + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" + exe = "pwsh" if HAS_PWSH else _POWERSHELL + + result = subprocess.run( + [exe, "-NoProfile", "-File", str(script), "-Json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + data = json.loads(result.stdout) + tasks_tmpl = Path(data["TASKS_TEMPLATE"]) + assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" + assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" + assert "overrides" in tasks_tmpl.parts, ( + f"Expected override path but got: {tasks_tmpl}" + ) + + +@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") +def test_setup_tasks_ps_missing_template_errors(tasks_repo: Path) -> None: + """ + When tasks-template.md is absent from all locations, setup-tasks.ps1 must + exit non-zero and write a helpful error to stderr. + """ + feat = _minimal_feature(tasks_repo) + + core = tasks_repo / ".specify" / "templates" / "tasks-template.md" + core.unlink() + + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" + exe = "pwsh" if HAS_PWSH else _POWERSHELL + + result = subprocess.run( + [exe, "-NoProfile", "-File", str(script), "-Json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode != 0 + assert "tasks-template" in result.stderr.lower() or "tasks-template" in result.stdout.lower() + + +@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") +def test_setup_tasks_ps_passes_custom_branch_when_feature_json_valid( + tasks_repo: Path, +) -> None: + """ + On a non-standard branch, setup-tasks.ps1 must succeed when feature.json + pins a valid FEATURE_DIR (branch validation should be skipped). + """ + subprocess.run( + ["git", "checkout", "-q", "-b", "feature/custom-branch"], + cwd=tasks_repo, + check=True, + ) + + feat = tasks_repo / "specs" / "001-my-feature" + feat.mkdir(parents=True, exist_ok=True) + (feat / "spec.md").write_text("# spec\n", encoding="utf-8") + (feat / "plan.md").write_text("# plan\n", encoding="utf-8") + + (tasks_repo / ".specify" / "feature.json").write_text( + json.dumps({"feature_directory": "specs/001-my-feature"}), + encoding="utf-8", + ) + + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" + exe = "pwsh" if HAS_PWSH else _POWERSHELL + + result = subprocess.run( + [exe, "-NoProfile", "-File", str(script), "-Json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + +@pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") +def test_setup_tasks_ps_fails_custom_branch_without_feature_json( + tasks_repo: Path, +) -> None: + """ + On a non-standard branch with no feature.json, setup-tasks.ps1 must fail + and report that we are not on a feature branch. + """ + subprocess.run( + ["git", "checkout", "-q", "-b", "feature/custom-branch"], + cwd=tasks_repo, + check=True, + ) + + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" + exe = "pwsh" if HAS_PWSH else _POWERSHELL + + result = subprocess.run( + [exe, "-NoProfile", "-File", str(script), "-Json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode != 0 + assert "Not on a feature branch" in result.stderr \ No newline at end of file From d78be91245133fbe745c1899f652c0ad55df1483 Mon Sep 17 00:00:00 2001 From: Nimra Akram Date: Fri, 1 May 2026 22:06:25 +0500 Subject: [PATCH 13/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/powershell/setup-tasks.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index 02202fc4fa..4a014d3265 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -52,7 +52,7 @@ if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' } $tasksTemplate = Resolve-Template -TemplateName 'tasks-template' -RepoRoot $paths.REPO_ROOT if (-not $tasksTemplate -or -not (Test-Path -LiteralPath $tasksTemplate -PathType Leaf)) { $expectedCoreTemplate = Join-Path $paths.REPO_ROOT '.specify/templates/tasks-template.md' - [Console]::Error.WriteLine("Tasks template not found for repository root: $($paths.REPO_ROOT)`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, restore the shared templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists, or add an override at '.specify/templates/overrides/tasks-template.md'.") + [Console]::Error.WriteLine("ERROR: Tasks template not found for repository root: $($paths.REPO_ROOT)`nTemplate resolution order: overrides -> presets -> extensions -> core.`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, verify whether 'tasks-template.md' is available in '.specify/templates/overrides/', preset templates, extension templates, or restore the shared/core templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists.") exit 1 } $tasksTemplate = (Resolve-Path -LiteralPath $tasksTemplate).Path From d26efa10b516caec28fb67c5de514ef2debb0984 Mon Sep 17 00:00:00 2001 From: Nimra Akram Date: Fri, 1 May 2026 22:16:20 +0500 Subject: [PATCH 14/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- templates/commands/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 05269a4369..e5af6793b6 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -58,7 +58,7 @@ You **MUST** consider the user input before proceeding (if not empty). ## Outline -1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR, TASKS_TEMPLATE, and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). +1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR, TASKS_TEMPLATE, and AVAILABLE_DOCS list. `FEATURE_DIR` and `TASKS_TEMPLATE` must be absolute paths when provided. `AVAILABLE_DOCS` is a list of document names/relative paths available under `FEATURE_DIR` (for example `research.md` or `contracts/`). For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). 2. **Load design documents**: Read from FEATURE_DIR: - **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities) From e606a1f9ce789ded41777ba4f4ffe30fc273e0f7 Mon Sep 17 00:00:00 2001 From: Nimra Akram Date: Fri, 1 May 2026 22:29:26 +0500 Subject: [PATCH 15/19] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/test_setup_tasks.py | 121 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/test_setup_tasks.py b/tests/test_setup_tasks.py index 785ed64d07..92f7fae205 100644 --- a/tests/test_setup_tasks.py +++ b/tests/test_setup_tasks.py @@ -181,6 +181,127 @@ def test_setup_tasks_bash_override_wins(tasks_repo: Path) -> None: ) +@requires_bash +def test_setup_tasks_bash_extension_wins_over_core(tasks_repo: Path) -> None: + """ + When an extension template exists, setup-tasks.sh --json must resolve + tasks-template.md from the extension before falling back to the core path. + """ + feat = _minimal_feature(tasks_repo) + + extension_dir = ( + tasks_repo / ".specify" / "templates" / "extensions" / "test-extension" + ) + extension_dir.mkdir(parents=True, exist_ok=True) + extension_file = extension_dir / "tasks-template.md" + extension_file.write_text("# extension tasks template\n", encoding="utf-8") + + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" + + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + data = json.loads(result.stdout) + tasks_tmpl = Path(data["TASKS_TEMPLATE"]) + assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" + assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" + assert tasks_tmpl == extension_file.resolve(), ( + f"Expected extension path but got: {tasks_tmpl}" + ) + + +@requires_bash +def test_setup_tasks_bash_preset_wins_over_extension(tasks_repo: Path) -> None: + """ + When both preset and extension templates exist, setup-tasks.sh --json must + resolve the preset path because presets outrank extensions. + """ + feat = _minimal_feature(tasks_repo) + + extension_dir = ( + tasks_repo / ".specify" / "templates" / "extensions" / "test-extension" + ) + extension_dir.mkdir(parents=True, exist_ok=True) + extension_file = extension_dir / "tasks-template.md" + extension_file.write_text("# extension tasks template\n", encoding="utf-8") + + preset_dir = tasks_repo / ".specify" / "templates" / "presets" / "test-preset" + preset_dir.mkdir(parents=True, exist_ok=True) + preset_file = preset_dir / "tasks-template.md" + preset_file.write_text("# preset tasks template\n", encoding="utf-8") + + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" + + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + data = json.loads(result.stdout) + tasks_tmpl = Path(data["TASKS_TEMPLATE"]) + assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" + assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" + assert tasks_tmpl == preset_file.resolve(), ( + f"Expected preset path but got: {tasks_tmpl}" + ) + + +@requires_bash +def test_setup_tasks_bash_preset_registry_wins_over_preset(tasks_repo: Path) -> None: + """ + When both a preset template and its .specify/presets/.../.registry copy + exist, setup-tasks.sh --json must resolve the registry path first. + """ + feat = _minimal_feature(tasks_repo) + + preset_dir = tasks_repo / ".specify" / "templates" / "presets" / "test-preset" + preset_dir.mkdir(parents=True, exist_ok=True) + preset_file = preset_dir / "tasks-template.md" + preset_file.write_text("# preset tasks template\n", encoding="utf-8") + + registry_dir = ( + tasks_repo / ".specify" / "presets" / "test-preset" / ".registry" + ) + registry_dir.mkdir(parents=True, exist_ok=True) + registry_file = registry_dir / "tasks-template.md" + registry_file.write_text("# preset registry tasks template\n", encoding="utf-8") + + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" + + result = subprocess.run( + ["bash", str(script), "--json"], + cwd=tasks_repo, + capture_output=True, + text=True, + check=False, + env=_clean_env(), + ) + + assert result.returncode == 0, result.stderr + result.stdout + + data = json.loads(result.stdout) + tasks_tmpl = Path(data["TASKS_TEMPLATE"]) + assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" + assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" + assert tasks_tmpl == registry_file.resolve(), ( + f"Expected preset registry path but got: {tasks_tmpl}" + ) + + @requires_bash def test_setup_tasks_bash_missing_template_errors(tasks_repo: Path) -> None: """ From 2b94ef76af1948e6e152a434a6f6a5469b8a5d04 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Fri, 1 May 2026 23:28:07 +0500 Subject: [PATCH 16/19] fix: correct fixture paths and add spec.md prerequisite checks --- scripts/bash/setup-tasks.sh | 13 +- scripts/powershell/setup-tasks.ps1 | 13 +- tests/test_setup_tasks.py | 269 +++++++++++++++-------------- 3 files changed, 154 insertions(+), 141 deletions(-) diff --git a/scripts/bash/setup-tasks.sh b/scripts/bash/setup-tasks.sh index c6830dec2b..3f6a40b12d 100644 --- a/scripts/bash/setup-tasks.sh +++ b/scripts/bash/setup-tasks.sh @@ -33,19 +33,18 @@ if ! feature_json_matches_feature_dir "$REPO_ROOT" "$FEATURE_DIR"; then check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 fi -# Validate prerequisites -if [[ ! -d "$FEATURE_DIR" ]]; then - echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 - echo "Run /speckit.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 /speckit.plan first to create the implementation plan." >&2 exit 1 fi +if [[ ! -f "$FEATURE_SPEC" ]]; then + echo "ERROR: spec.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit.specify first to create the feature structure." >&2 + exit 1 +fi + # Build available docs list docs=() [[ -f "$RESEARCH" ]] && docs+=("research.md") diff --git a/scripts/powershell/setup-tasks.ps1 b/scripts/powershell/setup-tasks.ps1 index 4a014d3265..e00ae7a02f 100644 --- a/scripts/powershell/setup-tasks.ps1 +++ b/scripts/powershell/setup-tasks.ps1 @@ -26,19 +26,18 @@ if (-not (Test-FeatureJsonMatchesFeatureDir -RepoRoot $paths.REPO_ROOT -ActiveFe } } -# Validate prerequisites -if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) { - [Console]::Error.WriteLine("ERROR: Feature directory not found: $($paths.FEATURE_DIR)") - [Console]::Error.WriteLine("Run /speckit.specify first to create the feature structure.") - exit 1 -} - if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) { [Console]::Error.WriteLine("ERROR: plan.md not found in $($paths.FEATURE_DIR)") [Console]::Error.WriteLine("Run /speckit.plan first to create the implementation plan.") exit 1 } +if (-not (Test-Path $paths.FEATURE_SPEC -PathType Leaf)) { + [Console]::Error.WriteLine("ERROR: spec.md not found in $($paths.FEATURE_DIR)") + [Console]::Error.WriteLine("Run /speckit.specify first to create the feature structure.") + exit 1 +} + # Build available docs list $docs = @() if (Test-Path $paths.RESEARCH) { $docs += 'research.md' } diff --git a/tests/test_setup_tasks.py b/tests/test_setup_tasks.py index 92f7fae205..f3727d7998 100644 --- a/tests/test_setup_tasks.py +++ b/tests/test_setup_tasks.py @@ -1,51 +1,51 @@ """Tests for setup-tasks.{sh,ps1} template resolution and branch validation.""" - + import json import os import shutil import subprocess from pathlib import Path - + import pytest - + from tests.conftest import requires_bash - + PROJECT_ROOT = Path(__file__).resolve().parent.parent COMMON_SH = PROJECT_ROOT / "scripts" / "bash" / "common.sh" SETUP_TASKS_SH = PROJECT_ROOT / "scripts" / "bash" / "setup-tasks.sh" COMMON_PS = PROJECT_ROOT / "scripts" / "powershell" / "common.ps1" SETUP_TASKS_PS = PROJECT_ROOT / "scripts" / "powershell" / "setup-tasks.ps1" TASKS_TEMPLATE = PROJECT_ROOT / "templates" / "tasks-template.md" - + HAS_PWSH = shutil.which("pwsh") is not None _POWERSHELL = shutil.which("powershell.exe") or shutil.which("powershell") - - + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- - + def _install_bash_scripts(repo: Path) -> None: d = repo / ".specify" / "scripts" / "bash" d.mkdir(parents=True, exist_ok=True) shutil.copy(COMMON_SH, d / "common.sh") shutil.copy(SETUP_TASKS_SH, d / "setup-tasks.sh") - - + + def _install_ps_scripts(repo: Path) -> None: d = repo / ".specify" / "scripts" / "powershell" d.mkdir(parents=True, exist_ok=True) shutil.copy(COMMON_PS, d / "common.ps1") shutil.copy(SETUP_TASKS_PS, d / "setup-tasks.ps1") - - + + def _install_core_tasks_template(repo: Path) -> None: """Copy the real tasks-template.md into the core template location.""" tdir = repo / ".specify" / "templates" tdir.mkdir(parents=True, exist_ok=True) shutil.copy(TASKS_TEMPLATE, tdir / "tasks-template.md") - - + + def _minimal_feature(repo: Path) -> Path: """ Create a numbered branch-style feature directory with spec.md and plan.md @@ -57,8 +57,8 @@ def _minimal_feature(repo: Path) -> Path: (feat / "spec.md").write_text("# spec\n", encoding="utf-8") (feat / "plan.md").write_text("# plan\n", encoding="utf-8") return feat - - + + def _clean_env() -> dict[str, str]: """ Return os.environ with all SPECIFY_* variables stripped so the scripts @@ -69,8 +69,8 @@ def _clean_env() -> dict[str, str]: if key.startswith("SPECIFY_"): env.pop(key) return env - - + + def _git_init(repo: Path) -> None: subprocess.run(["git", "init", "-q"], cwd=repo, check=True) subprocess.run( @@ -80,12 +80,12 @@ def _git_init(repo: Path) -> None: subprocess.run( ["git", "commit", "--allow-empty", "-m", "init", "-q"], cwd=repo, check=True ) - - + + # --------------------------------------------------------------------------- # Shared fixture # --------------------------------------------------------------------------- - + @pytest.fixture def tasks_repo(tmp_path: Path) -> Path: """ @@ -97,25 +97,25 @@ def tasks_repo(tmp_path: Path) -> Path: repo = tmp_path / "proj" repo.mkdir() _git_init(repo) - + # Switch to a numbered branch so branch validation passes without feature.json subprocess.run( ["git", "checkout", "-q", "-b", "001-my-feature"], cwd=repo, check=True, ) - + (repo / ".specify").mkdir() _install_core_tasks_template(repo) _install_bash_scripts(repo) _install_ps_scripts(repo) return repo - - + + # =========================================================================== # BASH TESTS # =========================================================================== - + @requires_bash def test_setup_tasks_bash_core_template_resolved(tasks_repo: Path) -> None: """ @@ -125,7 +125,7 @@ def test_setup_tasks_bash_core_template_resolved(tasks_repo: Path) -> None: """ feat = _minimal_feature(tasks_repo) script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" - + result = subprocess.run( ["bash", str(script), "--json"], cwd=tasks_repo, @@ -134,16 +134,16 @@ def test_setup_tasks_bash_core_template_resolved(tasks_repo: Path) -> None: check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - + data = json.loads(result.stdout) tasks_tmpl = Path(data["TASKS_TEMPLATE"]) assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" assert tasks_tmpl.name == "tasks-template.md" - - + + @requires_bash def test_setup_tasks_bash_override_wins(tasks_repo: Path) -> None: """ @@ -151,15 +151,15 @@ def test_setup_tasks_bash_override_wins(tasks_repo: Path) -> None: setup-tasks.sh --json must return the override path, not the core path. """ feat = _minimal_feature(tasks_repo) - + # Create the override overrides_dir = tasks_repo / ".specify" / "templates" / "overrides" overrides_dir.mkdir(parents=True, exist_ok=True) override_file = overrides_dir / "tasks-template.md" override_file.write_text("# override tasks template\n", encoding="utf-8") - + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" - + result = subprocess.run( ["bash", str(script), "--json"], cwd=tasks_repo, @@ -168,9 +168,9 @@ def test_setup_tasks_bash_override_wins(tasks_repo: Path) -> None: check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - + data = json.loads(result.stdout) tasks_tmpl = Path(data["TASKS_TEMPLATE"]) assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" @@ -179,8 +179,8 @@ def test_setup_tasks_bash_override_wins(tasks_repo: Path) -> None: assert "overrides" in tasks_tmpl.parts, ( f"Expected override path but got: {tasks_tmpl}" ) - - + + @requires_bash def test_setup_tasks_bash_extension_wins_over_core(tasks_repo: Path) -> None: """ @@ -188,16 +188,17 @@ def test_setup_tasks_bash_extension_wins_over_core(tasks_repo: Path) -> None: tasks-template.md from the extension before falling back to the core path. """ feat = _minimal_feature(tasks_repo) - + + # FIX: real extension layout is .specify/extensions//templates/.md extension_dir = ( - tasks_repo / ".specify" / "templates" / "extensions" / "test-extension" + tasks_repo / ".specify" / "extensions" / "test-extension" / "templates" ) extension_dir.mkdir(parents=True, exist_ok=True) extension_file = extension_dir / "tasks-template.md" extension_file.write_text("# extension tasks template\n", encoding="utf-8") - + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" - + result = subprocess.run( ["bash", str(script), "--json"], cwd=tasks_repo, @@ -206,9 +207,9 @@ def test_setup_tasks_bash_extension_wins_over_core(tasks_repo: Path) -> None: check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - + data = json.loads(result.stdout) tasks_tmpl = Path(data["TASKS_TEMPLATE"]) assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" @@ -216,8 +217,8 @@ def test_setup_tasks_bash_extension_wins_over_core(tasks_repo: Path) -> None: assert tasks_tmpl == extension_file.resolve(), ( f"Expected extension path but got: {tasks_tmpl}" ) - - + + @requires_bash def test_setup_tasks_bash_preset_wins_over_extension(tasks_repo: Path) -> None: """ @@ -225,21 +226,23 @@ def test_setup_tasks_bash_preset_wins_over_extension(tasks_repo: Path) -> None: resolve the preset path because presets outrank extensions. """ feat = _minimal_feature(tasks_repo) - + + # FIX: real extension layout is .specify/extensions//templates/.md extension_dir = ( - tasks_repo / ".specify" / "templates" / "extensions" / "test-extension" + tasks_repo / ".specify" / "extensions" / "test-extension" / "templates" ) extension_dir.mkdir(parents=True, exist_ok=True) extension_file = extension_dir / "tasks-template.md" extension_file.write_text("# extension tasks template\n", encoding="utf-8") - - preset_dir = tasks_repo / ".specify" / "templates" / "presets" / "test-preset" + + # FIX: real preset layout is .specify/presets//templates/.md + preset_dir = tasks_repo / ".specify" / "presets" / "test-preset" / "templates" preset_dir.mkdir(parents=True, exist_ok=True) preset_file = preset_dir / "tasks-template.md" preset_file.write_text("# preset tasks template\n", encoding="utf-8") - + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" - + result = subprocess.run( ["bash", str(script), "--json"], cwd=tasks_repo, @@ -248,9 +251,9 @@ def test_setup_tasks_bash_preset_wins_over_extension(tasks_repo: Path) -> None: check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - + data = json.loads(result.stdout) tasks_tmpl = Path(data["TASKS_TEMPLATE"]) assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" @@ -258,30 +261,41 @@ def test_setup_tasks_bash_preset_wins_over_extension(tasks_repo: Path) -> None: assert tasks_tmpl == preset_file.resolve(), ( f"Expected preset path but got: {tasks_tmpl}" ) - - + + @requires_bash -def test_setup_tasks_bash_preset_registry_wins_over_preset(tasks_repo: Path) -> None: +def test_setup_tasks_bash_preset_priority_order(tasks_repo: Path) -> None: """ - When both a preset template and its .specify/presets/.../.registry copy - exist, setup-tasks.sh --json must resolve the registry path first. + When two presets both provide tasks-template.md, the one listed first in + .specify/presets/.registry wins. """ feat = _minimal_feature(tasks_repo) - - preset_dir = tasks_repo / ".specify" / "templates" / "presets" / "test-preset" - preset_dir.mkdir(parents=True, exist_ok=True) - preset_file = preset_dir / "tasks-template.md" - preset_file.write_text("# preset tasks template\n", encoding="utf-8") - - registry_dir = ( - tasks_repo / ".specify" / "presets" / "test-preset" / ".registry" - ) - registry_dir.mkdir(parents=True, exist_ok=True) - registry_file = registry_dir / "tasks-template.md" - registry_file.write_text("# preset registry tasks template\n", encoding="utf-8") - + + # FIX: resolve_template uses .specify/presets/.registry (JSON list) to order + # presets, then reads templates from .specify/presets//templates/.md. + # Create two presets; aaa-preset is listed first so it should win. + high_priority_dir = ( + tasks_repo / ".specify" / "presets" / "aaa-preset" / "templates" + ) + high_priority_dir.mkdir(parents=True, exist_ok=True) + high_priority_file = high_priority_dir / "tasks-template.md" + high_priority_file.write_text("# high priority preset tasks template\n", encoding="utf-8") + + low_priority_dir = ( + tasks_repo / ".specify" / "presets" / "test-preset" / "templates" + ) + low_priority_dir.mkdir(parents=True, exist_ok=True) + low_priority_file = low_priority_dir / "tasks-template.md" + low_priority_file.write_text("# low priority preset tasks template\n", encoding="utf-8") + + # Write .registry JSON to declare preset order: aaa-preset first + registry_json = tasks_repo / ".specify" / "presets" / ".registry" + registry_json.write_text( + json.dumps(["aaa-preset", "test-preset"]), encoding="utf-8" + ) + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" - + result = subprocess.run( ["bash", str(script), "--json"], cwd=tasks_repo, @@ -290,18 +304,18 @@ def test_setup_tasks_bash_preset_registry_wins_over_preset(tasks_repo: Path) -> check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - + data = json.loads(result.stdout) tasks_tmpl = Path(data["TASKS_TEMPLATE"]) assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" - assert tasks_tmpl == registry_file.resolve(), ( - f"Expected preset registry path but got: {tasks_tmpl}" + assert tasks_tmpl == high_priority_file.resolve(), ( + f"Expected high-priority preset path but got: {tasks_tmpl}" ) - - + + @requires_bash def test_setup_tasks_bash_missing_template_errors(tasks_repo: Path) -> None: """ @@ -309,13 +323,13 @@ def test_setup_tasks_bash_missing_template_errors(tasks_repo: Path) -> None: exit non-zero and print a helpful ERROR message to stderr. """ feat = _minimal_feature(tasks_repo) - + # Remove the core template so no template exists anywhere core = tasks_repo / ".specify" / "templates" / "tasks-template.md" core.unlink() - + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" - + result = subprocess.run( ["bash", str(script), "--json"], cwd=tasks_repo, @@ -324,12 +338,12 @@ def test_setup_tasks_bash_missing_template_errors(tasks_repo: Path) -> None: check=False, env=_clean_env(), ) - + assert result.returncode != 0 assert "ERROR" in result.stderr assert "tasks-template" in result.stderr - - + + @requires_bash def test_setup_tasks_bash_passes_custom_branch_when_feature_json_valid( tasks_repo: Path, @@ -343,19 +357,19 @@ def test_setup_tasks_bash_passes_custom_branch_when_feature_json_valid( cwd=tasks_repo, check=True, ) - + feat = tasks_repo / "specs" / "001-my-feature" feat.mkdir(parents=True, exist_ok=True) (feat / "spec.md").write_text("# spec\n", encoding="utf-8") (feat / "plan.md").write_text("# plan\n", encoding="utf-8") - + (tasks_repo / ".specify" / "feature.json").write_text( json.dumps({"feature_directory": "specs/001-my-feature"}), encoding="utf-8", ) - + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" - + result = subprocess.run( ["bash", str(script), "--json"], cwd=tasks_repo, @@ -364,10 +378,10 @@ def test_setup_tasks_bash_passes_custom_branch_when_feature_json_valid( check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - - + + @requires_bash def test_setup_tasks_bash_fails_custom_branch_without_feature_json( tasks_repo: Path, @@ -381,9 +395,9 @@ def test_setup_tasks_bash_fails_custom_branch_without_feature_json( cwd=tasks_repo, check=True, ) - + script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" - + result = subprocess.run( ["bash", str(script), "--json"], cwd=tasks_repo, @@ -392,15 +406,15 @@ def test_setup_tasks_bash_fails_custom_branch_without_feature_json( check=False, env=_clean_env(), ) - + assert result.returncode != 0 assert "Not on a feature branch" in result.stderr - - + + # =========================================================================== # POWERSHELL TESTS # =========================================================================== - + @pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") def test_setup_tasks_ps_core_template_resolved(tasks_repo: Path) -> None: """ @@ -411,7 +425,7 @@ def test_setup_tasks_ps_core_template_resolved(tasks_repo: Path) -> None: feat = _minimal_feature(tasks_repo) script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" exe = "pwsh" if HAS_PWSH else _POWERSHELL - + result = subprocess.run( [exe, "-NoProfile", "-File", str(script), "-Json"], cwd=tasks_repo, @@ -420,16 +434,16 @@ def test_setup_tasks_ps_core_template_resolved(tasks_repo: Path) -> None: check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - + data = json.loads(result.stdout) tasks_tmpl = Path(data["TASKS_TEMPLATE"]) assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" assert tasks_tmpl.is_file(), "TASKS_TEMPLATE must point to an existing file" assert tasks_tmpl.name == "tasks-template.md" - - + + @pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") def test_setup_tasks_ps_override_wins(tasks_repo: Path) -> None: """ @@ -437,15 +451,15 @@ def test_setup_tasks_ps_override_wins(tasks_repo: Path) -> None: setup-tasks.ps1 -Json must return the override path, not the core path. """ feat = _minimal_feature(tasks_repo) - + overrides_dir = tasks_repo / ".specify" / "templates" / "overrides" overrides_dir.mkdir(parents=True, exist_ok=True) override_file = overrides_dir / "tasks-template.md" override_file.write_text("# override tasks template\n", encoding="utf-8") - + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" exe = "pwsh" if HAS_PWSH else _POWERSHELL - + result = subprocess.run( [exe, "-NoProfile", "-File", str(script), "-Json"], cwd=tasks_repo, @@ -454,9 +468,9 @@ def test_setup_tasks_ps_override_wins(tasks_repo: Path) -> None: check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - + data = json.loads(result.stdout) tasks_tmpl = Path(data["TASKS_TEMPLATE"]) assert tasks_tmpl.is_absolute(), "TASKS_TEMPLATE must be an absolute path" @@ -464,8 +478,8 @@ def test_setup_tasks_ps_override_wins(tasks_repo: Path) -> None: assert "overrides" in tasks_tmpl.parts, ( f"Expected override path but got: {tasks_tmpl}" ) - - + + @pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") def test_setup_tasks_ps_missing_template_errors(tasks_repo: Path) -> None: """ @@ -473,13 +487,13 @@ def test_setup_tasks_ps_missing_template_errors(tasks_repo: Path) -> None: exit non-zero and write a helpful error to stderr. """ feat = _minimal_feature(tasks_repo) - + core = tasks_repo / ".specify" / "templates" / "tasks-template.md" core.unlink() - + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" exe = "pwsh" if HAS_PWSH else _POWERSHELL - + result = subprocess.run( [exe, "-NoProfile", "-File", str(script), "-Json"], cwd=tasks_repo, @@ -488,11 +502,11 @@ def test_setup_tasks_ps_missing_template_errors(tasks_repo: Path) -> None: check=False, env=_clean_env(), ) - + assert result.returncode != 0 assert "tasks-template" in result.stderr.lower() or "tasks-template" in result.stdout.lower() - - + + @pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") def test_setup_tasks_ps_passes_custom_branch_when_feature_json_valid( tasks_repo: Path, @@ -506,20 +520,20 @@ def test_setup_tasks_ps_passes_custom_branch_when_feature_json_valid( cwd=tasks_repo, check=True, ) - + feat = tasks_repo / "specs" / "001-my-feature" feat.mkdir(parents=True, exist_ok=True) (feat / "spec.md").write_text("# spec\n", encoding="utf-8") (feat / "plan.md").write_text("# plan\n", encoding="utf-8") - + (tasks_repo / ".specify" / "feature.json").write_text( json.dumps({"feature_directory": "specs/001-my-feature"}), encoding="utf-8", ) - + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" exe = "pwsh" if HAS_PWSH else _POWERSHELL - + result = subprocess.run( [exe, "-NoProfile", "-File", str(script), "-Json"], cwd=tasks_repo, @@ -528,10 +542,10 @@ def test_setup_tasks_ps_passes_custom_branch_when_feature_json_valid( check=False, env=_clean_env(), ) - + assert result.returncode == 0, result.stderr + result.stdout - - + + @pytest.mark.skipif(not (HAS_PWSH or _POWERSHELL), reason="no PowerShell available") def test_setup_tasks_ps_fails_custom_branch_without_feature_json( tasks_repo: Path, @@ -545,10 +559,10 @@ def test_setup_tasks_ps_fails_custom_branch_without_feature_json( cwd=tasks_repo, check=True, ) - + script = tasks_repo / ".specify" / "scripts" / "powershell" / "setup-tasks.ps1" exe = "pwsh" if HAS_PWSH else _POWERSHELL - + result = subprocess.run( [exe, "-NoProfile", "-File", str(script), "-Json"], cwd=tasks_repo, @@ -557,6 +571,7 @@ def test_setup_tasks_ps_fails_custom_branch_without_feature_json( check=False, env=_clean_env(), ) - + assert result.returncode != 0 - assert "Not on a feature branch" in result.stderr \ No newline at end of file + assert "Not on a feature branch" in result.stderr + \ No newline at end of file From 8daf1baf028402fef66d2a90e7a3ee1b8a9458a9 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Sat, 2 May 2026 00:59:04 +0500 Subject: [PATCH 17/19] fix: use correct .registry schema in preset priority test --- tests/test_setup_tasks.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test_setup_tasks.py b/tests/test_setup_tasks.py index f3727d7998..ad9af9abde 100644 --- a/tests/test_setup_tasks.py +++ b/tests/test_setup_tasks.py @@ -277,21 +277,34 @@ def test_setup_tasks_bash_preset_priority_order(tasks_repo: Path) -> None: high_priority_dir = ( tasks_repo / ".specify" / "presets" / "aaa-preset" / "templates" ) + # resolve_template reads .specify/presets/.registry as a JSON object with a + # "presets" map where each entry has a numeric "priority" (lower = higher + # precedence). Create two presets; priority-1-preset wins over priority-2-preset. + high_priority_dir = ( + tasks_repo / ".specify" / "presets" / "priority-1-preset" / "templates" + ) high_priority_dir.mkdir(parents=True, exist_ok=True) high_priority_file = high_priority_dir / "tasks-template.md" high_priority_file.write_text("# high priority preset tasks template\n", encoding="utf-8") - + low_priority_dir = ( - tasks_repo / ".specify" / "presets" / "test-preset" / "templates" + tasks_repo / ".specify" / "presets" / "priority-2-preset" / "templates" ) low_priority_dir.mkdir(parents=True, exist_ok=True) low_priority_file = low_priority_dir / "tasks-template.md" low_priority_file.write_text("# low priority preset tasks template\n", encoding="utf-8") - - # Write .registry JSON to declare preset order: aaa-preset first + + # Write .registry JSON using the correct schema: object with "presets" map, + # each preset has a numeric "priority" (lower number = higher precedence). registry_json = tasks_repo / ".specify" / "presets" / ".registry" registry_json.write_text( - json.dumps(["aaa-preset", "test-preset"]), encoding="utf-8" + json.dumps({ + "presets": { + "priority-1-preset": {"priority": 1, "enabled": True}, + "priority-2-preset": {"priority": 2, "enabled": True}, + } + }), + encoding="utf-8", ) script = tasks_repo / ".specify" / "scripts" / "bash" / "setup-tasks.sh" From 4301fdf7a3480a7f134e868dd4f0f53f233a0758 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Sat, 2 May 2026 01:48:38 +0500 Subject: [PATCH 18/19] fix: remove stale aaa-preset block and duplicate comment in preset priority test --- tests/test_setup_tasks.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_setup_tasks.py b/tests/test_setup_tasks.py index ad9af9abde..19fb17025c 100644 --- a/tests/test_setup_tasks.py +++ b/tests/test_setup_tasks.py @@ -277,12 +277,7 @@ def test_setup_tasks_bash_preset_priority_order(tasks_repo: Path) -> None: high_priority_dir = ( tasks_repo / ".specify" / "presets" / "aaa-preset" / "templates" ) - # resolve_template reads .specify/presets/.registry as a JSON object with a - # "presets" map where each entry has a numeric "priority" (lower = higher - # precedence). Create two presets; priority-1-preset wins over priority-2-preset. - high_priority_dir = ( - tasks_repo / ".specify" / "presets" / "priority-1-preset" / "templates" - ) + high_priority_dir.mkdir(parents=True, exist_ok=True) high_priority_file = high_priority_dir / "tasks-template.md" high_priority_file.write_text("# high priority preset tasks template\n", encoding="utf-8") From e257796f55284b767dbd1ee9b8ba89fe86e445b4 Mon Sep 17 00:00:00 2001 From: Nimraakram22 Date: Sat, 2 May 2026 02:02:29 +0500 Subject: [PATCH 19/19] fix: align preset directory names with registry IDs in priority test --- tests/test_setup_tasks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_setup_tasks.py b/tests/test_setup_tasks.py index 19fb17025c..f2e10d8b0f 100644 --- a/tests/test_setup_tasks.py +++ b/tests/test_setup_tasks.py @@ -271,20 +271,19 @@ def test_setup_tasks_bash_preset_priority_order(tasks_repo: Path) -> None: """ feat = _minimal_feature(tasks_repo) - # FIX: resolve_template uses .specify/presets/.registry (JSON list) to order - # presets, then reads templates from .specify/presets//templates/.md. - # Create two presets; aaa-preset is listed first so it should win. + # resolve_template reads .specify/presets/.registry as a JSON object with a + # "presets" map where each entry has a numeric "priority" (lower = higher + # precedence). Create two presets; priority-1-preset wins over priority-2-preset. high_priority_dir = ( - tasks_repo / ".specify" / "presets" / "aaa-preset" / "templates" + tasks_repo / ".specify" / "presets" / "priority-1-preset" / "templates" ) - high_priority_dir.mkdir(parents=True, exist_ok=True) high_priority_file = high_priority_dir / "tasks-template.md" high_priority_file.write_text("# high priority preset tasks template\n", encoding="utf-8") - low_priority_dir = ( tasks_repo / ".specify" / "presets" / "priority-2-preset" / "templates" ) + low_priority_dir.mkdir(parents=True, exist_ok=True) low_priority_file = low_priority_dir / "tasks-template.md" low_priority_file.write_text("# low priority preset tasks template\n", encoding="utf-8")