Closing the intent-to-code chasm - specification-driven development with BDD verification chain
Overall
score
96%
Does it follow best practices?
Validation for skill structure
#!/usr/bin/env bash
# Common functions and variables for all scripts
# =============================================================================
# ACTIVE FEATURE HELPERS
# =============================================================================
# Read the sticky active feature from .specify/active-feature
# Returns the feature name if the file exists and the corresponding specs/ dir is valid
# Usage: read_active_feature [repo_root]
read_active_feature() {
local repo_root="${1:-$(get_repo_root)}"
local active_file="$repo_root/.specify/active-feature"
if [[ -f "$active_file" ]]; then
local feature
feature=$(cat "$active_file" 2>/dev/null)
if [[ -n "$feature" && -d "$repo_root/specs/$feature" ]]; then
echo "$feature"
return 0
fi
fi
return 1
}
# Write the sticky active feature to .specify/active-feature
# Usage: write_active_feature <feature> [repo_root]
write_active_feature() {
local feature="$1"
local repo_root="${2:-$(get_repo_root)}"
local active_file="$repo_root/.specify/active-feature"
mkdir -p "$repo_root/.specify"
echo "$feature" > "$active_file"
}
# Detect feature stage from artifacts present in the feature directory
# Returns: specified | planned | tasks-ready | implementing-NN% | complete
get_feature_stage() {
local repo_root="$1"
local feature="$2"
local feature_dir="$repo_root/specs/$feature"
if [[ ! -d "$feature_dir" ]]; then
echo "unknown"
return
fi
# Check for tasks and completion percentage
if [[ -f "$feature_dir/tasks.md" ]]; then
local total=0
local done=0
local re_task='^- \[[ xX]\]'
local re_done='^- \[[xX]\]'
while IFS= read -r line; do
if [[ "$line" =~ $re_task ]]; then
total=$((total + 1))
if [[ "$line" =~ $re_done ]]; then
done=$((done + 1))
fi
fi
done < "$feature_dir/tasks.md"
if [[ "$total" -gt 0 ]]; then
if [[ "$done" -eq "$total" ]]; then
echo "complete"
elif [[ "$done" -gt 0 ]]; then
local pct=$(( (done * 100) / total ))
echo "implementing-${pct}%"
else
echo "tasks-ready"
fi
return
fi
fi
if [[ -f "$feature_dir/plan.md" ]]; then
echo "planned"
return
fi
if [[ -f "$feature_dir/spec.md" ]]; then
echo "specified"
return
fi
echo "unknown"
}
# List all features with stages as a JSON array
list_features_json() {
local repo_root=$(get_repo_root)
local specs_dir="$repo_root/specs"
local first=true
printf '['
if [[ -d "$specs_dir" ]]; then
for dir in "$specs_dir"/*; do
if [[ -d "$dir" ]] && [[ "$(basename "$dir")" =~ ^[0-9]{3}- ]]; then
local name=$(basename "$dir")
local stage=$(get_feature_stage "$repo_root" "$name")
if $first; then
first=false
else
printf ','
fi
printf '{"name":"%s","stage":"%s"}' "$name" "$stage"
fi
done
fi
printf ']'
}
# Get repository root, with fallback for non-git repositories
get_repo_root() {
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)
fi
}
# Get current branch, with fallback for non-git repositories
# Detection cascade: active-feature file > SPECIFY_FEATURE env > git branch > single feature > fallback
get_current_branch() {
# 1. Check sticky active-feature file (survives restarts)
local active
active=$(read_active_feature 2>/dev/null) && [[ -n "$active" ]] && {
echo "$active"
return
}
# 2. Check SPECIFY_FEATURE environment variable (CI/scripts)
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
echo "$SPECIFY_FEATURE"
return
fi
# 3. Check git branch if available
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
git rev-parse --abbrev-ref HEAD
return
fi
# 4. 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
for dir in "$specs_dir"/*; do
if [[ -d "$dir" ]]; then
local dirname=$(basename "$dir")
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
local number=${BASH_REMATCH[1]}
number=$((10#$number))
if [[ "$number" -gt "$highest" ]]; then
highest=$number
latest_feature=$dirname
fi
fi
fi
done
if [[ -n "$latest_feature" ]]; then
echo "$latest_feature"
return
fi
fi
echo "main" # Final fallback
}
# Check if we have git available
has_git() {
git rev-parse --show-toplevel >/dev/null 2>&1
}
check_feature_branch() {
local branch="$1"
local has_git_repo="$2"
# For non-git repos, we can't enforce branch naming but still provide output
if [[ "$has_git_repo" != "true" ]]; then
echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
return 0
fi
# Accept if branch matches NNN- pattern (standard feature branch)
if [[ "$branch" =~ ^[0-9]{3}- ]]; then
write_active_feature "$branch"
return 0
fi
# Accept if SPECIFY_FEATURE env var is set (explicit feature context, e.g., --skip-branch)
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
echo "[specify] Using feature context from SPECIFY_FEATURE: $SPECIFY_FEATURE" >&2
write_active_feature "$SPECIFY_FEATURE"
return 0
fi
# Check if there are feature directories we can use
local repo_root=$(get_repo_root)
local specs_dir="$repo_root/specs"
local feature_count=0
local latest_feature=""
if [[ -d "$specs_dir" ]]; then
for dir in "$specs_dir"/*; do
if [[ -d "$dir" ]] && [[ "$(basename "$dir")" =~ ^[0-9]{3}- ]]; then
feature_count=$((feature_count + 1))
latest_feature=$(basename "$dir")
fi
done
fi
if [[ "$feature_count" -eq 1 ]]; then
echo "[specify] Not on feature branch, but found single feature directory: $latest_feature" >&2
export SPECIFY_FEATURE="$latest_feature"
write_active_feature "$latest_feature"
return 0
elif [[ "$feature_count" -gt 1 ]]; then
echo "WARNING: Not on a feature branch and multiple feature directories exist." >&2
echo "Current branch: $branch" >&2
echo "Run: /iikit-core use <feature> to select a feature." >&2
return 2
fi
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
echo "Run: /iikit-01-specify <feature description>" >&2
return 1
}
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)
find_feature_dir_by_prefix() {
local repo_root="$1"
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
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
for dir in "$specs_dir"/"$prefix"-*; do
if [[ -d "$dir" ]]; then
matches+=("$(basename "$dir")")
fi
done
fi
# Handle results
if [[ ${#matches[@]} -eq 0 ]]; then
# No match found - return the branch name path (will fail later with clear error)
echo "$specs_dir/$branch_name"
elif [[ ${#matches[@]} -eq 1 ]]; then
# Exactly one match - perfect!
echo "$specs_dir/${matches[0]}"
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
fi
}
get_feature_paths() {
local repo_root=$(get_repo_root)
local current_branch=$(get_current_branch)
local has_git_repo="false"
if has_git; then
has_git_repo="true"
fi
# Use prefix-based lookup to support multiple branches per spec
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
cat <<EOF
REPO_ROOT='$repo_root'
CURRENT_BRANCH='$current_branch'
HAS_GIT='$has_git_repo'
FEATURE_DIR='$feature_dir'
FEATURE_SPEC='$feature_dir/spec.md'
IMPL_PLAN='$feature_dir/plan.md'
TASKS='$feature_dir/tasks.md'
RESEARCH='$feature_dir/research.md'
DATA_MODEL='$feature_dir/data-model.md'
QUICKSTART='$feature_dir/quickstart.md'
CONTRACTS_DIR='$feature_dir/contracts'
EOF
}
check_file() { [[ -f "$1" ]] && echo " [Y] $2" || echo " [N] $2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " [Y] $2" || echo " [N] $2"; }
# =============================================================================
# VALIDATION FUNCTIONS
# =============================================================================
# Validate PREMISE.md exists and has no unresolved placeholders
validate_premise() {
local repo_root="$1"
local premise="$repo_root/PREMISE.md"
if [[ ! -f "$premise" ]]; then
echo "WARNING: PREMISE.md not found. Run /iikit-core init to create one." >&2
return 1
fi
# Check for remaining placeholders
if grep -q '\[[A-Z][A-Z_]*\]' "$premise" 2>/dev/null; then
local count
count=$(grep -c '\[[A-Z][A-Z_]*\]' "$premise" 2>/dev/null) || count=0
echo "WARNING: PREMISE.md has $count unresolved placeholder(s)" >&2
return 1
fi
return 0
}
# Validate constitution exists
validate_constitution() {
local repo_root="$1"
local constitution="$repo_root/CONSTITUTION.md"
if [[ ! -f "$constitution" ]]; then
echo "ERROR: Constitution not found at $constitution" >&2
echo "Run /iikit-00-constitution first to define project principles." >&2
return 1
fi
# Check for required sections
if ! grep -q "^## .*Principles\|^# .*Constitution" "$constitution" 2>/dev/null; then
echo "WARNING: Constitution may be incomplete - missing principles section" >&2
fi
# Check minimum principle count — supports both heading style (### I. Name)
# and bullet style (- **Name**: ...)
local principle_count
principle_count=$(grep -cE "^### [IVX]+\.|^### [0-9]+\.|^- \*\*[A-Z]" "$constitution" 2>/dev/null) || principle_count=0
if [[ "$principle_count" -lt 3 ]]; then
echo "WARNING: Constitution has only $principle_count principle(s) — minimum 3 recommended" >&2
fi
return 0
}
# Validate spec.md exists and has required structure
validate_spec() {
local spec_file="$1"
local errors=0
if [[ ! -f "$spec_file" ]]; then
echo "ERROR: spec.md not found at $spec_file" >&2
echo "Run /iikit-01-specify first to create the feature specification." >&2
return 1
fi
# Check for required sections
if ! grep -q "^## Requirements\|^## Functional Requirements\|^### Functional Requirements" "$spec_file" 2>/dev/null; then
echo "ERROR: spec.md missing 'Requirements' section" >&2
((errors++))
fi
if ! grep -q "^## Success Criteria" "$spec_file" 2>/dev/null; then
echo "ERROR: spec.md missing 'Success Criteria' section" >&2
((errors++))
fi
if ! grep -q "^## User Scenarios\|^### User Story" "$spec_file" 2>/dev/null; then
echo "ERROR: spec.md missing 'User Scenarios' or 'User Story' section" >&2
((errors++))
fi
# Check for unresolved clarifications
if grep -q "\[NEEDS CLARIFICATION" "$spec_file" 2>/dev/null; then
local count=$(grep -c "\[NEEDS CLARIFICATION" "$spec_file" 2>/dev/null || echo "0")
echo "WARNING: spec.md has $count unresolved [NEEDS CLARIFICATION] markers" >&2
echo "Consider running /iikit-02-clarify to resolve them." >&2
fi
[[ $errors -gt 0 ]] && return 1
return 0
}
# Validate plan.md exists and has required structure
validate_plan() {
local plan_file="$1"
local errors=0
if [[ ! -f "$plan_file" ]]; then
echo "ERROR: plan.md not found at $plan_file" >&2
echo "Run /iikit-03-plan first to create the implementation plan." >&2
return 1
fi
# Check for required sections
if ! grep -q "^## Technical Context\|^\*\*Language/Version\*\*" "$plan_file" 2>/dev/null; then
echo "WARNING: plan.md may be incomplete - missing Technical Context" >&2
fi
# Check for unresolved clarifications
if grep -q "NEEDS CLARIFICATION" "$plan_file" 2>/dev/null; then
local count=$(grep -c "NEEDS CLARIFICATION" "$plan_file" 2>/dev/null || echo "0")
echo "WARNING: plan.md has $count unresolved NEEDS CLARIFICATION items" >&2
fi
return 0
}
# Validate tasks.md exists and has required structure
validate_tasks() {
local tasks_file="$1"
if [[ ! -f "$tasks_file" ]]; then
echo "ERROR: tasks.md not found at $tasks_file" >&2
echo "Run /iikit-06-tasks first to create the task list." >&2
return 1
fi
# Check for at least one task
if ! grep -q "^- \[ \]\|^- \[x\]\|^- \[X\]" "$tasks_file" 2>/dev/null; then
echo "WARNING: tasks.md appears to have no task items" >&2
fi
return 0
}
# Calculate spec quality score (0-10)
calculate_spec_quality() {
local spec_file="$1"
local score=0
[[ ! -f "$spec_file" ]] && echo "0" && return
# +2 for having requirements section
grep -q "^## Requirements\|^### Functional Requirements" "$spec_file" 2>/dev/null && ((score+=2))
# +2 for having success criteria
grep -q "^## Success Criteria" "$spec_file" 2>/dev/null && ((score+=2))
# +2 for having user scenarios
grep -q "^## User Scenarios\|^### User Story" "$spec_file" 2>/dev/null && ((score+=2))
# +1 for having at least 3 requirements
local req_count
req_count=$(grep -c "^- \*\*FR-\|^- FR-" "$spec_file" 2>/dev/null) || req_count=0
[[ $req_count -ge 3 ]] && ((score+=1))
# +1 for having at least 3 success criteria
local sc_count
sc_count=$(grep -c "^- \*\*SC-\|^- SC-" "$spec_file" 2>/dev/null) || sc_count=0
[[ $sc_count -ge 3 ]] && ((score+=1))
# +1 for no NEEDS CLARIFICATION markers
! grep -q "\[NEEDS CLARIFICATION" "$spec_file" 2>/dev/null && ((score+=1))
# +1 for having edge cases section
grep -q "^### Edge Cases\|^## Edge Cases" "$spec_file" 2>/dev/null && ((score+=1))
echo "$score"
}
# =============================================================================
# BDD FRAMEWORK DETECTION (shared by verify-steps.sh and setup-bdd.sh)
# Returns: "framework_name|language" or empty string if none detected
# Callers that need only the framework: $(detect_framework "$plan" | cut -d'|' -f1)
# =============================================================================
detect_framework() {
local plan_file="$1"
if [[ ! -f "$plan_file" ]]; then
echo ""
return
fi
local content
content=$(cat "$plan_file")
# Python + pytest-bdd
if echo "$content" | grep -qi "pytest-bdd"; then
echo "pytest-bdd|python"; return
fi
# Python + behave
if echo "$content" | grep -qi "behave"; then
echo "behave|python"; return
fi
# JavaScript/TypeScript + Cucumber
if echo "$content" | grep -qi "@cucumber/cucumber\|cucumber-js\|cucumber\.js"; then
echo "@cucumber/cucumber|javascript"; return
fi
# Go + godog
if echo "$content" | grep -qi "godog"; then
echo "godog|go"; return
fi
# Java + Maven + Cucumber-JVM
if echo "$content" | grep -qi "cucumber" && echo "$content" | grep -qi "maven\|pom\.xml"; then
echo "cucumber-jvm-maven|java"; return
fi
# Java + Gradle + Cucumber-JVM
if echo "$content" | grep -qi "cucumber" && echo "$content" | grep -qi "gradle"; then
echo "cucumber-jvm-gradle|java"; return
fi
# Rust + cucumber-rs
if echo "$content" | grep -qi "cucumber-rs\|cucumber.*rust\|rust.*cucumber"; then
echo "cucumber-rs|rust"; return
fi
# C# + Reqnroll
if echo "$content" | grep -qi "reqnroll"; then
echo "reqnroll|csharp"; return
fi
# Fallback: detect language and infer default BDD framework
if echo "$content" | grep -qi "python\|pytest"; then
echo "pytest-bdd|python"; return
fi
if echo "$content" | grep -qi "javascript\|typescript\|node\.js\|jest\|vitest"; then
echo "@cucumber/cucumber|javascript"; return
fi
if echo "$content" | grep -qi "golang\|go test\|go 1\."; then
echo "godog|go"; return
fi
if echo "$content" | grep -qiE "java[^s].*(jdk|jre|spring|maven|gradle)"; then
if echo "$content" | grep -qi "gradle"; then
echo "cucumber-jvm-gradle|java"
else
echo "cucumber-jvm-maven|java"
fi
return
fi
if echo "$content" | grep -qi "rust\|cargo"; then
echo "cucumber-rs|rust"; return
fi
if echo "$content" | grep -qi "c#\|csharp\|\.net\|dotnet"; then
echo "reqnroll|csharp"; return
fi
echo ""
}
# =============================================================================
# BDD DEPENDENCY CHECK (used by pre-commit-hook.sh)
# Returns 0 (found) or 1 (not found)
# Outputs the dep file path on stdout when found
# =============================================================================
check_bdd_dependency() {
local framework="$1"
local repo_root="$2"
case "$framework" in
pytest-bdd|behave)
# Check Python dependency files for framework name
local py_dep_files=(
"$repo_root/requirements.txt"
"$repo_root/requirements-dev.txt"
"$repo_root/requirements-test.txt"
"$repo_root/pyproject.toml"
"$repo_root/setup.py"
"$repo_root/setup.cfg"
"$repo_root/Pipfile"
)
# Also check requirements*.txt via glob
for f in "$repo_root"/requirements*.txt; do
[[ -f "$f" ]] && py_dep_files+=("$f")
done
for dep_file in "${py_dep_files[@]}"; do
if [[ -f "$dep_file" ]] && grep -qi "$framework" "$dep_file" 2>/dev/null; then
echo "$dep_file"
return 0
fi
done
return 1
;;
@cucumber/cucumber)
local pkg_json="$repo_root/package.json"
if [[ -f "$pkg_json" ]] && grep -q "@cucumber/cucumber" "$pkg_json" 2>/dev/null; then
echo "$pkg_json"
return 0
fi
return 1
;;
godog)
local go_mod="$repo_root/go.mod"
if [[ -f "$go_mod" ]] && grep -qi "godog" "$go_mod" 2>/dev/null; then
echo "$go_mod"
return 0
fi
return 1
;;
cucumber-jvm-maven)
local pom="$repo_root/pom.xml"
if [[ -f "$pom" ]] && grep -qi "cucumber" "$pom" 2>/dev/null; then
echo "$pom"
return 0
fi
return 1
;;
cucumber-jvm-gradle)
for gf in "$repo_root/build.gradle" "$repo_root/build.gradle.kts"; do
if [[ -f "$gf" ]] && grep -qi "cucumber" "$gf" 2>/dev/null; then
echo "$gf"
return 0
fi
done
return 1
;;
cucumber-rs)
local cargo="$repo_root/Cargo.toml"
if [[ -f "$cargo" ]] && grep -qi "cucumber" "$cargo" 2>/dev/null; then
echo "$cargo"
return 0
fi
return 1
;;
reqnroll)
# Search for *.csproj files up to 2 levels deep
local found
found=$(find "$repo_root" -maxdepth 2 -name "*.csproj" -type f 2>/dev/null | while read -r csproj; do
if grep -qi "reqnroll" "$csproj" 2>/dev/null; then
echo "$csproj"
break
fi
done)
if [[ -n "$found" ]]; then
echo "$found"
return 0
fi
return 1
;;
*)
# Unknown framework — can't check
return 1
;;
esac
}Install with Tessl CLI
npx tessl i tessl-labs/intent-integrity-kit@2.3.5rules
skills
iikit-00-constitution
scripts
iikit-01-specify
iikit-02-clarify
iikit-03-plan
iikit-04-checklist
scripts
dashboard
iikit-05-testify
iikit-06-tasks
iikit-07-analyze
iikit-08-implement
iikit-09-taskstoissues
iikit-bugfix
scripts
iikit-core
scripts
bash
dashboard
powershell