Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/iikit-01-specify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<feature description>"
license: MIT
metadata:
version: "1.6.4"
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/iikit-08-taskstoissues/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/iikit-bugfix/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<bug description or GitHub issue URL>"
license: MIT
metadata:
version: "1.6.4"
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/iikit-clarify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/iikit-core/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jbaruch/coding-policy: skill-authoring says: "Optional fields: allowed-tools, disable-model-invocation, user-invocable". This PR adds argument-hint frontmatter to this skill (and the same pattern appears in the other changed SKILL.md files), but that field is not permitted by the loaded skill frontmatter contract. Remove these argument-hint entries, or update the policy first so argument-hint is an allowed skill frontmatter field before introducing it here.

license: MIT
metadata:
version: "1.6.4"
Expand Down
32 changes: 22 additions & 10 deletions .claude/skills/iikit-core/scripts/bash/check-prerequisites.sh
Original file line number Diff line number Diff line change
Expand Up @@ -205,19 +205,29 @@ 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jbaruch/coding-policy: error-handling says: "When multiple approaches exist, try alternatives before failing." In --phase core / --paths-only, SOFT_BRANCH_MODE is meant to make non-feature branches informational, but a multi-feature workspace still returns BRANCH_EXIT=2 and exits through the unchanged needs_selection block before the soft fallback runs. That means --paths-only --json still gates on feature selection as soon as two specs/NNN-* dirs exist. Handle BRANCH_EXIT=2 inside soft mode the same way as the no-feature fallback, or otherwise emit path JSON without exiting 2, and add a regression test with multiple feature dirs.

# 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
BRANCH_EXIT=0
else
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || BRANCH_EXIT=$?
fi
if [[ $BRANCH_EXIT -eq 2 ]]; 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"
Expand All @@ -227,8 +237,10 @@ 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
# 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=""
Expand All @@ -237,14 +249,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
Expand Down Expand Up @@ -360,7 +372,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

Expand Down
42 changes: 36 additions & 6 deletions .claude/skills/iikit-core/scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
32 changes: 27 additions & 5 deletions .claude/skills/iikit-core/scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ JSON_MODE=false
SHORT_NAME=""
BRANCH_NUMBER=""
SKIP_BRANCH=false
DRY_RUN=false
ARGS=()
i=1
while [ $i -le $# ]; do
Expand All @@ -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
Expand Down Expand Up @@ -44,19 +48,21 @@ while [ $i -le $# ]; do
BRANCH_NUMBER="$next_arg"
;;
--help|-h)
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--skip-branch] <feature_description>"
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--skip-branch] [--dry-run] <feature_description>"
echo ""
echo "Options:"
echo " --json Output in JSON format"
echo " --short-name <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
;;
*)
Expand Down Expand Up @@ -121,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
Expand Down Expand Up @@ -265,6 +273,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
Expand All @@ -273,12 +297,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,23 @@ if ($ProjectRoot) {
}
$hasGit = Test-HasGit
$currentBranch = Get-CurrentBranch
$statusNoFeature = $false
$branchResult = Test-FeatureBranch -Branch $currentBranch -HasGit $hasGit
if ($branchResult -eq "NEEDS_SELECTION") {
$noFeatureBranch = $false
# paths_only and status_mode tolerate non-feature branches.
$softBranchMode = ($cfg.Extras -match 'status_mode') -or ($cfg.Extras -match 'paths_only')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jbaruch/coding-policy: error-handling says: "When multiple approaches exist, try alternatives before failing." The PowerShell mirror sets $softBranchMode for paths_only, but NEEDS_SELECTION is still handled before the soft fallback, so -Phase core -Json exits 2 on a non-feature branch with multiple features instead of returning informational paths. Mirror the Bash fix here as well, and cover it in tests/powershell/check-prerequisites.Tests.ps1 so the two implementations stay aligned.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jbaruch/coding-policy: script-delegation says JSON-producing scripts must output structured data, not prose. This new soft branch mode still calls Test-FeatureBranch normally on line 126; in the PowerShell helper, the ERROR and NEEDS_SELECTION paths use Write-Output, so -Phase core -Json / -PathsOnly -Json can emit warning/error prose before the JSON object when no feature branch exists or multiple features exist. The bash mirror suppresses branch-validation diagnostics in soft mode. Capture or suppress this call in soft mode, then emit only the final JSON payload on stdout.

$pathsOnlyMode = ($cfg.Extras -match 'paths_only')
# 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
# can guide the user to select a feature.
$featuresJson = Get-FeaturesJson
if ($Json) {
Write-Output "{`"needs_selection`":true,`"features`":$featuresJson}"
Expand All @@ -130,9 +144,11 @@ if ($branchResult -eq "NEEDS_SELECTION") {
Write-Output "Run: /iikit-core use <feature> to select a feature."
}
exit 2
} elseif ($branchResult -eq "ERROR") {
if ($cfg.Extras -match 'status_mode') {
$statusNoFeature = $true
} elseif ($branchResult -eq "ERROR" -or ($branchResult -eq "NEEDS_SELECTION" -and $pathsOnlyMode)) {
if ($softBranchMode) {
# 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
CURRENT_BRANCH = $currentBranch
Expand All @@ -151,8 +167,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
Expand Down Expand Up @@ -269,7 +285,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'
}

Expand All @@ -290,7 +306,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 <description>'
$clearBefore = $false
} elseif (-not $vSpec) {
Expand Down
Loading
Loading