diff --git a/.claude/skills/speckit-specify/SKILL.md b/.claude/skills/speckit-specify/SKILL.md index 25daab1..3892b04 100644 --- a/.claude/skills/speckit-specify/SKILL.md +++ b/.claude/skills/speckit-specify/SKILL.md @@ -94,9 +94,9 @@ Given that feature description, do this: - If `branch_numbering` was used (and `feature_numbering` was absent), emit a one-line warning: "⚠️ `branch_numbering` in init-options.json is deprecated. Rename to `feature_numbering`." **Create the directory and spec file**: + - Run `.specify/scripts/bash/resolve-template.sh spec-template --json` from the repository root and parse `TEMPLATE_CONTENT` as the active `spec-template`. The shared resolver composes project overrides, preset layers, and extension layers over the core template, including `append`, `prepend`, and `wrap` strategies. If it fails, stop and report the resolution error; do not continue with only one contributing template layer. - `mkdir -p SPECIFY_FEATURE_DIRECTORY` - - Resolve the active `spec-template` through the Spec Kit preset/template resolution stack (equivalent to `specify preset resolve spec-template`) - - Copy the resolved `spec-template` file to `SPECIFY_FEATURE_DIRECTORY/spec.md` as the starting point + - Write `TEMPLATE_CONTENT` to `SPECIFY_FEATURE_DIRECTORY/spec.md` as the starting point. Do not copy a template file from disk instead: when a preset composes `spec-template`, no single file holds the full template. - Set `SPEC_FILE` to `SPECIFY_FEATURE_DIRECTORY/spec.md` - Persist the resolved path to `.specify/feature.json`: ```json @@ -112,7 +112,7 @@ Given that feature description, do this: - The spec directory name and the git branch name are independent — they may be the same but that is the user's choice - The spec directory and file are always created by this command, never by the hook -4. Load the resolved active `spec-template` file to understand required sections. +4. Use `TEMPLATE_CONTENT` to understand required sections. 5. **IF EXISTS**: Load `.specify/memory/constitution.md` for project principles and governance constraints. diff --git a/.claude/skills/speckit-taskstoissues/SKILL.md b/.claude/skills/speckit-taskstoissues/SKILL.md index b9db374..c0a6a9c 100644 --- a/.claude/skills/speckit-taskstoissues/SKILL.md +++ b/.claude/skills/speckit-taskstoissues/SKILL.md @@ -1,6 +1,6 @@ --- name: "speckit-taskstoissues" -description: "Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts." +description: "Deprecated: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts." argument-hint: "Optional filter or label for GitHub issues" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: @@ -11,6 +11,14 @@ disable-model-invocation: false --- +## Deprecation Notice + +You **MUST display the following concise warning first**, as the first step of this command and before the User Input and Pre-Execution Checks sections below: + +> ⚠️ `/speckit-taskstoissues` is deprecated and will be removed in a future minor release. Use the bundled `github` extension instead: run `specify extension add github`, then invoke `/speckit-github-taskstoissues`. + +Continue with the existing workflow unchanged. Do not install or enable the extension automatically, do not stop, and do not run the replacement command. + ## User Input ```text diff --git a/.specify/.gitignore b/.specify/.gitignore index e314fc2..bcd485e 100644 --- a/.specify/.gitignore +++ b/.specify/.gitignore @@ -7,3 +7,8 @@ feature.json # Per-machine extension config overrides. extensions/*/local-config.yml + +# Executable adapter packages and their per-checkout provenance registry. +# Execution consent is stored separately in the user's local trust registry. +integrations/packages/ +integrations/packages.json diff --git a/.specify/init-options.json b/.specify/init-options.json index e9d2d61..0f6f2fa 100644 --- a/.specify/init-options.json +++ b/.specify/init-options.json @@ -1,9 +1,10 @@ { "ai": "claude", "ai_skills": true, + "allow_symlinks": false, "feature_numbering": "sequential", "here": true, "integration": "claude", "script": "sh", - "speckit_version": "1.0.10" + "speckit_version": "1.2.6" } diff --git a/.specify/integration.json b/.specify/integration.json index 022c728..474aee4 100644 --- a/.specify/integration.json +++ b/.specify/integration.json @@ -1,5 +1,5 @@ { - "version": "1.0.10", + "version": "1.2.6", "integration_state_schema": 1, "installed_integrations": [ "claude" diff --git a/.specify/integrations/claude.manifest.json b/.specify/integrations/claude.manifest.json index b1f5917..4bfab3d 100644 --- a/.specify/integrations/claude.manifest.json +++ b/.specify/integrations/claude.manifest.json @@ -1,7 +1,7 @@ { "integration": "claude", - "version": "1.0.10", - "installed_at": "2026-09-23T04:46:11.261794+00:00", + "version": "1.2.6", + "installed_at": "2026-11-03T05:57:00.821508+00:00", "files": { ".claude/skills/speckit-analyze/SKILL.md": "3237cd827194efaf6bb06ba05ef9bc1a703a223d362f5e34d0558c5a38ae34d0", ".claude/skills/speckit-clarify/SKILL.md": "164f49ef87cb2e922585316f5281b455a8e1940b1efd25317d517a567acae472", @@ -10,8 +10,8 @@ ".claude/skills/speckit-converge/SKILL.md": "9f93c1d573592f146670202210128ed24a9d75c92133951b19a2bb0e34eac13b", ".claude/skills/speckit-plan/SKILL.md": "9e97fc0b4ce895c70a1ea720dc0377618c23e27c83ecf4d87b6e0f6c323790a4", ".claude/skills/speckit-checklist/SKILL.md": "5dc436df0c727e9078bf182dd385e5cb1b60ee67ebe644d3f659ec12e3b78abb", - ".claude/skills/speckit-specify/SKILL.md": "d4d93f2444355136e78bb533988aa2dd560dc4b258f4923f67b1a437e7de38f8", + ".claude/skills/speckit-specify/SKILL.md": "044bfaa8d053f1bb4006f318fdf691fabf7da71e1f0ec4908527d5c8eddc61d5", ".claude/skills/speckit-tasks/SKILL.md": "d148aeda1011260c4465c5f105fa1e045a0b0224ccb3e6e0c1ec32c732cae1ae", - ".claude/skills/speckit-taskstoissues/SKILL.md": "464bbaf464a28ffa371b0452599c18e7cb5799630148d9f945e6a4d7066a1234" + ".claude/skills/speckit-taskstoissues/SKILL.md": "471edc95cb41acdc18d4ab3c0f024334bbc4bac9936f955dde30fc3cb9d77473" } } diff --git a/.specify/integrations/speckit.manifest.json b/.specify/integrations/speckit.manifest.json index af0c03e..ea16212 100644 --- a/.specify/integrations/speckit.manifest.json +++ b/.specify/integrations/speckit.manifest.json @@ -1,19 +1,19 @@ { "integration": "speckit", - "version": "1.0.10", + "version": "1.2.6", "installed_at": "2026-07-09T09:52:36.384815+00:00", "files": { - ".specify/scripts/bash/common.sh": "ff7ab71ddcf3db2ee85564a3fd66b4d2a26390150a8540c62109541e5ae607a0", - ".specify/scripts/bash/setup-plan.sh": "d0bd298c026446f3c63167130d65e49e4aa4485de6bef18bcdfd57b19aa0de96", + ".specify/scripts/bash/common.sh": "53cb1c23e349db4586c272436ce35c6e2f36a9c0b5351a85e0b216d9887a26b7", + ".specify/scripts/bash/setup-plan.sh": "d378ec66ba20d2da01c71dad8935062c1c27f0bba890be9a6d3dda52ea056a4e", ".specify/scripts/bash/setup-tasks.sh": "4a33dd1e6c32ddc7d570f4b538572c54069192573eed0ec654d9fb826e31a4fe", - ".specify/scripts/bash/check-prerequisites.sh": "daa377146db4fb69912f42611a7b91c5d55873b3e3e27ed33bd8d67505826344", - ".specify/scripts/bash/create-new-feature.sh": "f4244c6cf95025e5726669938e9152d4a557cc7c1e8b027a43cb4de72677da16", + ".specify/scripts/bash/check-prerequisites.sh": "e127735bbc5ac42181e2a3c773a2d639979f865f18e74a1a977cdf53392a0a7d", + ".specify/scripts/bash/create-new-feature.sh": "972f03ed8ca5ee17b7baad6d56d821d46269d77abc337496beafb0fe85a725f2", ".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3", ".specify/templates/checklist-template.md": "856532b3cb66171c662cc16f16b31a5856e4655a8666aad1e545bbfc7f603ca1", ".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842", ".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e", ".specify/templates/plan-template.md": "7e637502d41eccf0ca672496636365691fdca62ef37b27ec07fcb412dbfa90d4", ".specify/scripts/bash/resolve-template.sh": "829e227096abc8bf0889889ec9f792f503ca5d395b7836a8a7eb739ee75214e7", - ".specify/.gitignore": "8c908410d177a1ef3d0dee16d7ad55f2ac3333df3104c4d4adee1c9b82f1dbc1" + ".specify/.gitignore": "0a61c5378b0bb2e0f85babff99c8b166f2170a2c12378cd17d929aa19296ec88" } } diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh index a6a8ad8..430da3c 100755 --- a/.specify/scripts/bash/check-prerequisites.sh +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -148,6 +148,12 @@ if [[ ! -f "$IMPL_PLAN" ]]; then exit 1 fi +if [[ ! -s "$IMPL_PLAN" ]]; then + echo "ERROR: plan.md is empty in $FEATURE_DIR" >&2 + echo "Populate it, or remove the empty file and rerun /speckit-plan with a valid plan template." >&2 + exit 1 +fi + # Check for spec.md if required if $REQUIRE_SPEC && [[ ! -f "$FEATURE_SPEC" ]]; then echo "ERROR: spec.md not found in $FEATURE_DIR" >&2 diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh index ce1171e..ff18cba 100755 --- a/.specify/scripts/bash/common.sh +++ b/.specify/scripts/bash/common.sh @@ -112,7 +112,7 @@ read_feature_json_feature_directory() { fi if [[ -z "$_fd" ]] && command -v python3 >/dev/null 2>&1; then # Use Python so pretty-printed/multi-line JSON still parses correctly. - if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then + if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1], encoding='utf-8')); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then _fd='' fi fi @@ -169,6 +169,13 @@ get_feature_paths() { no_persist=true shift fi + # SPECIFY_FEATURE_NO_PERSIST is the environment-level equivalent of --no-persist, + # letting an orchestrator (multi-agent runner, CI matrix) guarantee that no + # script invocation in the process tree writes .specify/feature.json, even + # scripts that don't pass --no-persist themselves (#4128). + if [[ "${SPECIFY_FEATURE_NO_PERSIST:-}" == "1" || "${SPECIFY_FEATURE_NO_PERSIST:-}" == "true" ]]; then + no_persist=true + fi # Split decl/assignment so a SPECIFY_INIT_DIR validation failure in # get_repo_root propagates as a hard error instead of being masked by `local`. @@ -399,7 +406,14 @@ check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } _python3_command() { - if command -v python3 >/dev/null 2>&1 && + # SPECKIT_PYTHON_EXECUTABLE is the canonical override; SPECKIT_PYTHON is + # kept as a deprecated fallback (still used by update-agent-context.sh). + local override="${SPECKIT_PYTHON_EXECUTABLE:-${SPECKIT_PYTHON:-}}" + if [[ -n "$override" ]] && command -v "$override" >/dev/null 2>&1 && + "$override" -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1 && + "$override" -c 'import yaml' >/dev/null 2>&1; then + printf '%s\n' "$override" + elif command -v python3 >/dev/null 2>&1 && python3 -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then printf '%s\n' "python3" elif command -v python >/dev/null 2>&1 && @@ -407,7 +421,7 @@ _python3_command() { printf '%s\n' "python" elif command -v py >/dev/null 2>&1 && py -3 -c 'import sys' >/dev/null 2>&1; then - printf '%s\n' "py -3" + printf '%s\n' "py" "-3" else return 1 fi @@ -415,10 +429,12 @@ _python3_command() { _sorted_extension_ids() { local ext_dir="$1" - local python_spec - if python_spec=$(_python3_command); then - local -a python_cmd - read -r -a python_cmd <<< "$python_spec" + local -a python_cmd=() + local _python_cmd_line + while IFS= read -r _python_cmd_line; do + python_cmd+=("$_python_cmd_line") + done < <(_python3_command) + if [ "${#python_cmd[@]}" -gt 0 ]; then local py_stderr sorted_ids py_stderr=$(mktemp) if sorted_ids=$(SPECKIT_EXTENSIONS="$ext_dir" "${python_cmd[@]}" -c " @@ -513,11 +529,11 @@ resolve_template() { local presets_dir="$repo_root/.specify/presets" if [ -d "$presets_dir" ]; then local registry_file="$presets_dir/.registry" - local python_spec="" local -a python_cmd=() - if python_spec=$(_python3_command); then - read -r -a python_cmd <<< "$python_spec" - fi + local _python_cmd_line + while IFS= read -r _python_cmd_line; do + python_cmd+=("$_python_cmd_line") + done < <(_python3_command) if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then # Read preset IDs sorted by priority (lower number = higher precedence). # The python3 call is wrapped in an if-condition so that set -e does not @@ -636,11 +652,11 @@ resolve_template_content() { local registry_file="$presets_dir/.registry" local sorted_presets="" local registry_parsed=false - local python_spec="" local -a python_cmd=() - if python_spec=$(_python3_command); then - read -r -a python_cmd <<< "$python_spec" - fi + local _python_cmd_line + while IFS= read -r _python_cmd_line; do + python_cmd+=("$_python_cmd_line") + done < <(_python3_command) if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" "${python_cmd[@]}" -c " import json, re, sys, os diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh index 94294f2..3ce2f90 100755 --- a/.specify/scripts/bash/create-new-feature.sh +++ b/.specify/scripts/bash/create-new-feature.sh @@ -146,17 +146,82 @@ spec_prefix_exists() { # Function to clean and format a branch name # -# Three details keep this byte-identical to the Python and PowerShell twins: -# * LC_ALL=C -- in a UTF-8 locale glibc resolves the a-z *range* through -# collation, so [^a-z0-9] keeps accented lowercase letters that -# re.sub(r"[^a-z0-9]", ...) and .NET's -replace both strip. +# Three details keep this consistent with the Python and PowerShell twins: +# * Unicode classification uses Python: POSIX [:alnum:] differs by platform. # * `--*` instead of the GNU-only `\+`, which POSIX/BSD sed reads as a literal # '+', leaving repeated separators uncollapsed on macOS. # * printf instead of echo, so a name of "-n"/"-e"/"-E" is text, not options. +contains_non_ascii() { + LC_ALL=C grep -q '[^[:print:][:cntrl:]]' +} + +UNICODE_LOCALE="" +locale_candidates=(C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8 "${LC_CTYPE:-${LANG:-}}") +if [ -n "${LC_ALL:-}" ]; then + locale_candidates=("$LC_ALL") +fi +for candidate in "${locale_candidates[@]}"; do + if [ -n "$candidate" ] && [ "$(printf 'é。' | LC_ALL="$candidate" sed 's/[^[:alnum:]]/-/g' 2>/dev/null)" = 'é-' ]; then + UNICODE_LOCALE="$candidate" + break + fi +done + +if [ -z "$UNICODE_LOCALE" ]; then + UNICODE_LOCALE=C + if printf '%s' "${SHORT_NAME:-$FEATURE_DESCRIPTION}" | contains_non_ascii; then + if [ -n "${LC_ALL:-}" ]; then + echo "Error: A UTF-8 locale is required to create a Unicode feature name; LC_ALL=$LC_ALL is not usable" >&2 + else + echo "Error: A UTF-8 locale is required to create a Unicode feature name" >&2 + fi + exit 1 + fi +fi + +unicode_words() { + local name="${1//$'\n'/ }" + local separator="$2" + if printf '%s' "$name" | contains_non_ascii; then + local -a python_cmd=() + local override="${SPECKIT_PYTHON_EXECUTABLE:-${SPECKIT_PYTHON:-}}" + if [ -n "$override" ] && command -v "$override" >/dev/null 2>&1 && + "$override" -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then + python_cmd=("$override") + else + local python_line + while IFS= read -r python_line; do + python_cmd+=("$python_line") + done < <(_python3_command) + fi + if [ "${#python_cmd[@]}" -eq 0 ]; then + echo "Error: Python 3 is required to create a Unicode feature name" >&2 + return 1 + fi + printf '%s' "$name" | "${python_cmd[@]}" -c ' +import sys +value = sys.stdin.buffer.read().decode("utf-8") +lower = str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") +result = "".join( + char if char.isalpha() or char.isdecimal() else sys.argv[1] + for char in value.translate(lower) +) +sys.stdout.buffer.write(result.encode("utf-8")) +' "$separator" + else + printf '%s' "$name" | LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C sed "s/[^a-z0-9]/$separator/g" + fi +} + clean_branch_name() { local name="$1" - local -x LC_ALL=C - printf '%s\n' "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//' + local cleaned + cleaned=$(unicode_words "$name" '-') || return 1 + printf '%s\n' "$cleaned" | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//' +} + +branch_byte_count() { + printf '%s' "$1" | wc -c | tr -d '[:space:]' } # Fit a feature prefix and suffix within GitHub's branch-name limit. @@ -165,11 +230,25 @@ fit_branch_name() { local branch_suffix="$2" local branch_name="${feature_num}-${branch_suffix}" - if [ ${#branch_name} -gt $MAX_BRANCH_LENGTH ]; then + if [ "$(branch_byte_count "$branch_name")" -gt "$MAX_BRANCH_LENGTH" ]; then local prefix_length=$(( ${#feature_num} + 1 )) local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length)) local truncated_suffix - truncated_suffix=$(printf '%s' "$branch_suffix" | cut -c "1-$max_suffix_length" | sed 's/-$//') + local -x LC_ALL="$UNICODE_LOCALE" + local low=0 high=${#branch_suffix} mid + if (( high > max_suffix_length )); then + high=$max_suffix_length + fi + while (( low < high )); do + mid=$(((low + high + 1) / 2)) + if [ "$(branch_byte_count "${branch_suffix:0:$mid}")" -le "$max_suffix_length" ]; then + low=$mid + else + high=$((mid - 1)) + fi + done + truncated_suffix="${branch_suffix:0:$low}" + truncated_suffix="${truncated_suffix%-}" branch_name="${feature_num}-${truncated_suffix}" fi @@ -209,12 +288,10 @@ generate_branch_name() { # Common stop words to filter out local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" - # Convert to lowercase and split into words. LC_ALL=C for the same - # collation reason documented on clean_branch_name, and so the `grep -qw` - # acronym probe below uses ASCII word boundaries like the Python twin's - # (?= 3 OR are potential acronyms) - if ! echo "$word" | grep -qiE "$stop_words"; then - if [ ${#word} -ge 3 ]; then + # Retain non-ASCII words even when shorter than three characters. + if ! printf '%s\n' "$word" | LC_ALL=C grep -qE "$stop_words"; then + if [ ${#word} -ge 3 ] || printf '%s' "$word" | contains_non_ascii; then meaningful_words+=("$word") # Keep short words that appear as an uppercase acronym in the original. # Uppercase via tr and match with grep -w (both portable) rather than # bash's 4+ "^^" case expansion (breaks on macOS bash 3.2) and \b (non-POSIX). - elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then + elif printf '%s' "$description" | LC_ALL=C grep -qw -- "$(printf '%s' "$word" | LC_ALL=C tr '[:lower:]' '[:upper:]')"; then meaningful_words+=("$word") fi fi @@ -266,7 +343,7 @@ else fi if [ -z "$BRANCH_SUFFIX" ]; then - echo "[specify] Warning: Feature name is empty after removing unsupported characters. Use --short-name with ASCII letters or digits (for example, user-auth)." >&2 + echo "[specify] Warning: Feature name is empty after removing unsupported characters. Use --short-name with letters or digits (for example, user-auth)." >&2 fi # Warn if --number and --timestamp are both specified @@ -339,8 +416,8 @@ ORIGINAL_BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX") if [ "$BRANCH_NAME" != "$ORIGINAL_BRANCH_NAME" ]; then >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" - >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" - >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" + >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME ($(branch_byte_count "$ORIGINAL_BRANCH_NAME") bytes)" + >&2 echo "[specify] Truncated to: $BRANCH_NAME ($(branch_byte_count "$BRANCH_NAME") bytes)" fi FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" @@ -383,8 +460,11 @@ if [ "$DRY_RUN" != true ]; then fi fi - # Persist to .specify/feature.json so downstream commands can find the feature - _persist_feature_json "$REPO_ROOT" "$FEATURE_DIR" + # Persist to .specify/feature.json so downstream commands can find the + # feature, unless the orchestrator opted out via SPECIFY_FEATURE_NO_PERSIST (#4129). + if [[ "${SPECIFY_FEATURE_NO_PERSIST:-}" != "1" && "${SPECIFY_FEATURE_NO_PERSIST:-}" != "true" ]]; then + _persist_feature_json "$REPO_ROOT" "$FEATURE_DIR" + fi # Inform the user how to set feature state in their own shell printf '# To persist: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" >&2 diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh index aa39418..9c8d163 100755 --- a/.specify/scripts/bash/setup-plan.sh +++ b/.specify/scripts/bash/setup-plan.sh @@ -43,7 +43,15 @@ if [[ -f "$IMPL_PLAN" ]]; then echo "Plan already exists at $IMPL_PLAN, skipping template copy" fi else - if resolve_template_content "plan-template" "$REPO_ROOT" > "$IMPL_PLAN"; then + template_file=$(mktemp) + trap 'rm -f "$template_file"' EXIT + if resolve_template_content "plan-template" "$REPO_ROOT" > "$template_file"; then + if [[ ! -s "$template_file" ]]; then + echo "ERROR: Plan template is empty; no new plan.md was created." >&2 + echo "Restore a valid plan template and rerun plan setup." >&2 + exit 1 + fi + cat "$template_file" > "$IMPL_PLAN" if $JSON_MODE; then echo "Copied plan template to $IMPL_PLAN" >&2 else @@ -51,16 +59,12 @@ else fi else resolve_status=$? - rm -f "$IMPL_PLAN" if [ "$resolve_status" -ne 1 ]; then exit "$resolve_status" fi - if $JSON_MODE; then - echo "Warning: Plan template not found" >&2 - else - echo "Warning: Plan template not found" - fi - touch "$IMPL_PLAN" + echo "ERROR: Plan template not found; no new plan.md was created." >&2 + echo "Restore a valid plan template and rerun plan setup." >&2 + exit 1 fi fi