From 7fed37014af416ada649aa762d7d4db260300077 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 3 Jun 2026 23:32:28 +0200 Subject: [PATCH 1/6] feat(skills): add argument-hint frontmatter on argument-taking skills (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five iikit skills that accept arguments — iikit-core, iikit-01-specify, iikit-bugfix, iikit-08-taskstoissues, iikit-clarify — now carry an argument-hint: field. Claude Code's slash-command UI surfaces the hint inline so users see the expected input shape without reading the SKILL body. Other agents ignore the unknown field per rule-frontmatter passthrough. Harvested from upstream spec-kit commit 10be4848 (PRs #2059, #1951). Co-Authored-By: Claude Opus 4.7 --- .claude/skills/iikit-01-specify/SKILL.md | 1 + .claude/skills/iikit-08-taskstoissues/SKILL.md | 1 + .claude/skills/iikit-bugfix/SKILL.md | 1 + .claude/skills/iikit-clarify/SKILL.md | 1 + .claude/skills/iikit-core/SKILL.md | 1 + 5 files changed, 5 insertions(+) diff --git a/.claude/skills/iikit-01-specify/SKILL.md b/.claude/skills/iikit-01-specify/SKILL.md index a5e6797..a91080d 100644 --- a/.claude/skills/iikit-01-specify/SKILL.md +++ b/.claude/skills/iikit-01-specify/SKILL.md @@ -3,6 +3,7 @@ name: iikit-01-specify description: >- Create a feature specification from a natural language description — generates user stories with Given/When/Then scenarios, functional requirements (FR-XXX), success criteria, and a quality checklist. Use when starting a new feature, writing a PRD, defining user stories, capturing acceptance criteria, or documenting requirements for a product idea. +argument-hint: "" license: MIT metadata: version: "1.6.4" diff --git a/.claude/skills/iikit-08-taskstoissues/SKILL.md b/.claude/skills/iikit-08-taskstoissues/SKILL.md index d93a292..7e0c10d 100644 --- a/.claude/skills/iikit-08-taskstoissues/SKILL.md +++ b/.claude/skills/iikit-08-taskstoissues/SKILL.md @@ -3,6 +3,7 @@ name: iikit-08-taskstoissues description: >- Convert tasks from tasks.md into GitHub Issues with labels and dependencies. Use when exporting work items to GitHub, setting up project boards, or assigning tasks to team members. +argument-hint: "[--dry-run]" license: MIT metadata: version: "1.6.4" diff --git a/.claude/skills/iikit-bugfix/SKILL.md b/.claude/skills/iikit-bugfix/SKILL.md index 04ce871..9b22126 100644 --- a/.claude/skills/iikit-bugfix/SKILL.md +++ b/.claude/skills/iikit-bugfix/SKILL.md @@ -3,6 +3,7 @@ name: iikit-bugfix description: >- Report a bug against an existing feature — creates a structured bugs.md record, generates fix tasks in tasks.md, and optionally imports from or creates GitHub issues. Use when fixing a bug, reporting a defect, importing a GitHub issue into the workflow, or triaging an error without running the full specification process. +argument-hint: "" license: MIT metadata: version: "1.6.4" diff --git a/.claude/skills/iikit-clarify/SKILL.md b/.claude/skills/iikit-clarify/SKILL.md index 6d40524..ef6939d 100644 --- a/.claude/skills/iikit-clarify/SKILL.md +++ b/.claude/skills/iikit-clarify/SKILL.md @@ -5,6 +5,7 @@ description: >- asks targeted questions with option tables, and writes answers back into the artifact's Clarifications section. Use when requirements are unclear, a plan has trade-off gaps, checklist thresholds feel wrong, test scenarios are imprecise, task dependencies seem off, or constitution principles are vague. +argument-hint: "[spec|plan|checklist|testify|tasks|constitution]" license: MIT metadata: version: "2.6.0" diff --git a/.claude/skills/iikit-core/SKILL.md b/.claude/skills/iikit-core/SKILL.md index 39b4261..d536a73 100644 --- a/.claude/skills/iikit-core/SKILL.md +++ b/.claude/skills/iikit-core/SKILL.md @@ -3,6 +3,7 @@ name: iikit-core description: >- Initialize an IIKit (Intent Integrity Kit) project, uninit (remove IIKit scaffolding before `tessl uninstall`), check IIKit feature progress, select the active IIKit feature, and display the IIKit workflow command reference. Use when starting a new IIKit project, running IIKit init or setup, uninstalling/removing/uninit-ing IIKit before running `tessl uninstall`, checking IIKit status, switching between IIKit features, looking up IIKit available commands and phases, or asking for help with the IIKit workflow. +argument-hint: "[init|status|use|uninit|help] [feature]" license: MIT metadata: version: "1.6.4" From 7b4fccda80d8b422db7a6ce8f73a212fa264bb5b Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 3 Jun 2026 23:35:17 +0200 Subject: [PATCH 2/6] feat(scripts): --paths-only on non-feature branches and --dry-run (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check-prerequisites.sh --paths-only (and --phase core) now skips the feature-branch gate, mirroring status_mode. Returns paths informationally on a non-feature branch with empty feature dirs, instead of exiting 1. Use cases: CI dry-runs, worktree-isolated read-only inspection, and feeding a planned /iikit-core doctor (#74) with structured paths. create-new-feature.sh --dry-run (and -DryRun in PowerShell) previews BRANCH_NAME / FEATURE_NUM / SPEC_FILE without writing — no branch checkout, no specs/NNN-*/ directory, no spec.md copy, no active-feature write. JSON output carries "dry_run": true. Harvested from upstream spec-kit commits 3227b966 (#2672) and b44ffc01 (#1998). Co-Authored-By: Claude Opus 4.7 --- .../scripts/bash/check-prerequisites.sh | 22 +++++++----- .../scripts/bash/create-new-feature.sh | 26 ++++++++++++-- .../powershell/check-prerequisites.ps1 | 16 +++++---- .../scripts/powershell/create-new-feature.ps1 | 31 ++++++++++++++-- tests/bash/check-prerequisites.bats | 19 ++++++++++ tests/bash/create-new-feature.bats | 35 +++++++++++++++++++ 6 files changed, 128 insertions(+), 21 deletions(-) diff --git a/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh b/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh index c0e74c5..dbc543a 100755 --- a/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh +++ b/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh @@ -205,10 +205,16 @@ has_git && HAS_GIT="true" CURRENT_BRANCH=$(get_current_branch) # Check feature branch (may set SPECIFY_FEATURE, may exit 2 for needs_selection) -STATUS_NO_FEATURE=false +NO_FEATURE_BRANCH=false BRANCH_EXIT=0 -if [[ "$P_EXTRAS" == *"status_mode"* ]]; then - # Status mode: suppress stderr from branch validation (info goes in JSON only) +# paths_only and status_mode tolerate non-feature branches — they emit informational +# output rather than gating on branch shape. +SOFT_BRANCH_MODE=false +if [[ "$P_EXTRAS" == *"status_mode"* ]] || [[ "$P_EXTRAS" == *"paths_only"* ]]; then + SOFT_BRANCH_MODE=true +fi +if $SOFT_BRANCH_MODE; then + # Suppress stderr from branch validation (info goes in JSON only) check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" 2>/dev/null || BRANCH_EXIT=$? elif [[ "$PHASE" == "00" ]]; then # Constitution phase: skip feature branch validation entirely @@ -227,8 +233,8 @@ if [[ $BRANCH_EXIT -eq 2 ]]; then fi exit 2 elif [[ $BRANCH_EXIT -ne 0 ]]; then - if [[ "$P_EXTRAS" == *"status_mode"* ]]; then - # Status mode: no feature branch is informational, not fatal + if $SOFT_BRANCH_MODE; then + # No feature branch is informational, not fatal FEATURE_DIR="" FEATURE_SPEC="" IMPL_PLAN="" @@ -237,14 +243,14 @@ elif [[ $BRANCH_EXIT -ne 0 ]]; then DATA_MODEL="" QUICKSTART="" CONTRACTS_DIR="" - STATUS_NO_FEATURE=true + NO_FEATURE_BRANCH=true else exit 1 fi fi # Get all feature paths (uses SPECIFY_FEATURE if set by check_feature_branch) -if ! $STATUS_NO_FEATURE; then +if ! $NO_FEATURE_BRANCH; then eval $(get_feature_paths) # Override paths if --project-root was specified @@ -360,7 +366,7 @@ if [[ "$P_EXTRAS" == *"status_mode"* ]]; then if [[ -n "$FEATURE_DIR" && -d "$FEATURE_DIR" ]]; then local_feature=$(basename "$FEATURE_DIR") FEATURE_STAGE=$(get_feature_stage "$REPO_ROOT" "$local_feature") - elif $STATUS_NO_FEATURE; then + elif $NO_FEATURE_BRANCH; then FEATURE_STAGE="unknown" fi diff --git a/.claude/skills/iikit-core/scripts/bash/create-new-feature.sh b/.claude/skills/iikit-core/scripts/bash/create-new-feature.sh index 807af09..07d9552 100755 --- a/.claude/skills/iikit-core/scripts/bash/create-new-feature.sh +++ b/.claude/skills/iikit-core/scripts/bash/create-new-feature.sh @@ -6,6 +6,7 @@ JSON_MODE=false SHORT_NAME="" BRANCH_NUMBER="" SKIP_BRANCH=false +DRY_RUN=false ARGS=() i=1 while [ $i -le $# ]; do @@ -17,6 +18,9 @@ while [ $i -le $# ]; do --skip-branch) SKIP_BRANCH=true ;; + --dry-run) + DRY_RUN=true + ;; --short-name) if [ $((i + 1)) -gt $# ]; then echo 'Error: --short-name requires a value' >&2 @@ -44,19 +48,21 @@ while [ $i -le $# ]; do BRANCH_NUMBER="$next_arg" ;; --help|-h) - echo "Usage: $0 [--json] [--short-name ] [--number N] [--skip-branch] " + echo "Usage: $0 [--json] [--short-name ] [--number N] [--skip-branch] [--dry-run] " 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 " --skip-branch Create feature directory without creating a git branch" + echo " --dry-run Preview the branch name and feature number without writing" 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 --skip-branch 'Fix bug on existing branch'" + echo " $0 --dry-run 'Preview without creating anything'" exit 0 ;; *) @@ -265,6 +271,22 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" fi +FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" +SPEC_FILE="$FEATURE_DIR/spec.md" + +if [ "$DRY_RUN" = true ]; then + if $JSON_MODE; then + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_DIR":"%s","FEATURE_NUM":"%s","HAS_GIT":%s,"dry_run":true}\n' \ + "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_DIR" "$FEATURE_NUM" "$HAS_GIT" + else + echo "[specify] DRY RUN — no branch, directory, or spec.md will be created" + echo "BRANCH_NAME: $BRANCH_NAME" + echo "SPEC_FILE: $SPEC_FILE" + echo "FEATURE_NUM: $FEATURE_NUM" + fi + exit 0 +fi + if [ "$SKIP_BRANCH" = true ]; then >&2 echo "[specify] Skipping branch creation (--skip-branch). Feature directory: $BRANCH_NAME" elif [ "$HAS_GIT" = true ]; then @@ -273,12 +295,10 @@ else >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" fi -FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" mkdir -p "$FEATURE_DIR" # Template path relative to script location (works for both .tessl and .claude installs) TEMPLATE="$SCRIPT_DIR/../../templates/spec-template.md" -SPEC_FILE="$FEATURE_DIR/spec.md" if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi # Set the SPECIFY_FEATURE environment variable diff --git a/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 b/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 index fa4ef7a..8f8ecef 100644 --- a/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 +++ b/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 @@ -119,7 +119,9 @@ if ($ProjectRoot) { } $hasGit = Test-HasGit $currentBranch = Get-CurrentBranch -$statusNoFeature = $false +$noFeatureBranch = $false +# paths_only and status_mode tolerate non-feature branches. +$softBranchMode = ($cfg.Extras -match 'status_mode') -or ($cfg.Extras -match 'paths_only') $branchResult = Test-FeatureBranch -Branch $currentBranch -HasGit $hasGit if ($branchResult -eq "NEEDS_SELECTION") { $featuresJson = Get-FeaturesJson @@ -131,8 +133,8 @@ if ($branchResult -eq "NEEDS_SELECTION") { } exit 2 } elseif ($branchResult -eq "ERROR") { - if ($cfg.Extras -match 'status_mode') { - $statusNoFeature = $true + if ($softBranchMode) { + $noFeatureBranch = $true $paths = [PSCustomObject]@{ REPO_ROOT = $repoRoot CURRENT_BRANCH = $currentBranch @@ -151,8 +153,8 @@ if ($branchResult -eq "NEEDS_SELECTION") { } } -# Get all feature paths (skip if status mode with no feature) -if (-not $statusNoFeature) { +# Get all feature paths (skip if soft branch mode with no feature) +if (-not $noFeatureBranch) { $paths = Get-FeaturePathsEnv # Override paths if -ProjectRoot was specified @@ -269,7 +271,7 @@ if ($cfg.Extras -match 'status_mode') { if ($paths.FEATURE_DIR -and (Test-Path $paths.FEATURE_DIR -PathType Container -ErrorAction SilentlyContinue)) { $localFeature = Split-Path $paths.FEATURE_DIR -Leaf $featureStage = Get-FeatureStage -RepoRoot $repoRoot -Feature $localFeature - } elseif ($statusNoFeature) { + } elseif ($noFeatureBranch) { $featureStage = 'no-feature' } @@ -290,7 +292,7 @@ if ($cfg.Extras -match 'status_mode') { if (-not $aConstitution) { $nextStep = '/iikit-00-constitution' $clearBefore = $false - } elseif ($statusNoFeature -or -not $paths.FEATURE_DIR -or -not (Test-Path $paths.FEATURE_DIR -PathType Container -ErrorAction SilentlyContinue)) { + } elseif ($noFeatureBranch -or -not $paths.FEATURE_DIR -or -not (Test-Path $paths.FEATURE_DIR -PathType Container -ErrorAction SilentlyContinue)) { $nextStep = '/iikit-01-specify ' $clearBefore = $false } elseif (-not $vSpec) { diff --git a/.claude/skills/iikit-core/scripts/powershell/create-new-feature.ps1 b/.claude/skills/iikit-core/scripts/powershell/create-new-feature.ps1 index 394dff4..954bf33 100644 --- a/.claude/skills/iikit-core/scripts/powershell/create-new-feature.ps1 +++ b/.claude/skills/iikit-core/scripts/powershell/create-new-feature.ps1 @@ -12,6 +12,8 @@ param( [int]$Number = 0, [Alias('b')] [switch]$SkipBranch, + [Alias('d')] + [switch]$DryRun, [Alias('h')] [switch]$Help ) @@ -19,19 +21,21 @@ $ErrorActionPreference = 'Stop' # Show help if requested if ($Help) { - Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName ] [-Number N] [-SkipBranch] " + Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName ] [-Number N] [-SkipBranch] [-DryRun] " Write-Host "" Write-Host "Options:" Write-Host " -Json Output in JSON format" Write-Host " -ShortName Provide a custom short name (2-4 words) for the branch" Write-Host " -Number N Specify branch number manually (overrides auto-detection)" Write-Host " -SkipBranch Create feature directory without creating a git branch" + Write-Host " -DryRun Preview the branch name and feature number without writing" Write-Host " -Help Show this help message" Write-Host "" Write-Host "Examples:" Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'" Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'" Write-Host " ./create-new-feature.ps1 -SkipBranch 'Fix bug on existing branch'" + Write-Host " ./create-new-feature.ps1 -DryRun 'Preview without creating anything'" exit 0 } @@ -257,6 +261,29 @@ if ($branchName.Length -gt $maxBranchLength) { Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)" } +$featureDir = Join-Path $specsDir $branchName +$specFile = Join-Path $featureDir 'spec.md' + +if ($DryRun) { + if ($Json) { + $obj = [PSCustomObject]@{ + BRANCH_NAME = $branchName + SPEC_FILE = $specFile + FEATURE_DIR = $featureDir + FEATURE_NUM = $featureNum + HAS_GIT = $hasGit + dry_run = $true + } + $obj | ConvertTo-Json -Compress + } else { + Write-Output "[specify] DRY RUN — no branch, directory, or spec.md will be created" + Write-Output "BRANCH_NAME: $branchName" + Write-Output "SPEC_FILE: $specFile" + Write-Output "FEATURE_NUM: $featureNum" + } + exit 0 +} + if ($SkipBranch) { Write-Warning "[specify] Skipping branch creation (-SkipBranch). Feature directory: $branchName" } elseif ($hasGit) { @@ -269,12 +296,10 @@ if ($SkipBranch) { Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName" } -$featureDir = Join-Path $specsDir $branchName New-Item -ItemType Directory -Path $featureDir -Force | Out-Null # Template path relative to script location (works for both .tessl and .claude installs) $template = Join-Path $PSScriptRoot '..\..\templates\spec-template.md' -$specFile = Join-Path $featureDir 'spec.md' if (Test-Path $template) { Copy-Item $template $specFile -Force } else { diff --git a/tests/bash/check-prerequisites.bats b/tests/bash/check-prerequisites.bats index 7719071..c65a6e5 100644 --- a/tests/bash/check-prerequisites.bats +++ b/tests/bash/check-prerequisites.bats @@ -43,6 +43,25 @@ teardown() { [[ "$status" -eq 0 ]] } +@test "check-prerequisites: --paths-only succeeds on non-feature branch with no features" { + unset SPECIFY_FEATURE + rm -rf specs + mkdir -p specs + + run "$CHECK_SCRIPT" --paths-only --json + [[ "$status" -eq 0 ]] + assert_contains "$output" '"FEATURE_DIR"' +} + +@test "check-prerequisites: --phase core succeeds on non-feature branch (gitflow-agnostic)" { + unset SPECIFY_FEATURE + rm -rf specs + mkdir -p specs + + run "$CHECK_SCRIPT" --phase core --json + [[ "$status" -eq 0 ]] +} + # ============================================================================= # Validation mode tests # ============================================================================= diff --git a/tests/bash/create-new-feature.bats b/tests/bash/create-new-feature.bats index 781c439..3bbde71 100644 --- a/tests/bash/create-new-feature.bats +++ b/tests/bash/create-new-feature.bats @@ -169,6 +169,41 @@ teardown() { [[ ${#branch_name} -le 244 ]] } +# ============================================================================= +# Dry-run tests +# ============================================================================= + +@test "create-new-feature: --dry-run does not create directory" { + original_branch=$(git branch --show-current) + "$CREATE_SCRIPT" --json --dry-run "Preview only feature" >/dev/null + + # No specs/ children should exist + [[ -z "$(ls -A specs 2>/dev/null)" ]] +} + +@test "create-new-feature: --dry-run does not create git branch" { + original_branch=$(git branch --show-current) + "$CREATE_SCRIPT" --json --dry-run "Preview only feature" >/dev/null + + current_branch=$(git branch --show-current) + [[ "$current_branch" == "$original_branch" ]] +} + +@test "create-new-feature: --dry-run emits dry_run=true in JSON" { + result=$("$CREATE_SCRIPT" --json --dry-run "Preview only feature") + + assert_contains "$result" '"dry_run":true' + assert_contains "$result" '"BRANCH_NAME"' + assert_contains "$result" '"FEATURE_NUM"' +} + +@test "create-new-feature: --dry-run respects --short-name and --number" { + result=$("$CREATE_SCRIPT" --json --dry-run --short-name "user-auth" --number 7 "ignored desc") + + assert_contains "$result" '"FEATURE_NUM":"007"' + assert_contains "$result" "user-auth" +} + # ============================================================================= # Bug regression tests (from e2e test findings) # ============================================================================= From 1157d3931a6e8dbe6640c10c99ba567416eaa037 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 3 Jun 2026 23:36:01 +0200 Subject: [PATCH 3/6] feat(scripts): recognize gitflow branch prefixes in feature detection (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The active-feature cascade now recognizes both `NNN-name` (standard) and `/NNN-name` (gitflow — feat/001-user-auth, fix/042-null-deref, chore/007-foo). New extract_feature_id / Get-FeatureIdFromBranch helper strips the prefix; get_current_branch, check_feature_branch, find_feature_dir_by_prefix, and create-new-feature's highest-number scan all route through it. Teams on gitflow conventions no longer fall through to "single feature auto-select" or hit a "not on a feature branch" error. Cascade semantics unchanged. Also rolls the three CHANGELOG entries for this harvest set (#75 #76 #77) into Unreleased. Harvested from upstream spec-kit commit 4687c33b (PR #2202). Co-Authored-By: Claude Opus 4.7 --- .../skills/iikit-core/scripts/bash/common.sh | 42 +++++++++++++--- .../scripts/bash/create-new-feature.sh | 6 ++- .../iikit-core/scripts/powershell/common.ps1 | 37 ++++++++++++-- .../scripts/powershell/create-new-feature.ps1 | 4 +- CHANGELOG.md | 3 ++ tests/bash/common.bats | 50 +++++++++++++++++++ tests/bash/create-new-feature.bats | 23 +++++++++ 7 files changed, 151 insertions(+), 14 deletions(-) diff --git a/.claude/skills/iikit-core/scripts/bash/common.sh b/.claude/skills/iikit-core/scripts/bash/common.sh index b53f1f1..78f6c62 100755 --- a/.claude/skills/iikit-core/scripts/bash/common.sh +++ b/.claude/skills/iikit-core/scripts/bash/common.sh @@ -142,6 +142,24 @@ list_features_json() { printf ']' } +# Extract the feature id from a branch name. +# Accepts: +# NNN-name -> NNN-name (standard) +# prefix/NNN-name -> NNN-name (gitflow, e.g. feat/001-auth, fix/042-bug) +# Anything else returns the empty string and exit 1. +extract_feature_id() { + local branch="$1" + if [[ "$branch" =~ ^[0-9]{3}- ]]; then + echo "$branch" + return 0 + fi + if [[ "$branch" =~ ^[^/]+/([0-9]{3}-.+)$ ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + return 1 +} + # Get repository root, with fallback for non-git repositories get_repo_root() { if git rev-parse --show-toplevel >/dev/null 2>&1; then @@ -162,13 +180,15 @@ get_current_branch() { return fi - # 2. Check git branch — if it matches a feature pattern (NNN-*), use it + # 2. Check git branch — if it matches a feature pattern (NNN-* or + # prefix/NNN-* gitflow style), use the extracted feature id. # This ensures switching branches always picks up the correct feature, # even when a stale active-feature file points elsewhere. local git_branch git_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) && [[ -n "$git_branch" ]] && { - if [[ "$git_branch" =~ ^[0-9]{3}- ]]; then - echo "$git_branch" + local feature_id + if feature_id=$(extract_feature_id "$git_branch"); then + echo "$feature_id" return fi @@ -238,9 +258,12 @@ check_feature_branch() { return 0 fi - # Accept if branch matches NNN- pattern (standard feature branch) - if [[ "$branch" =~ ^[0-9]{3}- ]]; then - write_active_feature "$branch" + # Accept NNN- or prefix/NNN- (gitflow) patterns; store the extracted feature id + local feature_id + if feature_id=$(extract_feature_id "$branch"); then + write_active_feature "$feature_id" + # Export so downstream get_feature_paths uses the extracted id + export SPECIFY_FEATURE="$feature_id" return 0 fi @@ -287,11 +310,18 @@ get_feature_dir() { echo "$1/specs/$2"; } # Find feature directory by numeric prefix instead of exact branch match # This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) +# Accepts gitflow-prefixed branches (e.g. feat/004-foo) by extracting the feature id first. find_feature_dir_by_prefix() { local repo_root="$1" local branch_name="$2" local specs_dir="$repo_root/specs" + # Strip gitflow prefix if present (feat/004-foo -> 004-foo) + local feature_id + if feature_id=$(extract_feature_id "$branch_name"); then + branch_name="$feature_id" + fi + # 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 diff --git a/.claude/skills/iikit-core/scripts/bash/create-new-feature.sh b/.claude/skills/iikit-core/scripts/bash/create-new-feature.sh index 07d9552..f29b9e0 100755 --- a/.claude/skills/iikit-core/scripts/bash/create-new-feature.sh +++ b/.claude/skills/iikit-core/scripts/bash/create-new-feature.sh @@ -127,8 +127,10 @@ get_highest_from_branches() { # Skip current branch (avoid self-inflation) [ "$clean_branch" = "$current_branch" ] && continue - if echo "$clean_branch" | grep -qE '^[0-9]{3}-'; then - number=$(echo "$clean_branch" | grep -oE '^[0-9]{3}' || echo "0") + # Match NNN-* (standard) and prefix/NNN-* (gitflow) + if echo "$clean_branch" | grep -qE '^([^/]+/)?[0-9]{3}-'; then + number=$(echo "$clean_branch" | grep -oE '[0-9]{3}-' | head -1 | grep -oE '^[0-9]{3}') + [ -z "$number" ] && number=0 number=$((10#$number)) if [ "$number" -gt "$highest" ]; then highest=$number diff --git a/.claude/skills/iikit-core/scripts/powershell/common.ps1 b/.claude/skills/iikit-core/scripts/powershell/common.ps1 index a606c13..b8bc001 100644 --- a/.claude/skills/iikit-core/scripts/powershell/common.ps1 +++ b/.claude/skills/iikit-core/scripts/powershell/common.ps1 @@ -93,6 +93,21 @@ function Get-FeaturesJson { return ($features | ConvertTo-Json -Compress -AsArray) } +function Get-FeatureIdFromBranch { + # Extract the feature id from a branch name. + # Accepts NNN-name and prefix/NNN-name (gitflow) shapes. + # Returns the empty string when no feature id can be extracted. + param([string]$Branch) + + if ($Branch -match '^[0-9]{3}-') { + return $Branch + } + if ($Branch -match '^[^/]+/([0-9]{3}-.+)$') { + return $Matches[1] + } + return '' +} + function Get-RepoRoot { try { $result = git rev-parse --show-toplevel 2>$null @@ -121,10 +136,15 @@ function Get-CurrentBranch { return $env:SPECIFY_FEATURE } - # 3. Check git branch if available + # 3. Check git branch if available. If it's NNN-* or prefix/NNN-* (gitflow), + # return the extracted feature id; otherwise return the raw branch. try { $result = git rev-parse --abbrev-ref HEAD 2>$null if ($LASTEXITCODE -eq 0) { + $featureId = Get-FeatureIdFromBranch -Branch $result + if ($featureId) { + return $featureId + } return $result } } catch { @@ -179,9 +199,11 @@ function Test-FeatureBranch { return "OK" } - # Accept if branch matches NNN- pattern (standard feature branch) - if ($Branch -match '^[0-9]{3}-') { - Write-ActiveFeature -Feature $Branch + # Accept NNN- (standard) or prefix/NNN- (gitflow); store the extracted feature id + $featureId = Get-FeatureIdFromBranch -Branch $Branch + if ($featureId) { + Write-ActiveFeature -Feature $featureId + $env:SPECIFY_FEATURE = $featureId return "OK" } @@ -225,6 +247,7 @@ function Get-FeatureDir { # Find feature directory by numeric prefix instead of exact branch match # This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) +# Accepts gitflow-prefixed branches (e.g. feat/004-foo) by extracting the feature id first. function Find-FeatureDirByPrefix { param( [string]$RepoRoot, @@ -233,6 +256,12 @@ function Find-FeatureDirByPrefix { $specsDir = Join-Path $RepoRoot "specs" + # Strip gitflow prefix if present (feat/004-foo -> 004-foo) + $featureId = Get-FeatureIdFromBranch -Branch $BranchName + if ($featureId) { + $BranchName = $featureId + } + # Extract numeric prefix from branch (e.g., "004" from "004-whatever") if ($BranchName -notmatch '^(\d{3})-') { # If branch doesn't have numeric prefix, fall back to exact match diff --git a/.claude/skills/iikit-core/scripts/powershell/create-new-feature.ps1 b/.claude/skills/iikit-core/scripts/powershell/create-new-feature.ps1 index 954bf33..c57661b 100644 --- a/.claude/skills/iikit-core/scripts/powershell/create-new-feature.ps1 +++ b/.claude/skills/iikit-core/scripts/powershell/create-new-feature.ps1 @@ -97,8 +97,8 @@ function Get-HighestNumberFromBranches { # Clean branch name: remove leading markers and remote prefixes $cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', '' - # Extract feature number if branch matches pattern ###-* - if ($cleanBranch -match '^(\d+)-') { + # Extract feature number from NNN-* (standard) or prefix/NNN-* (gitflow) + if ($cleanBranch -match '^(?:[^/]+/)?(\d{3})-') { $num = [int]$matches[1] if ($num -gt $highest) { $highest = $num } } diff --git a/CHANGELOG.md b/CHANGELOG.md index add0f5f..8c95fb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - **Tessl install path moved from `.tessl/tiles/` to `.tessl/plugins/`** (#80): Tessl CLI 0.81+ installs tiles to `.tessl/plugins///` instead of `.tessl/tiles///`. Every hardcoded reference in the repo (CI workflows, gh-aw policy-reviewer prompts, every `iikit-*/SKILL.md` invoking a script, pre/post-commit hooks, dev-time symlink, source/tile test paths) was updated to the new path. Without this fix, CI's E2E install test fails on every PR, the OpenAI gh-aw reviewer posts `Policy load failed` `CHANGES_REQUESTED` on every PR, and end-users on Tessl 0.81+ get "No such file or directory" when invoking any iikit skill. Mechanical rename only — no behavior change. ### Features +- **`argument-hint:` frontmatter on argument-taking skills** (#75): The five iikit skills that accept arguments — `iikit-core`, `iikit-01-specify`, `iikit-bugfix`, `iikit-08-taskstoissues`, `iikit-clarify` — now carry an `argument-hint:` frontmatter field. Claude Code's slash-command UI surfaces the hint inline so users see the expected input shape without reading the SKILL body. Harvested from upstream spec-kit commit `10be4848` (PRs #2059, #1951). No runtime cost; other agents ignore the unknown field per `rule-frontmatter` passthrough. +- **`--paths-only` works on non-feature branches; `--dry-run` for create-new-feature** (#76): `check-prerequisites.sh --paths-only` (and its equivalent `--phase core`) now emits paths without the feature-branch gate — useful for CI dry-runs, worktree-isolated read-only inspection, and feeding the planned `/iikit-core doctor` (#74) with structured path data. PowerShell mirror updated. `create-new-feature.sh --dry-run` (and `-DryRun` in PowerShell) previews `BRANCH_NAME` / `FEATURE_NUM` / `SPEC_FILE` without creating the git branch, the `specs/NNN-*/` directory, or `spec.md`. JSON output carries `"dry_run": true`. Harvested from upstream spec-kit commits `3227b966` (#2672) and `b44ffc01` (#1998). +- **Gitflow branch prefixes in feature detection** (#77): The active-feature cascade now recognizes both `NNN-name` (standard) and `/NNN-name` (gitflow, e.g. `feat/001-user-auth`, `fix/042-null-deref`). New `extract_feature_id` / `Get-FeatureIdFromBranch` helper strips the prefix; `get_current_branch`, `check_feature_branch`, `find_feature_dir_by_prefix`, and the `create-new-feature.sh` highest-number scan all route through it. Teams on gitflow conventions no longer fall through to "single feature auto-select" or hit an error. Cascade semantics unchanged. Harvested from upstream spec-kit commit `4687c33b` (PR #2202). - **Canonical `## Step N — Title` headings across all iikit skills** (#86): Every `iikit-*/SKILL.md` now uses the flat `## Step N — Descriptive Title` form mandated by `jbaruch/coding-policy: skill-authoring`. Each skill has the appropriate execution-mode preamble after its H1 (sequential workflows get `Process steps in order. Do not skip ahead.`; `iikit-core` gets the action-router preamble). Parent `## Execution Flow` / `## Execution Steps` headings removed. Sub-step decimals flattened: iikit-04-testify's `#### 2.1/2.2` reference sections become bold labels under Step 2; iikit-06-analyze's `### 5b` promoted to its own Step 6 (existing 6, 7 renumbered to 7, 8); iikit-07-implement's `### 2.1–2.4` constraints absorbed into Step 3 body and the existing 3→6 numbering gap closed (now Steps 1–9); iikit-bugfix's `### 2a/2b` branches merged into one Step 2 with bold branch labels. iikit-00-constitution's numbered list under `## Execution Flow` converted to 10 flat Step headings. iikit-core restructured: each subcommand (init, status, use, uninit, help) now lives under its own `## Step N — name` heading with internal procedure as numbered list bullets in the body. No behavior change; affects 88 step headings across 12 skills. - **Re-validate checklists after `/iikit-clarify`** (#73): When the clarified target is `spec.md` and one or more `checklists/*.md` files are present, the skill now re-evaluates each item against the updated spec — unchecking items invalidated by the clarification (`[Stale]` tag with one-line note), checking off `[Gap]` items the clarification resolves, and writing a regressions/resolved/unchanged diff into the spec's `## Clarifications` session. Non-spec targets (plan, tasks, testify, checklist, constitution) skip the step. Closes the stale-state hole where a clarification could leave a previously-green checklist out of sync with the spec it scored. Harvested from upstream spec-kit PR #2715. - **Linked-worktree / submodule hook install** (#67): `/iikit-core init` and `/iikit-core uninit` now resolve the hooks directory via `git rev-parse --git-path hooks` instead of assuming `$PROJECT_ROOT/.git/hooks`. Running init/uninit from a linked worktree or submodule (where `.git` is a file pointing at the real gitdir) now lands hooks in — and removes them from — the actual gitdir's `hooks/` directory (the main repo's `.git/hooks/` for worktrees, `.git/modules//hooks/` for submodules), matching the place git invokes hooks from. `install_hook` / `Install-IIKitHook` and the `pre-commit.d/` extension point share a single resolved path so they stay in lockstep across every layout. Repo detection switched from `-d .git` / `Test-Path .git` to `git rev-parse --is-inside-work-tree`. JSON contract unchanged; the human-readable status line for `pre-commit.d/` now prints the resolved path so worktree users see where the extension dir actually lives. BATS + Pester coverage for both checkouts and worktree-initiated runs. diff --git a/tests/bash/common.bats b/tests/bash/common.bats index 58d9ac8..e8b976f 100644 --- a/tests/bash/common.bats +++ b/tests/bash/common.bats @@ -72,6 +72,41 @@ teardown() { [[ "$status" -eq 0 ]] } +@test "extract_feature_id: returns NNN-name for standard branch" { + result=$(extract_feature_id "001-user-auth") + [[ "$result" == "001-user-auth" ]] +} + +@test "extract_feature_id: strips feat/ gitflow prefix" { + result=$(extract_feature_id "feat/042-null-deref") + [[ "$result" == "042-null-deref" ]] +} + +@test "extract_feature_id: strips fix/ gitflow prefix" { + result=$(extract_feature_id "fix/005-bug") + [[ "$result" == "005-bug" ]] +} + +@test "extract_feature_id: returns empty for non-feature branch" { + run extract_feature_id "main" + [[ "$status" -ne 0 ]] +} + +@test "extract_feature_id: returns empty for prefix without NNN-" { + run extract_feature_id "feat/no-number" + [[ "$status" -ne 0 ]] +} + +@test "check_feature_branch: accepts feat/NNN- gitflow pattern" { + run check_feature_branch "feat/001-test-feature" "true" + [[ "$status" -eq 0 ]] +} + +@test "check_feature_branch: accepts fix/NNN- gitflow pattern" { + run check_feature_branch "fix/042-some-bug" "true" + [[ "$status" -eq 0 ]] +} + @test "check_feature_branch: accepts SPECIFY_FEATURE override" { export SPECIFY_FEATURE="manual-override" run check_feature_branch "main" "true" @@ -107,6 +142,21 @@ teardown() { assert_contains "$result" "004-original-feature" } +@test "find_feature_dir_by_prefix: resolves gitflow feat/NNN- branch" { + mkdir -p specs/007-gitflow-feature + + result=$(find_feature_dir_by_prefix "$TEST_DIR" "feat/007-gitflow-feature") + assert_contains "$result" "007-gitflow-feature" +} + +@test "find_feature_dir_by_prefix: resolves gitflow fix/NNN- branch by prefix only" { + mkdir -p specs/008-existing-feature + + # Caller may be on fix/008-different-name; we still resolve by numeric prefix + result=$(find_feature_dir_by_prefix "$TEST_DIR" "fix/008-different-name") + assert_contains "$result" "008-existing-feature" +} + @test "find_feature_dir_by_prefix: returns exact path for non-prefixed branch" { result=$(find_feature_dir_by_prefix "$TEST_DIR" "main") [[ "$result" == "$TEST_DIR/specs/main" ]] diff --git a/tests/bash/create-new-feature.bats b/tests/bash/create-new-feature.bats index 3bbde71..67c157e 100644 --- a/tests/bash/create-new-feature.bats +++ b/tests/bash/create-new-feature.bats @@ -204,6 +204,29 @@ teardown() { assert_contains "$result" "user-auth" } +# ============================================================================= +# Gitflow branch prefix tests +# ============================================================================= + +@test "create-new-feature: counts feat/NNN-* gitflow branches when picking next number" { + git checkout -b feat/003-existing >/dev/null 2>&1 + git checkout -b temp-branch >/dev/null 2>&1 + + result=$("$CREATE_SCRIPT" --json --dry-run "New feature") + + # 003 exists on feat/003-existing; next should be 004 + assert_contains "$result" '"FEATURE_NUM":"004"' +} + +@test "create-new-feature: counts fix/NNN-* gitflow branches when picking next number" { + git checkout -b fix/012-some-bug >/dev/null 2>&1 + git checkout -b temp-branch >/dev/null 2>&1 + + result=$("$CREATE_SCRIPT" --json --dry-run "New feature") + + assert_contains "$result" '"FEATURE_NUM":"013"' +} + # ============================================================================= # Bug regression tests (from e2e test findings) # ============================================================================= From 82d256e0d3be09dc16ce31971a450375cccf9cea Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 4 Jun 2026 00:30:17 +0200 Subject: [PATCH 4/6] fix(scripts): soft branch mode swallows needs_selection; PS cascade + Pester coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review-driven fixes to PR #89: 1. Soft branch mode (paths_only, status_mode) was still exiting 2 with needs_selection when multiple feature dirs existed. The caller wants informational paths, not a selection prompt. Both bash and PowerShell now fall through to empty-paths in soft mode regardless of which non-zero branch-validation status the caller hit. 2. PowerShell Get-CurrentBranch read .specify/active-feature before consulting the git branch, so a gitflow branch (feat/001-foo) could be masked by a stale sticky file — diverging from bash and undermining the gitflow support added in this PR. Cascade reordered to match bash: SPECIFY_FEATURE → git branch (with extract) → active-feature → raw branch → non-git fallbacks. 3. PowerShell mirrors of the new behavior had no Pester coverage. Added tests for: Get-FeatureIdFromBranch (standard, feat/, fix/, rejection); Test-FeatureBranch accepting gitflow shapes; Find-FeatureDirByPrefix resolving gitflow branches; -DryRun (no writes, dry_run=true JSON, respects -ShortName/-Number); gitflow numbering in get-highest-from- branches; -PathsOnly / -Phase core / -Phase status not exiting 2 under multi-feature non-feature-branch workspaces. Bats also gets the corresponding soft-mode regression cases. Co-Authored-By: Claude Opus 4.7 --- .../scripts/bash/check-prerequisites.sh | 6 ++- .../powershell/check-prerequisites.ps1 | 6 ++- .../iikit-core/scripts/powershell/common.ps1 | 41 +++++++++----- tests/bash/check-prerequisites.bats | 22 ++++++++ .../powershell/check-prerequisites.Tests.ps1 | 47 ++++++++++++++++ tests/powershell/common.Tests.ps1 | 54 +++++++++++++++++++ tests/powershell/create-new-feature.Tests.ps1 | 52 ++++++++++++++++++ 7 files changed, 210 insertions(+), 18 deletions(-) diff --git a/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh b/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh index dbc543a..3e34667 100755 --- a/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh +++ b/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh @@ -222,7 +222,7 @@ elif [[ "$PHASE" == "00" ]]; then else check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || BRANCH_EXIT=$? fi -if [[ $BRANCH_EXIT -eq 2 ]]; then +if [[ $BRANCH_EXIT -eq 2 ]] && ! $SOFT_BRANCH_MODE; then # Multiple features, no active one — caller should present picker FEATURES_JSON=$(list_features_json) if $JSON_MODE; then @@ -234,7 +234,9 @@ if [[ $BRANCH_EXIT -eq 2 ]]; then exit 2 elif [[ $BRANCH_EXIT -ne 0 ]]; then if $SOFT_BRANCH_MODE; then - # No feature branch is informational, not fatal + # Soft modes (paths_only, status_mode) treat both "no feature branch" + # and "multiple features, none active" as informational — emit empty + # path JSON instead of gating. FEATURE_DIR="" FEATURE_SPEC="" IMPL_PLAN="" diff --git a/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 b/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 index 8f8ecef..35118e4 100644 --- a/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 +++ b/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 @@ -123,7 +123,7 @@ $noFeatureBranch = $false # paths_only and status_mode tolerate non-feature branches. $softBranchMode = ($cfg.Extras -match 'status_mode') -or ($cfg.Extras -match 'paths_only') $branchResult = Test-FeatureBranch -Branch $currentBranch -HasGit $hasGit -if ($branchResult -eq "NEEDS_SELECTION") { +if ($branchResult -eq "NEEDS_SELECTION" -and -not $softBranchMode) { $featuresJson = Get-FeaturesJson if ($Json) { Write-Output "{`"needs_selection`":true,`"features`":$featuresJson}" @@ -132,8 +132,10 @@ if ($branchResult -eq "NEEDS_SELECTION") { Write-Output "Run: /iikit-core use to select a feature." } exit 2 -} elseif ($branchResult -eq "ERROR") { +} elseif ($branchResult -eq "ERROR" -or $branchResult -eq "NEEDS_SELECTION") { if ($softBranchMode) { + # Soft modes (paths_only, status_mode) treat both "no feature branch" + # and "multiple features, none active" as informational. $noFeatureBranch = $true $paths = [PSCustomObject]@{ REPO_ROOT = $repoRoot diff --git a/.claude/skills/iikit-core/scripts/powershell/common.ps1 b/.claude/skills/iikit-core/scripts/powershell/common.ps1 index b8bc001..a7a7096 100644 --- a/.claude/skills/iikit-core/scripts/powershell/common.ps1 +++ b/.claude/skills/iikit-core/scripts/powershell/common.ps1 @@ -123,35 +123,48 @@ function Get-RepoRoot { } function Get-CurrentBranch { - # Detection cascade: active-feature file > SPECIFY_FEATURE env > git branch > single feature > fallback - - # 1. Check sticky active-feature file (survives restarts) - $active = Read-ActiveFeature - if ($active) { - return $active - } - - # 2. Check SPECIFY_FEATURE environment variable (CI/scripts) + # Detection cascade (mirrors bash get_current_branch): + # 1. SPECIFY_FEATURE env (explicit CI override) + # 2. git branch — if NNN-* or prefix/NNN-* (gitflow), return extracted + # feature id. This ensures branch switching picks up the correct + # feature even when a stale active-feature file exists. + # 3. Non-feature git branch: fall back to sticky active-feature + # 4. Non-feature git branch, no sticky file — return raw branch + # 5. Non-git: sticky active-feature + # 6. Non-git: latest feature dir + # 7. "main" fallback + + # 1. SPECIFY_FEATURE if ($env:SPECIFY_FEATURE) { return $env:SPECIFY_FEATURE } - # 3. Check git branch if available. If it's NNN-* or prefix/NNN-* (gitflow), - # return the extracted feature id; otherwise return the raw branch. + # 2-4. Git branch path try { $result = git rev-parse --abbrev-ref HEAD 2>$null - if ($LASTEXITCODE -eq 0) { + if ($LASTEXITCODE -eq 0 -and $result) { $featureId = Get-FeatureIdFromBranch -Branch $result if ($featureId) { return $featureId } + # Non-feature branch: prefer sticky active-feature, else raw branch + $active = Read-ActiveFeature + if ($active) { + return $active + } return $result } } catch { # Git command failed } - # 4. For non-git repos, try to find the latest feature directory + # 5. Non-git: sticky active-feature + $active = Read-ActiveFeature + if ($active) { + return $active + } + + # 6. Non-git: try to find the latest feature directory $repoRoot = Get-RepoRoot $specsDir = Join-Path $repoRoot "specs" @@ -174,7 +187,7 @@ function Get-CurrentBranch { } } - # Final fallback + # 7. Final fallback return "main" } diff --git a/tests/bash/check-prerequisites.bats b/tests/bash/check-prerequisites.bats index c65a6e5..19f5dbc 100644 --- a/tests/bash/check-prerequisites.bats +++ b/tests/bash/check-prerequisites.bats @@ -62,6 +62,28 @@ teardown() { [[ "$status" -eq 0 ]] } +@test "check-prerequisites: --paths-only does not exit 2 with multiple features on non-feature branch" { + unset SPECIFY_FEATURE + # Multi-feature workspace; current branch is not a feature branch + mkdir -p specs/001-first specs/002-second + rm -f .specify/active-feature + + run "$CHECK_SCRIPT" --paths-only --json + [[ "$status" -eq 0 ]] + # Must NOT prompt for selection in soft mode + [[ "$output" != *'"needs_selection":true'* ]] +} + +@test "check-prerequisites: --phase status does not exit 2 with multiple features on non-feature branch" { + unset SPECIFY_FEATURE + mkdir -p specs/001-first specs/002-second + rm -f .specify/active-feature + + run "$CHECK_SCRIPT" --phase status --json + [[ "$status" -eq 0 ]] + [[ "$output" != *'"needs_selection":true'* ]] +} + # ============================================================================= # Validation mode tests # ============================================================================= diff --git a/tests/powershell/check-prerequisites.Tests.ps1 b/tests/powershell/check-prerequisites.Tests.ps1 index 4095397..55ced21 100644 --- a/tests/powershell/check-prerequisites.Tests.ps1 +++ b/tests/powershell/check-prerequisites.Tests.ps1 @@ -38,6 +38,53 @@ Describe "Paths-only mode" { } } +Describe "Soft branch mode (#76)" { + BeforeEach { + $script:TestDir = New-TestDirectory + Push-Location $script:TestDir + $env:SPECIFY_FEATURE = $null + } + + AfterEach { + Pop-Location + $env:SPECIFY_FEATURE = $null + Remove-TestDirectory -TestDir $script:TestDir + } + + It "-PathsOnly succeeds on non-feature branch with no features" { + Remove-Item -Path "specs" -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path "specs" -Force | Out-Null + + $result = & $script:CheckScript -PathsOnly -Json | Out-String + $result | Should -Match '"FEATURE_DIR"' + } + + It "-Phase core succeeds on non-feature branch (gitflow-agnostic)" { + Remove-Item -Path "specs" -Recurse -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path "specs" -Force | Out-Null + + { & $script:CheckScript -Phase core -Json } | Should -Not -Throw + } + + It "-PathsOnly does not exit 2 with multiple features on non-feature branch" { + New-Item -ItemType Directory -Path "specs/001-first" -Force | Out-Null + New-Item -ItemType Directory -Path "specs/002-second" -Force | Out-Null + Remove-Item -Path ".specify/active-feature" -Force -ErrorAction SilentlyContinue + + $result = & $script:CheckScript -PathsOnly -Json | Out-String + $result | Should -Not -Match '"needs_selection":\s*true' + } + + It "-Phase status does not exit 2 with multiple features on non-feature branch" { + New-Item -ItemType Directory -Path "specs/001-first" -Force | Out-Null + New-Item -ItemType Directory -Path "specs/002-second" -Force | Out-Null + Remove-Item -Path ".specify/active-feature" -Force -ErrorAction SilentlyContinue + + $result = & $script:CheckScript -Phase status -Json | Out-String + $result | Should -Not -Match '"needs_selection":\s*true' + } +} + Describe "Validation mode" { BeforeEach { $script:TestDir = New-TestDirectory diff --git a/tests/powershell/common.Tests.ps1 b/tests/powershell/common.Tests.ps1 index 4024203..5403eef 100644 --- a/tests/powershell/common.Tests.ps1 +++ b/tests/powershell/common.Tests.ps1 @@ -92,6 +92,45 @@ Describe "Test-FeatureBranch" { $result | Should -Be $true $env:SPECIFY_FEATURE = $null } + + It "accepts feat/NNN- gitflow pattern (#77)" { + $env:SPECIFY_FEATURE = $null + $result = Test-FeatureBranch -Branch "feat/001-test-feature" -HasGit $true + $result | Should -Be "OK" + } + + It "accepts fix/NNN- gitflow pattern (#77)" { + $env:SPECIFY_FEATURE = $null + $result = Test-FeatureBranch -Branch "fix/042-some-bug" -HasGit $true + $result | Should -Be "OK" + } +} + +Describe "Get-FeatureIdFromBranch (#77)" { + It "returns NNN-name for standard branch" { + $result = Get-FeatureIdFromBranch -Branch "001-user-auth" + $result | Should -Be "001-user-auth" + } + + It "strips feat/ gitflow prefix" { + $result = Get-FeatureIdFromBranch -Branch "feat/042-null-deref" + $result | Should -Be "042-null-deref" + } + + It "strips fix/ gitflow prefix" { + $result = Get-FeatureIdFromBranch -Branch "fix/005-bug" + $result | Should -Be "005-bug" + } + + It "returns empty for non-feature branch" { + $result = Get-FeatureIdFromBranch -Branch "main" + $result | Should -Be '' + } + + It "returns empty for prefix without NNN-" { + $result = Get-FeatureIdFromBranch -Branch "feat/no-number" + $result | Should -Be '' + } } Describe "Find-FeatureDirByPrefix" { @@ -121,6 +160,21 @@ Describe "Find-FeatureDirByPrefix" { $result = Find-FeatureDirByPrefix -RepoRoot $script:TestDir -BranchName "999-nonexistent" $result | Should -Be (Join-Path $script:TestDir "specs/999-nonexistent") } + + It "resolves gitflow feat/NNN- branch (#77)" { + New-Item -ItemType Directory -Path "specs/007-gitflow-feature" -Force | Out-Null + + $result = Find-FeatureDirByPrefix -RepoRoot $script:TestDir -BranchName "feat/007-gitflow-feature" + $result | Should -Match "007-gitflow-feature" + } + + It "resolves gitflow fix/NNN- branch by prefix only (#77)" { + New-Item -ItemType Directory -Path "specs/008-existing-feature" -Force | Out-Null + + # Caller may be on fix/008-different-name; we still resolve by numeric prefix + $result = Find-FeatureDirByPrefix -RepoRoot $script:TestDir -BranchName "fix/008-different-name" + $result | Should -Match "008-existing-feature" + } } Describe "Test-Constitution" { diff --git a/tests/powershell/create-new-feature.Tests.ps1 b/tests/powershell/create-new-feature.Tests.ps1 index f2738d5..35ea697 100644 --- a/tests/powershell/create-new-feature.Tests.ps1 +++ b/tests/powershell/create-new-feature.Tests.ps1 @@ -147,4 +147,56 @@ Describe "create-new-feature" { $json.BRANCH_NAME.Length | Should -BeLessOrEqual 244 } } + + Context "Dry-run (#76)" { + It "DryRun does not create directory" { + & $script:CreateScript -Json -DryRun -Description "Preview only feature" | Out-Null + + $children = Get-ChildItem -Path "specs" -ErrorAction SilentlyContinue + $children | Should -BeNullOrEmpty + } + + It "DryRun does not create git branch" { + $originalBranch = git branch --show-current + & $script:CreateScript -Json -DryRun -Description "Preview only feature" | Out-Null + + $currentBranch = git branch --show-current + $currentBranch | Should -Be $originalBranch + } + + It "DryRun emits dry_run=true in JSON" { + $result = & $script:CreateScript -Json -DryRun -Description "Preview only feature" | Out-String + + $result | Should -Match '"dry_run":\s*true' + $result | Should -Match '"BRANCH_NAME"' + $result | Should -Match '"FEATURE_NUM"' + } + + It "DryRun respects ShortName and Number" { + $result = & $script:CreateScript -Json -DryRun -ShortName "user-auth" -Number 7 -Description "ignored desc" | Out-String + + $result | Should -Match '"FEATURE_NUM":\s*"007"' + $result | Should -Match "user-auth" + } + } + + Context "Gitflow branch prefix (#77)" { + It "counts feat/NNN-* gitflow branches when picking next number" { + git checkout -b "feat/003-existing" 2>&1 | Out-Null + git checkout -b "temp-branch" 2>&1 | Out-Null + + $result = & $script:CreateScript -Json -DryRun -Description "New feature" | Out-String + + $result | Should -Match '"FEATURE_NUM":\s*"004"' + } + + It "counts fix/NNN-* gitflow branches when picking next number" { + git checkout -b "fix/012-some-bug" 2>&1 | Out-Null + git checkout -b "temp-branch" 2>&1 | Out-Null + + $result = & $script:CreateScript -Json -DryRun -Description "New feature" | Out-String + + $result | Should -Match '"FEATURE_NUM":\s*"013"' + } + } } From 22059bdd000d0ed04bfba1e071e7a465a18c4bee Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 4 Jun 2026 00:36:47 +0200 Subject: [PATCH 5/6] fix(scripts): narrow needs_selection swallow to paths_only mode The prior fixup let status_mode also bypass the needs_selection picker, but BUG-19's regression test (check-prerequisites: --phase status still exits 2 for needs_selection) correctly asserts the opposite: /iikit-core status SHOULD surface the picker so the user gets guidance on selecting a feature. Narrow scope: only paths_only swallows needs_selection. status_mode keeps the picker. Bash and PowerShell mirror, with the over-broad bats test from the prior fixup removed. Co-Authored-By: Claude Opus 4.7 --- .../scripts/bash/check-prerequisites.sh | 14 +++++++++----- .../scripts/powershell/check-prerequisites.ps1 | 12 ++++++++---- tests/bash/check-prerequisites.bats | 15 +++------------ tests/powershell/check-prerequisites.Tests.ps1 | 10 +--------- 4 files changed, 21 insertions(+), 30 deletions(-) diff --git a/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh b/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh index 3e34667..47445db 100755 --- a/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh +++ b/.claude/skills/iikit-core/scripts/bash/check-prerequisites.sh @@ -222,8 +222,12 @@ elif [[ "$PHASE" == "00" ]]; then else check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || BRANCH_EXIT=$? fi -if [[ $BRANCH_EXIT -eq 2 ]] && ! $SOFT_BRANCH_MODE; then - # Multiple features, no active one — caller should present picker +if [[ $BRANCH_EXIT -eq 2 ]] && [[ "$P_EXTRAS" != *"paths_only"* ]]; then + # Multiple features, no active one — caller should present picker. + # paths_only is the only mode that swallows this: callers asking only + # for paths (CI dry-runs, doctor #74) must never be blocked by a picker. + # status_mode still surfaces the picker so /iikit-core status can guide + # the user to select a feature. FEATURES_JSON=$(list_features_json) if $JSON_MODE; then printf '{"needs_selection":true,"features":%s}\n' "$FEATURES_JSON" @@ -234,9 +238,9 @@ if [[ $BRANCH_EXIT -eq 2 ]] && ! $SOFT_BRANCH_MODE; then exit 2 elif [[ $BRANCH_EXIT -ne 0 ]]; then if $SOFT_BRANCH_MODE; then - # Soft modes (paths_only, status_mode) treat both "no feature branch" - # and "multiple features, none active" as informational — emit empty - # path JSON instead of gating. + # Soft modes treat "no feature branch" as informational — emit empty + # path JSON instead of gating. (paths_only additionally swallows + # needs_selection per the branch above.) FEATURE_DIR="" FEATURE_SPEC="" IMPL_PLAN="" diff --git a/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 b/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 index 35118e4..261567e 100644 --- a/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 +++ b/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 @@ -122,8 +122,12 @@ $currentBranch = Get-CurrentBranch $noFeatureBranch = $false # paths_only and status_mode tolerate non-feature branches. $softBranchMode = ($cfg.Extras -match 'status_mode') -or ($cfg.Extras -match 'paths_only') +$pathsOnlyMode = ($cfg.Extras -match 'paths_only') $branchResult = Test-FeatureBranch -Branch $currentBranch -HasGit $hasGit -if ($branchResult -eq "NEEDS_SELECTION" -and -not $softBranchMode) { +if ($branchResult -eq "NEEDS_SELECTION" -and -not $pathsOnlyMode) { + # Multiple features, no active one — present picker. Only paths_only + # swallows this; status_mode still surfaces it so /iikit-core status + # can guide the user to select a feature. $featuresJson = Get-FeaturesJson if ($Json) { Write-Output "{`"needs_selection`":true,`"features`":$featuresJson}" @@ -132,10 +136,10 @@ if ($branchResult -eq "NEEDS_SELECTION" -and -not $softBranchMode) { Write-Output "Run: /iikit-core use to select a feature." } exit 2 -} elseif ($branchResult -eq "ERROR" -or $branchResult -eq "NEEDS_SELECTION") { +} elseif ($branchResult -eq "ERROR" -or ($branchResult -eq "NEEDS_SELECTION" -and $pathsOnlyMode)) { if ($softBranchMode) { - # Soft modes (paths_only, status_mode) treat both "no feature branch" - # and "multiple features, none active" as informational. + # Soft modes treat "no feature branch" as informational. + # paths_only additionally swallows needs_selection per the branch above. $noFeatureBranch = $true $paths = [PSCustomObject]@{ REPO_ROOT = $repoRoot diff --git a/tests/bash/check-prerequisites.bats b/tests/bash/check-prerequisites.bats index 19f5dbc..72ec249 100644 --- a/tests/bash/check-prerequisites.bats +++ b/tests/bash/check-prerequisites.bats @@ -64,23 +64,14 @@ teardown() { @test "check-prerequisites: --paths-only does not exit 2 with multiple features on non-feature branch" { unset SPECIFY_FEATURE - # Multi-feature workspace; current branch is not a feature branch + # Multi-feature workspace; current branch is not a feature branch. + # paths_only must not gate on feature selection — callers (CI dry-runs, + # doctor #74) only want paths. mkdir -p specs/001-first specs/002-second rm -f .specify/active-feature run "$CHECK_SCRIPT" --paths-only --json [[ "$status" -eq 0 ]] - # Must NOT prompt for selection in soft mode - [[ "$output" != *'"needs_selection":true'* ]] -} - -@test "check-prerequisites: --phase status does not exit 2 with multiple features on non-feature branch" { - unset SPECIFY_FEATURE - mkdir -p specs/001-first specs/002-second - rm -f .specify/active-feature - - run "$CHECK_SCRIPT" --phase status --json - [[ "$status" -eq 0 ]] [[ "$output" != *'"needs_selection":true'* ]] } diff --git a/tests/powershell/check-prerequisites.Tests.ps1 b/tests/powershell/check-prerequisites.Tests.ps1 index 55ced21..716b505 100644 --- a/tests/powershell/check-prerequisites.Tests.ps1 +++ b/tests/powershell/check-prerequisites.Tests.ps1 @@ -67,6 +67,7 @@ Describe "Soft branch mode (#76)" { } It "-PathsOnly does not exit 2 with multiple features on non-feature branch" { + # paths_only must not gate on feature selection. New-Item -ItemType Directory -Path "specs/001-first" -Force | Out-Null New-Item -ItemType Directory -Path "specs/002-second" -Force | Out-Null Remove-Item -Path ".specify/active-feature" -Force -ErrorAction SilentlyContinue @@ -74,15 +75,6 @@ Describe "Soft branch mode (#76)" { $result = & $script:CheckScript -PathsOnly -Json | Out-String $result | Should -Not -Match '"needs_selection":\s*true' } - - It "-Phase status does not exit 2 with multiple features on non-feature branch" { - New-Item -ItemType Directory -Path "specs/001-first" -Force | Out-Null - New-Item -ItemType Directory -Path "specs/002-second" -Force | Out-Null - Remove-Item -Path ".specify/active-feature" -Force -ErrorAction SilentlyContinue - - $result = & $script:CheckScript -Phase status -Json | Out-String - $result | Should -Not -Match '"needs_selection":\s*true' - } } Describe "Validation mode" { From d0bca2f7c210b5454f1c6791ccd3253e1e68ce27 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 4 Jun 2026 00:50:32 +0200 Subject: [PATCH 6/6] fix(scripts): suppress Test-FeatureBranch streams in PS soft mode The PowerShell mirror let Test-FeatureBranch's warning (stream 3) and error (stream 2) streams reach the caller in soft modes, where the script's contract is to emit only JSON or status text. Bash uses `2>/dev/null` for parity; PS now mirrors that with `2>$null 3>$null` on the Test-FeatureBranch call inside soft branch mode. Hard modes still surface those streams so users on a wrong branch see the diagnostic. Co-Authored-By: Claude Opus 4.7 --- .../scripts/powershell/check-prerequisites.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 b/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 index 261567e..b4c9d15 100644 --- a/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 +++ b/.claude/skills/iikit-core/scripts/powershell/check-prerequisites.ps1 @@ -123,7 +123,15 @@ $noFeatureBranch = $false # paths_only and status_mode tolerate non-feature branches. $softBranchMode = ($cfg.Extras -match 'status_mode') -or ($cfg.Extras -match 'paths_only') $pathsOnlyMode = ($cfg.Extras -match 'paths_only') -$branchResult = Test-FeatureBranch -Branch $currentBranch -HasGit $hasGit +# In soft modes the script emits JSON or status text only — suppress +# Test-FeatureBranch's diagnostic warning/error streams so they cannot +# contaminate the structured payload. Mirrors `2>/dev/null` in the bash +# implementation. +if ($softBranchMode) { + $branchResult = Test-FeatureBranch -Branch $currentBranch -HasGit $hasGit 2>$null 3>$null +} else { + $branchResult = Test-FeatureBranch -Branch $currentBranch -HasGit $hasGit +} if ($branchResult -eq "NEEDS_SELECTION" -and -not $pathsOnlyMode) { # Multiple features, no active one — present picker. Only paths_only # swallows this; status_mode still surfaces it so /iikit-core status