From 18b71f29c3a7aee02d6718fd45dc98958a0b3a52 Mon Sep 17 00:00:00 2001 From: Implementer Date: Tue, 3 Nov 2026 06:59:17 +0100 Subject: [PATCH] The spec-kit pin is 1.2.6 Fourteen releases had accumulated since 1.0.10, and taskstoissues is deprecated with removal planned for a future minor release, so the gap only gets harder to cross. None of them changes how a slice is run here. speckit-specify now takes its spec template from resolve-template.sh, which matters only with presets, and we have none; setup-plan.sh now stops when the plan template is missing or empty, where it used to create an empty plan.md. A refresh reverts the templates it did not write, so plan-template.md and tasks-template.md are restored: they carry this repository's constitution gates, the TDD rule and the documentation tasks. Upstream changed neither file between the two tags, and their manifest hashes are unchanged, so the restored copies lose nothing. The refresh also writes allow_symlinks: false and ignores adapter packages under .specify, neither of which affects anything here. Verified: ./mvnw -B verify passes with 234 tests; conventions-sync check, conventions-check, conventions-format, service-conventions-sync check and service-conventions-check pass; on bash 3.2, resolve-template.sh resolves the tailored plan template and create-new-feature.sh --dry-run names 010. Process: Delivery Phase: Implement Track: Code Co-Authored-By: Claude Opus 5.5 --- .claude/skills/speckit-specify/SKILL.md | 6 +- .claude/skills/speckit-taskstoissues/SKILL.md | 10 +- .specify/.gitignore | 5 + .specify/init-options.json | 3 +- .specify/integration.json | 2 +- .specify/integrations/claude.manifest.json | 8 +- .specify/integrations/speckit.manifest.json | 12 +- .specify/scripts/bash/check-prerequisites.sh | 6 + .specify/scripts/bash/common.sh | 46 ++++--- .specify/scripts/bash/create-new-feature.sh | 126 ++++++++++++++---- .specify/scripts/bash/setup-plan.sh | 20 +-- 11 files changed, 182 insertions(+), 62 deletions(-) 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