From 2641afaabe7de76f68a8309114e6348728eecd12 Mon Sep 17 00:00:00 2001 From: "Peter.Morton" Date: Fri, 27 Mar 2026 15:18:48 -0500 Subject: [PATCH] Updated speckit to v0.4.3 --- .github/agents/speckit.analyze.agent.md | 6 +- .github/agents/speckit.clarify.agent.md | 2 +- .github/agents/speckit.implement.agent.md | 65 ++++- .github/agents/speckit.plan.agent.md | 63 +++++ .github/agents/speckit.specify.agent.md | 100 +++++-- .github/agents/speckit.tasks.agent.md | 63 +++++ .specify/init-options.json | 11 + .specify/scripts/bash/check-prerequisites.sh | 42 ++- .specify/scripts/bash/common.sh | 246 +++++++++++++++--- .specify/scripts/bash/create-new-feature.sh | 142 +++++----- .specify/scripts/bash/setup-plan.sh | 24 +- .specify/scripts/bash/update-agent-context.sh | 228 ++++++++-------- .specify/templates/spec-template.md | 13 + 13 files changed, 753 insertions(+), 252 deletions(-) create mode 100644 .specify/init-options.json diff --git a/.github/agents/speckit.analyze.agent.md b/.github/agents/speckit.analyze.agent.md index 98b04b0..0c71cf3 100644 --- a/.github/agents/speckit.analyze.agent.md +++ b/.github/agents/speckit.analyze.agent.md @@ -41,7 +41,7 @@ Load only the minimal necessary context from each artifact: - Overview/Context - Functional Requirements -- Non-Functional Requirements +- Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact) - User Stories - Edge Cases (if present) @@ -68,7 +68,7 @@ Load only the minimal necessary context from each artifact: Create internal representations (do not include raw artifacts in output): -- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`) +- **Requirements inventory**: For each Functional Requirement (FR-###) and Success Criterion (SC-###), record a stable key. Use the explicit FR-/SC- identifier as the primary key when present, and optionally also derive an imperative-phrase slug for readability (e.g., "User can upload file" → `user-can-upload-file`). Include only Success Criteria items that require buildable work (e.g., load-testing infrastructure, security audit tooling), and exclude post-launch outcome metrics and business KPIs (e.g., "Reduce support tickets by 50%"). - **User story/action inventory**: Discrete user actions with acceptance criteria - **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) - **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements @@ -102,7 +102,7 @@ Focus on high-signal findings. Limit to 50 findings total; aggregate remainder i - Requirements with zero associated tasks - Tasks with no mapped requirement/story -- Non-functional requirements not reflected in tasks (e.g., performance, security) +- Success Criteria requiring buildable work (performance, security, availability) not reflected in tasks #### F. Inconsistency diff --git a/.github/agents/speckit.clarify.agent.md b/.github/agents/speckit.clarify.agent.md index 657439f..3f4376a 100644 --- a/.github/agents/speckit.clarify.agent.md +++ b/.github/agents/speckit.clarify.agent.md @@ -142,7 +142,7 @@ Execution steps: - Functional ambiguity → Update or add a bullet in Functional Requirements. - User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario. - Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly. - - Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target). + - Non-functional constraint → Add/modify measurable criteria in Success Criteria > Measurable Outcomes (convert vague adjective to metric or explicit target). - Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it). - Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once. - If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text. diff --git a/.github/agents/speckit.implement.agent.md b/.github/agents/speckit.implement.agent.md index 14bfc9e..5847f61 100644 --- a/.github/agents/speckit.implement.agent.md +++ b/.github/agents/speckit.implement.agent.md @@ -10,6 +10,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before implementation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_implement` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` 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"). @@ -85,7 +119,7 @@ You **MUST** consider the user input before proceeding (if not empty). - **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*` - **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*` - **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*` - - **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*` + - **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `*.dll`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*` - **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/` - **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/` - **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/` @@ -133,3 +167,32 @@ You **MUST** consider the user input before proceeding (if not empty). - Report final status with summary of completed work Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list. + +10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_implement` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally + - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. + - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation + - For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.github/agents/speckit.plan.agent.md b/.github/agents/speckit.plan.agent.md index 0ffb929..99ab54d 100644 --- a/.github/agents/speckit.plan.agent.md +++ b/.github/agents/speckit.plan.agent.md @@ -18,6 +18,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before planning)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_plan` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. 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"). @@ -35,6 +69,35 @@ You **MUST** consider the user input before proceeding (if not empty). 4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts. +5. **Check for extension hooks**: After reporting, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_plan` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally + - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. + - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation + - For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Phases ### Phase 0: Outline & Research diff --git a/.github/agents/speckit.specify.agent.md b/.github/agents/speckit.specify.agent.md index ae62bc6..fbb630b 100644 --- a/.github/agents/speckit.specify.agent.md +++ b/.github/agents/speckit.specify.agent.md @@ -18,6 +18,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before specification)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_specify` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command. @@ -36,33 +70,20 @@ Given that feature description, do this: - "Create a dashboard for analytics" → "analytics-dashboard" - "Fix payment processing timeout bug" → "fix-payment-timeout" -2. **Check for existing branches before creating new one**: +2. **Create the feature branch** by running the script with `--short-name` (and `--json`). In sequential mode, do NOT pass `--number` — the script auto-detects the next available number. In timestamp mode, the script generates a `YYYYMMDD-HHMMSS` prefix automatically: - a. First, fetch all remote branches to ensure we have the latest information: + **Branch numbering mode**: Before running the script, check if `.specify/init-options.json` exists and read the `branch_numbering` value. + - If `"timestamp"`, add `--timestamp` (Bash) or `-Timestamp` (PowerShell) to the script invocation + - If `"sequential"` or absent, do not add any extra flag (default behavior) - ```bash - git fetch --all --prune - ``` - - b. Find the highest feature number across all sources for the short-name: - - Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-$'` - - Local branches: `git branch | grep -E '^[* ]*[0-9]+-$'` - - Specs directories: Check for directories matching `specs/[0-9]+-` - - c. Determine the next available number: - - Extract all numbers from all three sources - - Find the highest number N - - Use N+1 for the new branch number - - d. Run the script `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` with the calculated number and short-name: - - Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description - - Bash example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"` - - PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"` + - Bash example: `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" --json --short-name "user-auth" "Add user authentication"` + - Bash (timestamp): `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" --json --timestamp --short-name "user-auth" "Add user authentication"` + - PowerShell example: `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" -Json -ShortName "user-auth" "Add user authentication"` + - PowerShell (timestamp): `.specify/scripts/bash/create-new-feature.sh "$ARGUMENTS" -Json -Timestamp -ShortName "user-auth" "Add user authentication"` **IMPORTANT**: - - Check all three sources (remote branches, local branches, specs directories) to find the highest number - - Only match branches/directories with the exact short-name pattern - - If no existing branches/directories found with this short-name, start with number 1 + - Do NOT pass `--number` — the script determines the correct next number automatically + - Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably - You must only ever run this script once per feature - The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for - The JSON output will contain BRANCH_NAME and SPEC_FILE paths @@ -145,7 +166,7 @@ Given that feature description, do this: c. **Handle Validation Results**: - - **If all items pass**: Mark checklist complete and proceed to step 6 + - **If all items pass**: Mark checklist complete and proceed to step 7 - **If items fail (excluding [NEEDS CLARIFICATION])**: 1. List the failing items and specific issues @@ -192,9 +213,36 @@ Given that feature description, do this: 7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`). -**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. +8. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_specify` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally + - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. + - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation + - For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks -## General Guidelines + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + +**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing. ## Quick Guidelines diff --git a/.github/agents/speckit.tasks.agent.md b/.github/agents/speckit.tasks.agent.md index e09112b..d021892 100644 --- a/.github/agents/speckit.tasks.agent.md +++ b/.github/agents/speckit.tasks.agent.md @@ -19,6 +19,40 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Pre-Execution Checks + +**Check for extension hooks (before tasks generation)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_tasks` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation +- For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Pre-Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Pre-Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + + Wait for the result of the hook command before proceeding to the Outline. + ``` +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + ## Outline 1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` 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"). @@ -60,6 +94,35 @@ You **MUST** consider the user input before proceeding (if not empty). - Suggested MVP scope (typically just User Story 1) - Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths) +6. **Check for extension hooks**: After tasks.md is generated, check if `.specify/extensions.yml` exists in the project root. + - If it exists, read it and look for entries under the `hooks.after_tasks` key + - If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally + - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. + - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: + - If the hook has no `condition` field, or it is null/empty, treat the hook as executable + - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation + - For each executable hook, output the following based on its `optional` flag: + - **Optional hook** (`optional: true`): + ``` + ## Extension Hooks + + **Optional Hook**: {extension} + Command: `/{command}` + Description: {description} + + Prompt: {prompt} + To execute: `/{command}` + ``` + - **Mandatory hook** (`optional: false`): + ``` + ## Extension Hooks + + **Automatic Hook**: {extension} + Executing: `/{command}` + EXECUTE_COMMAND: {command} + ``` + - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently + Context for task generation: $ARGUMENTS The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context. diff --git a/.specify/init-options.json b/.specify/init-options.json new file mode 100644 index 0000000..4a51d76 --- /dev/null +++ b/.specify/init-options.json @@ -0,0 +1,11 @@ +{ + "ai": "copilot", + "ai_commands_dir": null, + "ai_skills": false, + "branch_numbering": "sequential", + "here": true, + "offline": false, + "preset": null, + "script": "sh", + "speckit_version": "0.4.3" +} \ No newline at end of file diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh index 98e387c..88a5559 100755 --- a/.specify/scripts/bash/check-prerequisites.sh +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -79,15 +79,28 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" # Get feature paths and validate branch -eval $(get_feature_paths) +_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +eval "$_paths_output" +unset _paths_output check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 # If paths-only mode, output paths and exit (support JSON + paths-only combined) if $PATHS_ONLY; then if $JSON_MODE; then # Minimal JSON paths payload (no validation performed) - printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ - "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" + if has_jq; then + jq -cn \ + --arg repo_root "$REPO_ROOT" \ + --arg branch "$CURRENT_BRANCH" \ + --arg feature_dir "$FEATURE_DIR" \ + --arg feature_spec "$FEATURE_SPEC" \ + --arg impl_plan "$IMPL_PLAN" \ + --arg tasks "$TASKS" \ + '{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}' + else + printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ + "$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")" + fi else echo "REPO_ROOT: $REPO_ROOT" echo "BRANCH: $CURRENT_BRANCH" @@ -141,14 +154,25 @@ fi # Output results if $JSON_MODE; then # Build JSON array of documents - if [[ ${#docs[@]} -eq 0 ]]; then - json_docs="[]" + 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" \ + '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}' else - json_docs=$(printf '"%s",' "${docs[@]}") - json_docs="[${json_docs%,}]" + 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}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs" fi - - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" else # Text output echo "FEATURE_DIR:$FEATURE_DIR" diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh index 2c3165e..416fcad 100755 --- a/.specify/scripts/bash/common.sh +++ b/.specify/scripts/bash/common.sh @@ -1,15 +1,48 @@ #!/usr/bin/env bash # Common functions and variables for all scripts -# Get repository root, with fallback for non-git repositories +# Find repository root by searching upward for .specify directory +# This is the primary marker for spec-kit projects +find_specify_root() { + local dir="${1:-$(pwd)}" + # Normalize to absolute path to prevent infinite loop with relative paths + # Use -- to handle paths starting with - (e.g., -P, -L) + dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1 + local prev_dir="" + while true; do + if [ -d "$dir/.specify" ]; then + echo "$dir" + return 0 + fi + # Stop if we've reached filesystem root or dirname stops changing + if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then + break + fi + prev_dir="$dir" + dir="$(dirname "$dir")" + done + return 1 +} + +# Get repository root, prioritizing .specify directory over git +# This prevents using a parent git repo when spec-kit is initialized in a subdirectory get_repo_root() { + # First, look for .specify directory (spec-kit's own marker) + local specify_root + if specify_root=$(find_specify_root); then + echo "$specify_root" + return + fi + + # Fallback to git if no .specify found if git rev-parse --show-toplevel >/dev/null 2>&1; then git rev-parse --show-toplevel - else - # Fall back to script location for non-git repos - local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - (cd "$script_dir/../../.." && pwd) + return fi + + # Final fallback to script location for non-git repos + local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../../.." && pwd) } # Get current branch, with fallback for non-git repositories @@ -20,29 +53,40 @@ get_current_branch() { return fi - # Then check git if available - if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then - git rev-parse --abbrev-ref HEAD + # Then check git if available at the spec-kit root (not parent) + local repo_root=$(get_repo_root) + if has_git; then + git -C "$repo_root" rev-parse --abbrev-ref HEAD return fi # For non-git repos, try to find the latest feature directory - local repo_root=$(get_repo_root) local specs_dir="$repo_root/specs" if [[ -d "$specs_dir" ]]; then local latest_feature="" local highest=0 + local latest_timestamp="" for dir in "$specs_dir"/*; do if [[ -d "$dir" ]]; then local dirname=$(basename "$dir") - if [[ "$dirname" =~ ^([0-9]{3})- ]]; then + if [[ "$dirname" =~ ^([0-9]{8}-[0-9]{6})- ]]; then + # Timestamp-based branch: compare lexicographically + local ts="${BASH_REMATCH[1]}" + if [[ "$ts" > "$latest_timestamp" ]]; then + latest_timestamp="$ts" + latest_feature=$dirname + fi + elif [[ "$dirname" =~ ^([0-9]{3})- ]]; then local number=${BASH_REMATCH[1]} number=$((10#$number)) if [[ "$number" -gt "$highest" ]]; then highest=$number - latest_feature=$dirname + # Only update if no timestamp branch found yet + if [[ -z "$latest_timestamp" ]]; then + latest_feature=$dirname + fi fi fi fi @@ -57,9 +101,17 @@ get_current_branch() { echo "main" # Final fallback } -# Check if we have git available +# Check if we have git available at the spec-kit root level +# Returns true only if git is installed and the repo root is inside a git work tree +# Handles both regular repos (.git directory) and worktrees/submodules (.git file) has_git() { - git rev-parse --show-toplevel >/dev/null 2>&1 + # First check if git command is available (before calling get_repo_root which may use git) + command -v git >/dev/null 2>&1 || return 1 + local repo_root=$(get_repo_root) + # Check if .git exists (directory or file for worktrees/submodules) + [ -e "$repo_root/.git" ] || return 1 + # Verify it's actually a valid git work tree + git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1 } check_feature_branch() { @@ -72,9 +124,9 @@ check_feature_branch() { return 0 fi - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then + if [[ ! "$branch" =~ ^[0-9]{3}- ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 - echo "Feature branches should be named like: 001-feature-name" >&2 + echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2 return 1 fi @@ -90,15 +142,18 @@ find_feature_dir_by_prefix() { local branch_name="$2" local specs_dir="$repo_root/specs" - # Extract numeric prefix from branch (e.g., "004" from "004-whatever") - if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then - # If branch doesn't have numeric prefix, fall back to exact match + # Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches) + local prefix="" + if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then + prefix="${BASH_REMATCH[1]}" + elif [[ "$branch_name" =~ ^([0-9]{3})- ]]; then + prefix="${BASH_REMATCH[1]}" + else + # If branch doesn't have a recognized prefix, fall back to exact match echo "$specs_dir/$branch_name" return fi - local prefix="${BASH_REMATCH[1]}" - # Search for directories in specs/ that start with this prefix local matches=() if [[ -d "$specs_dir" ]]; then @@ -119,8 +174,8 @@ find_feature_dir_by_prefix() { else # Multiple matches - this shouldn't happen with proper naming convention echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2 - echo "Please ensure only one spec directory exists per numeric prefix." >&2 - echo "$specs_dir/$branch_name" # Return something to avoid breaking the script + echo "Please ensure only one spec directory exists per prefix." >&2 + return 1 fi } @@ -134,23 +189,142 @@ get_feature_paths() { fi # Use prefix-based lookup to support multiple branches per spec - local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch") + local feature_dir + if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then + echo "ERROR: Failed to resolve feature directory" >&2 + return 1 + fi - cat </dev/null 2>&1 +} + +# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable). +# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259). +json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//$'\n'/\\n}" + s="${s//$'\t'/\\t}" + s="${s//$'\r'/\\r}" + s="${s//$'\b'/\\b}" + s="${s//$'\f'/\\f}" + # Escape any remaining U+0001-U+001F control characters as \uXXXX. + # (U+0000/NUL cannot appear in bash strings and is excluded.) + # LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes, + # so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact. + local LC_ALL=C + local i char code + for (( i=0; i<${#s}; i++ )); do + char="${s:$i:1}" + printf -v code '%d' "'$char" 2>/dev/null || code=256 + if (( code >= 1 && code <= 31 )); then + printf '\\u%04x' "$code" + else + printf '%s' "$char" + fi + done } check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } +# Resolve a template name to a file path using the priority stack: +# 1. .specify/templates/overrides/ +# 2. .specify/presets//templates/ (sorted by priority from .registry) +# 3. .specify/extensions//templates/ +# 4. .specify/templates/ (core) +resolve_template() { + local template_name="$1" + local repo_root="$2" + local base="$repo_root/.specify/templates" + + # Priority 1: Project overrides + local override="$base/overrides/${template_name}.md" + [ -f "$override" ] && echo "$override" && return 0 + + # Priority 2: Installed presets (sorted by priority from .registry) + local presets_dir="$repo_root/.specify/presets" + if [ -d "$presets_dir" ]; then + local registry_file="$presets_dir/.registry" + if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; 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 + # abort the function when python3 exits non-zero (e.g. invalid JSON). + local sorted_presets="" + if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c " +import json, sys, os +try: + with open(os.environ['SPECKIT_REGISTRY']) as f: + data = json.load(f) + presets = data.get('presets', {}) + for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)): + print(pid) +except Exception: + sys.exit(1) +" 2>/dev/null); then + if [ -n "$sorted_presets" ]; then + # python3 succeeded and returned preset IDs — search in priority order + while IFS= read -r preset_id; do + local candidate="$presets_dir/$preset_id/templates/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 + done <<< "$sorted_presets" + fi + # python3 succeeded but registry has no presets — nothing to search + else + # python3 failed (missing, or registry parse error) — fall back to unordered directory scan + for preset in "$presets_dir"/*/; do + [ -d "$preset" ] || continue + local candidate="$preset/templates/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 + done + fi + else + # Fallback: alphabetical directory order (no python3 available) + for preset in "$presets_dir"/*/; do + [ -d "$preset" ] || continue + local candidate="$preset/templates/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 + done + fi + fi + + # Priority 3: Extension-provided templates + local ext_dir="$repo_root/.specify/extensions" + if [ -d "$ext_dir" ]; then + for ext in "$ext_dir"/*/; do + [ -d "$ext" ] || continue + # Skip hidden directories (e.g. .backup, .cache) + case "$(basename "$ext")" in .*) continue;; esac + local candidate="$ext/templates/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 + done + fi + + # Priority 4: Core templates + local core="$base/${template_name}.md" + [ -f "$core" ] && echo "$core" && return 0 + + # Template not found in any location. + # Return 1 so callers can distinguish "not found" from "found". + # Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true + return 1 +} + diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh index 5469702..a393edd 100755 --- a/.specify/scripts/bash/create-new-feature.sh +++ b/.specify/scripts/bash/create-new-feature.sh @@ -5,13 +5,14 @@ set -e JSON_MODE=false SHORT_NAME="" BRANCH_NUMBER="" +USE_TIMESTAMP=false ARGS=() i=1 while [ $i -le $# ]; do arg="${!i}" case "$arg" in - --json) - JSON_MODE=true + --json) + JSON_MODE=true ;; --short-name) if [ $((i + 1)) -gt $# ]; then @@ -40,22 +41,27 @@ while [ $i -le $# ]; do fi BRANCH_NUMBER="$next_arg" ;; - --help|-h) - echo "Usage: $0 [--json] [--short-name ] [--number N] " + --timestamp) + USE_TIMESTAMP=true + ;; + --help|-h) + echo "Usage: $0 [--json] [--short-name ] [--number N] [--timestamp] " echo "" echo "Options:" echo " --json Output in JSON format" echo " --short-name Provide a custom short name (2-4 words) for the branch" echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering" echo " --help, -h Show this help message" echo "" echo "Examples:" echo " $0 'Add user authentication system' --short-name 'user-auth'" echo " $0 'Implement OAuth2 integration for API' --number 5" + echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'" exit 0 ;; - *) - ARGS+=("$arg") + *) + ARGS+=("$arg") ;; esac i=$((i + 1)) @@ -63,7 +69,7 @@ done FEATURE_DESCRIPTION="${ARGS[*]}" if [ -z "$FEATURE_DESCRIPTION" ]; then - echo "Usage: $0 [--json] [--short-name ] [--number N] " >&2 + echo "Usage: $0 [--json] [--short-name ] [--number N] [--timestamp] " >&2 exit 1 fi @@ -74,19 +80,6 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then exit 1 fi -# Function to find the repository root by searching for existing project markers -find_repo_root() { - local dir="$1" - while [ "$dir" != "/" ]; do - if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then - echo "$dir" - return 0 - fi - dir="$(dirname "$dir")" - done - return 1 -} - # Function to get highest number from specs directory get_highest_from_specs() { local specs_dir="$1" @@ -96,10 +89,13 @@ get_highest_from_specs() { for dir in "$specs_dir"/*; do [ -d "$dir" ] || continue dirname=$(basename "$dir") - number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") - number=$((10#$number)) - if [ "$number" -gt "$highest" ]; then - highest=$number + # Match sequential prefixes (>=3 digits), but skip timestamp dirs. + if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then + number=$(echo "$dirname" | grep -Eo '^[0-9]+') + number=$((10#$number)) + if [ "$number" -gt "$highest" ]; then + highest=$number + fi fi done fi @@ -119,9 +115,9 @@ get_highest_from_branches() { # Clean branch name: remove leading markers and remote prefixes clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') - # Extract feature number if branch matches pattern ###-* - if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then - number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0") + # Extract sequential feature number (>=3 digits), skip timestamp branches. + if echo "$clean_branch" | grep -Eq '^[0-9]{3,}-' && ! echo "$clean_branch" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then + number=$(echo "$clean_branch" | grep -Eo '^[0-9]+' || echo "0") number=$((10#$number)) if [ "$number" -gt "$highest" ]; then highest=$number @@ -138,7 +134,7 @@ check_existing_branches() { local specs_dir="$1" # Fetch all remotes to get latest branch info (suppress errors if no remotes) - git fetch --all --prune 2>/dev/null || true + git fetch --all --prune >/dev/null 2>&1 || true # Get highest number from ALL branches (not just matching short name) local highest_branch=$(get_highest_from_branches) @@ -162,20 +158,16 @@ clean_branch_name() { echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' } -# Resolve repository root. Prefer git information when available, but fall back -# to searching for repository markers so the workflow still functions in repositories that -# were initialised with --no-git. +# Resolve repository root using common.sh functions which prioritize .specify over git SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" -if git rev-parse --show-toplevel >/dev/null 2>&1; then - REPO_ROOT=$(git rev-parse --show-toplevel) +REPO_ROOT=$(get_repo_root) + +# Check if git is available at this repo root (not a parent) +if has_git; then HAS_GIT=true else - REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")" - if [ -z "$REPO_ROOT" ]; then - echo "Error: Could not determine repository root. Please run this script from within the repository." >&2 - exit 1 - fi HAS_GIT=false fi @@ -241,29 +233,42 @@ else BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") fi -# Determine branch number -if [ -z "$BRANCH_NUMBER" ]; then - if [ "$HAS_GIT" = true ]; then - # Check existing branches on remotes - BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") - else - # Fall back to local directory check - HIGHEST=$(get_highest_from_specs "$SPECS_DIR") - BRANCH_NUMBER=$((HIGHEST + 1)) - fi +# Warn if --number and --timestamp are both specified +if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then + >&2 echo "[specify] Warning: --number is ignored when --timestamp is used" + BRANCH_NUMBER="" fi -# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) -FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") -BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" +# Determine branch prefix +if [ "$USE_TIMESTAMP" = true ]; then + FEATURE_NUM=$(date +%Y%m%d-%H%M%S) + BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" +else + # Determine branch number + if [ -z "$BRANCH_NUMBER" ]; then + if [ "$HAS_GIT" = true ]; then + # Check existing branches on remotes + BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") + else + # Fall back to local directory check + HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + BRANCH_NUMBER=$((HIGHEST + 1)) + fi + fi + + # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) + FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") + BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" +fi # GitHub enforces a 244-byte limit on branch names # Validate and truncate if necessary MAX_BRANCH_LENGTH=244 if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then # Calculate how much we need to trim from suffix - # Account for: feature number (3) + hyphen (1) = 4 chars - MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) + # Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4 + PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 )) + MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH)) # Truncate suffix at word boundary if possible TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) @@ -282,7 +287,11 @@ if [ "$HAS_GIT" = true ]; then if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then # Check if branch already exists if git branch --list "$BRANCH_NAME" | grep -q .; then - >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number." + if [ "$USE_TIMESTAMP" = true ]; then + >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name." + else + >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number." + fi exit 1 else >&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again." @@ -296,18 +305,31 @@ fi FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" mkdir -p "$FEATURE_DIR" -TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" +TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true SPEC_FILE="$FEATURE_DIR/spec.md" -if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi +if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then + cp "$TEMPLATE" "$SPEC_FILE" +else + echo "Warning: Spec template not found; created empty spec file" >&2 + touch "$SPEC_FILE" +fi -# Set the SPECIFY_FEATURE environment variable for the current session -export SPECIFY_FEATURE="$BRANCH_NAME" +# Inform the user how to persist the feature variable in their own shell +printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2 if $JSON_MODE; then - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" + if command -v jq >/dev/null 2>&1; then + jq -cn \ + --arg branch_name "$BRANCH_NAME" \ + --arg spec_file "$SPEC_FILE" \ + --arg feature_num "$FEATURE_NUM" \ + '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}' + else + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" + fi else echo "BRANCH_NAME: $BRANCH_NAME" echo "SPEC_FILE: $SPEC_FILE" echo "FEATURE_NUM: $FEATURE_NUM" - echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" + printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" fi diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh index d01c6d6..9f55231 100755 --- a/.specify/scripts/bash/setup-plan.sh +++ b/.specify/scripts/bash/setup-plan.sh @@ -28,7 +28,9 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" # Get all paths and variables from common functions -eval $(get_feature_paths) +_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +eval "$_paths_output" +unset _paths_output # Check if we're on a proper feature branch (only for git repos) check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 @@ -37,20 +39,30 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 mkdir -p "$FEATURE_DIR" # Copy plan template if it exists -TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" -if [[ -f "$TEMPLATE" ]]; then +TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true +if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then cp "$TEMPLATE" "$IMPL_PLAN" echo "Copied plan template to $IMPL_PLAN" else - echo "Warning: Plan template not found at $TEMPLATE" + echo "Warning: Plan template not found" # Create a basic plan file if template doesn't exist touch "$IMPL_PLAN" fi # Output results if $JSON_MODE; then - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" + if has_jq; then + jq -cn \ + --arg feature_spec "$FEATURE_SPEC" \ + --arg impl_plan "$IMPL_PLAN" \ + --arg specs_dir "$FEATURE_DIR" \ + --arg branch "$CURRENT_BRANCH" \ + --arg has_git "$HAS_GIT" \ + '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}' + else + printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ + "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")" + fi else echo "FEATURE_SPEC: $FEATURE_SPEC" echo "IMPL_PLAN: $IMPL_PLAN" diff --git a/.specify/scripts/bash/update-agent-context.sh b/.specify/scripts/bash/update-agent-context.sh index fdebac6..ede8d77 100755 --- a/.specify/scripts/bash/update-agent-context.sh +++ b/.specify/scripts/bash/update-agent-context.sh @@ -30,12 +30,12 @@ # # 5. Multi-Agent Support # - Handles agent-specific file paths and naming conventions -# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Kiro CLI, or Antigravity +# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Antigravity or Generic # - Can update single agents or all existing agent files # - Creates default Claude file if no agent files exist # # Usage: ./update-agent-context.sh [agent_type] -# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli +# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic # Leave empty to update all existing agent files set -e @@ -53,7 +53,9 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" # Get all paths and variables from common functions -eval $(get_feature_paths) +_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +eval "$_paths_output" +unset _paths_output NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code AGENT_TYPE="${1:-}" @@ -66,16 +68,24 @@ CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" QWEN_FILE="$REPO_ROOT/QWEN.md" AGENTS_FILE="$REPO_ROOT/AGENTS.md" WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" +JUNIE_FILE="$REPO_ROOT/.junie/AGENTS.md" KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" QODER_FILE="$REPO_ROOT/QODER.md" -AMP_FILE="$REPO_ROOT/AGENTS.md" +# Amp, Kiro CLI, IBM Bob, and Pi all share AGENTS.md — use AGENTS_FILE to avoid +# updating the same file multiple times. +AMP_FILE="$AGENTS_FILE" SHAI_FILE="$REPO_ROOT/SHAI.md" -KIRO_FILE="$REPO_ROOT/AGENTS.md" +TABNINE_FILE="$REPO_ROOT/TABNINE.md" +KIRO_FILE="$AGENTS_FILE" AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md" -BOB_FILE="$REPO_ROOT/AGENTS.md" +BOB_FILE="$AGENTS_FILE" +VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md" +KIMI_FILE="$REPO_ROOT/KIMI.md" +TRAE_FILE="$REPO_ROOT/.trae/rules/AGENTS.md" +IFLOW_FILE="$REPO_ROOT/IFLOW.md" # Template file TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" @@ -109,6 +119,8 @@ log_warning() { # Cleanup function for temporary files cleanup() { local exit_code=$? + # Disarm traps to prevent re-entrant loop + trap - EXIT INT TERM rm -f /tmp/agent_update_*_$$ rm -f /tmp/manual_additions_$$ exit $exit_code @@ -473,7 +485,7 @@ update_existing_agent_file() { fi # Update timestamp - if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then + if [[ "$line" =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" else echo "$line" >> "$temp_file" @@ -604,158 +616,155 @@ update_specific_agent() { case "$agent_type" in claude) - update_agent_file "$CLAUDE_FILE" "Claude Code" + update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1 ;; gemini) - update_agent_file "$GEMINI_FILE" "Gemini CLI" + update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1 ;; copilot) - update_agent_file "$COPILOT_FILE" "GitHub Copilot" + update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1 ;; cursor-agent) - update_agent_file "$CURSOR_FILE" "Cursor IDE" + update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1 ;; qwen) - update_agent_file "$QWEN_FILE" "Qwen Code" + update_agent_file "$QWEN_FILE" "Qwen Code" || return 1 ;; opencode) - update_agent_file "$AGENTS_FILE" "opencode" + update_agent_file "$AGENTS_FILE" "opencode" || return 1 ;; codex) - update_agent_file "$AGENTS_FILE" "Codex CLI" + update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1 ;; windsurf) - update_agent_file "$WINDSURF_FILE" "Windsurf" + update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1 + ;; + junie) + update_agent_file "$JUNIE_FILE" "Junie" || return 1 ;; kilocode) - update_agent_file "$KILOCODE_FILE" "Kilo Code" + update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1 ;; auggie) - update_agent_file "$AUGGIE_FILE" "Auggie CLI" + update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1 ;; roo) - update_agent_file "$ROO_FILE" "Roo Code" + update_agent_file "$ROO_FILE" "Roo Code" || return 1 ;; codebuddy) - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" + update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1 ;; qodercli) - update_agent_file "$QODER_FILE" "Qoder CLI" + update_agent_file "$QODER_FILE" "Qoder CLI" || return 1 ;; amp) - update_agent_file "$AMP_FILE" "Amp" + update_agent_file "$AMP_FILE" "Amp" || return 1 ;; shai) - update_agent_file "$SHAI_FILE" "SHAI" + update_agent_file "$SHAI_FILE" "SHAI" || return 1 + ;; + tabnine) + update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1 ;; kiro-cli) - update_agent_file "$KIRO_FILE" "Kiro CLI" + update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1 ;; agy) - update_agent_file "$AGY_FILE" "Antigravity" + update_agent_file "$AGY_FILE" "Antigravity" || return 1 ;; bob) - update_agent_file "$BOB_FILE" "IBM Bob" + update_agent_file "$BOB_FILE" "IBM Bob" || return 1 + ;; + vibe) + update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1 + ;; + kimi) + update_agent_file "$KIMI_FILE" "Kimi Code" || return 1 + ;; + trae) + update_agent_file "$TRAE_FILE" "Trae" || return 1 + ;; + pi) + update_agent_file "$AGENTS_FILE" "Pi Coding Agent" || return 1 + ;; + iflow) + update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1 ;; generic) log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent." ;; *) log_error "Unknown agent type '$agent_type'" - log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic" + log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic" exit 1 ;; esac } +# Helper: skip non-existent files and files already updated (dedup by +# realpath so that variables pointing to the same file — e.g. AMP_FILE, +# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once). +# Uses a linear array instead of associative array for bash 3.2 compatibility. +# Note: defined at top level because bash 3.2 does not support true +# nested/local functions. _updated_paths, _found_agent, and _all_ok are +# initialised exclusively inside update_all_existing_agents so that +# sourcing this script has no side effects on the caller's environment. + +_update_if_new() { + local file="$1" name="$2" + [[ -f "$file" ]] || return 0 + local real_path + real_path=$(realpath "$file" 2>/dev/null || echo "$file") + local p + if [[ ${#_updated_paths[@]} -gt 0 ]]; then + for p in "${_updated_paths[@]}"; do + [[ "$p" == "$real_path" ]] && return 0 + done + fi + # Record the file as seen before attempting the update so that: + # (a) aliases pointing to the same path are not retried on failure + # (b) _found_agent reflects file existence, not update success + _updated_paths+=("$real_path") + _found_agent=true + update_agent_file "$file" "$name" +} + update_all_existing_agents() { - local found_agent=false - - # Check each possible agent file and update if it exists - if [[ -f "$CLAUDE_FILE" ]]; then - update_agent_file "$CLAUDE_FILE" "Claude Code" - found_agent=true - fi - - if [[ -f "$GEMINI_FILE" ]]; then - update_agent_file "$GEMINI_FILE" "Gemini CLI" - found_agent=true - fi - - if [[ -f "$COPILOT_FILE" ]]; then - update_agent_file "$COPILOT_FILE" "GitHub Copilot" - found_agent=true - fi - - if [[ -f "$CURSOR_FILE" ]]; then - update_agent_file "$CURSOR_FILE" "Cursor IDE" - found_agent=true - fi - - if [[ -f "$QWEN_FILE" ]]; then - update_agent_file "$QWEN_FILE" "Qwen Code" - found_agent=true - fi - - if [[ -f "$AGENTS_FILE" ]]; then - update_agent_file "$AGENTS_FILE" "Codex/opencode" - found_agent=true - fi - - if [[ -f "$WINDSURF_FILE" ]]; then - update_agent_file "$WINDSURF_FILE" "Windsurf" - found_agent=true - fi - - if [[ -f "$KILOCODE_FILE" ]]; then - update_agent_file "$KILOCODE_FILE" "Kilo Code" - found_agent=true - fi + _found_agent=false + _updated_paths=() + local _all_ok=true - if [[ -f "$AUGGIE_FILE" ]]; then - update_agent_file "$AUGGIE_FILE" "Auggie CLI" - found_agent=true - fi - - if [[ -f "$ROO_FILE" ]]; then - update_agent_file "$ROO_FILE" "Roo Code" - found_agent=true - fi + _update_if_new "$CLAUDE_FILE" "Claude Code" || _all_ok=false + _update_if_new "$GEMINI_FILE" "Gemini CLI" || _all_ok=false + _update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false + _update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false + _update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false + _update_if_new "$AGENTS_FILE" "Codex/opencode" || _all_ok=false + _update_if_new "$AMP_FILE" "Amp" || _all_ok=false + _update_if_new "$KIRO_FILE" "Kiro CLI" || _all_ok=false + _update_if_new "$BOB_FILE" "IBM Bob" || _all_ok=false + _update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false + _update_if_new "$JUNIE_FILE" "Junie" || _all_ok=false + _update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false + _update_if_new "$AUGGIE_FILE" "Auggie CLI" || _all_ok=false + _update_if_new "$ROO_FILE" "Roo Code" || _all_ok=false + _update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI" || _all_ok=false + _update_if_new "$SHAI_FILE" "SHAI" || _all_ok=false + _update_if_new "$TABNINE_FILE" "Tabnine CLI" || _all_ok=false + _update_if_new "$QODER_FILE" "Qoder CLI" || _all_ok=false + _update_if_new "$AGY_FILE" "Antigravity" || _all_ok=false + _update_if_new "$VIBE_FILE" "Mistral Vibe" || _all_ok=false + _update_if_new "$KIMI_FILE" "Kimi Code" || _all_ok=false + _update_if_new "$TRAE_FILE" "Trae" || _all_ok=false + _update_if_new "$IFLOW_FILE" "iFlow CLI" || _all_ok=false - if [[ -f "$CODEBUDDY_FILE" ]]; then - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" - found_agent=true - fi - - if [[ -f "$SHAI_FILE" ]]; then - update_agent_file "$SHAI_FILE" "SHAI" - found_agent=true - fi - - if [[ -f "$QODER_FILE" ]]; then - update_agent_file "$QODER_FILE" "Qoder CLI" - found_agent=true - fi - - if [[ -f "$KIRO_FILE" ]]; then - update_agent_file "$KIRO_FILE" "Kiro CLI" - found_agent=true - fi - - if [[ -f "$AGY_FILE" ]]; then - update_agent_file "$AGY_FILE" "Antigravity" - found_agent=true - fi - if [[ -f "$BOB_FILE" ]]; then - update_agent_file "$BOB_FILE" "IBM Bob" - found_agent=true - fi - # If no agent files exist, create a default Claude file - if [[ "$found_agent" == false ]]; then + if [[ "$_found_agent" == false ]]; then log_info "No existing agent files found, creating default Claude file..." - update_agent_file "$CLAUDE_FILE" "Claude Code" + update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1 fi + + [[ "$_all_ok" == true ]] } print_summary() { echo @@ -774,8 +783,7 @@ print_summary() { fi echo - - log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli]" + log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]" } #============================================================================== diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md index c67d914..4581e40 100644 --- a/.specify/templates/spec-template.md +++ b/.specify/templates/spec-template.md @@ -113,3 +113,16 @@ - **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"] - **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"] - **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"] + +## Assumptions + + + +- [Assumption about target users, e.g., "Users have stable internet connectivity"] +- [Assumption about scope boundaries, e.g., "Mobile support is out of scope for v1"] +- [Assumption about data/environment, e.g., "Existing authentication system will be reused"] +- [Dependency on existing system/service, e.g., "Requires access to the existing user profile API"]