From 3c63224243d84baf3e7242958b7a5c37e46fb63c Mon Sep 17 00:00:00 2001 From: Rik Dryfoos Date: Tue, 4 Aug 2026 14:29:49 -0400 Subject: [PATCH 1/5] Install real spec-kit Claude Code skills (were gitignored, never present on Spudnik). specify integration upgrade claude - the actual .claude/skills/speckit-* skill definitions this project's .specify/integrations/claude.manifest.json already referenced were never committed (per-machine, gitignored) and were missing entirely on the Spudnik host. Confirmed via `specify integration status` before running: 10 managed files missing, 2 genuinely customized (spec-template.md, tasks-template.md - the mandatory-ID-field edits from traceability.md). The upgrade preserved both customizations untouched and only installed the missing skill files. Side effect, reviewed: 6 shared scripts/templates came forward from speckit_version 0.11.9.dev0 to 0.15.3.dev0 in the same pass (real content changes, e.g. common.sh gained a more robust JSON-parser fallback chain), since they weren't flagged as locally modified. Accepted as-is. Co-authored-by: Claude Sonnet 5 --- .specify/extensions/.registry | 4 +- .specify/init-options.json | 4 +- .specify/integration.json | 2 +- .specify/integrations/claude.manifest.json | 24 +-- .specify/integrations/speckit.manifest.json | 14 +- .specify/scripts/bash/check-prerequisites.sh | 20 ++- .specify/scripts/bash/common.sh | 115 +++++++++++-- .specify/scripts/bash/create-new-feature.sh | 163 +++++++++++++++---- .specify/scripts/bash/setup-plan.sh | 15 +- .specify/templates/checklist-template.md | 6 +- .specify/templates/plan-template.md | 2 +- 11 files changed, 277 insertions(+), 92 deletions(-) diff --git a/.specify/extensions/.registry b/.specify/extensions/.registry index a19cfa2..f67f85d 100644 --- a/.specify/extensions/.registry +++ b/.specify/extensions/.registry @@ -12,7 +12,9 @@ "speckit.agent-context.update" ] }, - "registered_skills": [], + "registered_skills": [ + "speckit-agent-context-update" + ], "installed_at": "2026-06-26T14:27:06.775090+00:00" } } diff --git a/.specify/init-options.json b/.specify/init-options.json index 2a307ad..e836523 100644 --- a/.specify/init-options.json +++ b/.specify/init-options.json @@ -5,5 +5,5 @@ "here": true, "integration": "claude", "script": "sh", - "speckit_version": "0.11.9.dev0" -} \ No newline at end of file + "speckit_version": "0.15.3.dev0" +} diff --git a/.specify/integration.json b/.specify/integration.json index 4fffd24..2556803 100644 --- a/.specify/integration.json +++ b/.specify/integration.json @@ -1,5 +1,5 @@ { - "version": "0.11.9.dev0", + "version": "0.15.3.dev0", "integration_state_schema": 1, "installed_integrations": [ "claude" diff --git a/.specify/integrations/claude.manifest.json b/.specify/integrations/claude.manifest.json index ccc09f5..308ba69 100644 --- a/.specify/integrations/claude.manifest.json +++ b/.specify/integrations/claude.manifest.json @@ -1,17 +1,17 @@ { "integration": "claude", - "version": "0.11.9.dev0", - "installed_at": "2026-06-26T14:27:06.733387+00:00", + "version": "0.15.3.dev0", + "installed_at": "2026-08-04T18:24:39.287860+00:00", "files": { - ".claude/skills/speckit-analyze/SKILL.md": "73ec1460968620f0e3e9aa3568d57cc789e2414cf5cac2abbf5fee0d6511b565", - ".claude/skills/speckit-clarify/SKILL.md": "c1c2098756ca407530cca11c5b608f517d769962215ddafa013951b81e3e19c5", - ".claude/skills/speckit-constitution/SKILL.md": "ee3972318415a05559c6bf281dcbd2e8deda944e595d64ab5474abeacf558697", - ".claude/skills/speckit-implement/SKILL.md": "823049e49aa983fe398d4bccf6c686ab6afe8f2cd3856e0380c3ef797d78d56d", - ".claude/skills/speckit-converge/SKILL.md": "04226b8443797337624983111546d5e5a48d9993a176c4e6d72a4099a0af50d4", - ".claude/skills/speckit-plan/SKILL.md": "53733c8a4f4fd01685759bb1c68e94c73da4ce90d549139e79e419dec6471510", - ".claude/skills/speckit-checklist/SKILL.md": "946c6bc808891436972a11a423f89f0fbd272a79809bb8fd1d29f481ebe02613", - ".claude/skills/speckit-specify/SKILL.md": "9324dd55d12d420cd581031419fa37eb94ef75ae0bdd53391dd4414bd9d45e02", - ".claude/skills/speckit-tasks/SKILL.md": "cb29fb8247a30aac751be83de88d0399221692589dd26327552ae6f193816fda", - ".claude/skills/speckit-taskstoissues/SKILL.md": "dfe23aaca349cd76e98505dafa9aae1ef4616a0c35a5c79122b9bd881e16b62f" + ".claude/skills/speckit-analyze/SKILL.md": "e1f8323285ed23a9ec6b74a6436d7ebd23a3511778d5cc181bd27afa2b545427", + ".claude/skills/speckit-clarify/SKILL.md": "f4b3f2c95087ac2343c0b67faff67f7223d34213ca1816aa25908db5b9aff0ac", + ".claude/skills/speckit-constitution/SKILL.md": "2605d00afc53af300ce2efd630bc1103b5fa6acefd3e33ead63fd27b3f63abee", + ".claude/skills/speckit-implement/SKILL.md": "624632825da8f2be64eadb50292506d4f067c785f5a2f866b62e908e582c6b50", + ".claude/skills/speckit-converge/SKILL.md": "65ea8b863835750931b522a9935391da887c4d188f1410113d16efe6396c2214", + ".claude/skills/speckit-plan/SKILL.md": "2fe3f96886e96284965c8586d14df5d226c1e796b3eb39d110c9d8231abe6417", + ".claude/skills/speckit-checklist/SKILL.md": "091388fe7283456088c72e6674bc751b4e745a7fb656a8d6392b4fb3435e8538", + ".claude/skills/speckit-specify/SKILL.md": "42fe016b9183bb8fa7ce7c65e04ea8d382f7f2abfc94849aeead999247675886", + ".claude/skills/speckit-tasks/SKILL.md": "8323958595e60c9111d82d875e6d1e4c86f272481ae010f67876ce525603015f", + ".claude/skills/speckit-taskstoissues/SKILL.md": "82743575ccbd0831e7f7e187785e9ff650fd1ca81feca81520a29d1fff0bea44" } } diff --git a/.specify/integrations/speckit.manifest.json b/.specify/integrations/speckit.manifest.json index cd4dd70..2580bf7 100644 --- a/.specify/integrations/speckit.manifest.json +++ b/.specify/integrations/speckit.manifest.json @@ -1,17 +1,17 @@ { "integration": "speckit", - "version": "0.11.9.dev0", + "version": "0.15.3.dev0", "installed_at": "2026-06-26T14:27:06.744456+00:00", "files": { - ".specify/scripts/bash/common.sh": "af8a16f87b4f9084759c42ff9abf35c0b2a2025dffe58c298758ff86de2923b2", - ".specify/scripts/bash/setup-plan.sh": "4eb12c5b00f5c66a7d01b56c90898d320dcef4425d9b96652d57156c84948eda", + ".specify/scripts/bash/common.sh": "6ff86bf39f6b4684b0f80927dc7a1dadec26b4671988a3fe4d6c2523cbd3aa22", + ".specify/scripts/bash/setup-plan.sh": "4469b22960f43c07c33dca00de6dedb252145e9a9ce8fbb0e63be82e02b082ab", ".specify/scripts/bash/setup-tasks.sh": "cf21ba2212b4dd5b435c5ea8527500cfd27768b86c0bbc7ebc3207759f118d27", - ".specify/scripts/bash/check-prerequisites.sh": "afce0aa8db177320d83aa0b8e3619c06b865fd810781894e4a7a3f81664941ce", - ".specify/scripts/bash/create-new-feature.sh": "9ba116b64f0328eb69bc1a195d209074ea38823a73a554160d69df34a74daa65", + ".specify/scripts/bash/check-prerequisites.sh": "a7d8a14ecf87332b600cd966b5d0e7cb9d594abce7e4d1ee4372b2b5b3efff06", + ".specify/scripts/bash/create-new-feature.sh": "ad09a94a2c1107e25e5386a834da1d7a31f9abb06ab8bfd323a7b84038221e39", ".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3", - ".specify/templates/checklist-template.md": "c37695297e5d3153d64f82c21223509940b13932046c7961c42d1d669516130c", + ".specify/templates/checklist-template.md": "709d8ab8384a3a49f5e0f64479f71553ef6d6f8bb4f00281b05f47837993b536", ".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842", ".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e", - ".specify/templates/plan-template.md": "cc7f7979cf8d8836ec26492785affd80791d3422a2b745062ec695be8c985ef7" + ".specify/templates/plan-template.md": "7e637502d41eccf0ca672496636365691fdca62ef37b27ec07fcb412dbfa90d4" } } diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh index 8377d8e..bf75140 100755 --- a/.specify/scripts/bash/check-prerequisites.sh +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -57,13 +57,13 @@ OPTIONS: EXAMPLES: # Check task prerequisites (plan.md required) ./check-prerequisites.sh --json - + # Check implementation prerequisites (plan.md + tasks.md required) ./check-prerequisites.sh --json --require-tasks --include-tasks - + # Get feature paths only (no validation) ./check-prerequisites.sh --paths-only - + EOF exit 0 ;; @@ -78,8 +78,14 @@ done SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" -# Get feature paths -_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +# Get feature paths. +# In --paths-only mode this is pure resolution, so pass --no-persist to opt out +# of the feature.json write side effect (issue #3025). +if $PATHS_ONLY; then + _paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +else + _paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +fi eval "$_paths_output" unset _paths_output @@ -176,13 +182,13 @@ else # Text output echo "FEATURE_DIR:$FEATURE_DIR" echo "AVAILABLE_DOCS:" - + # Show status of each potential document check_file "$RESEARCH" "research.md" check_file "$DATA_MODEL" "data-model.md" check_dir "$CONTRACTS_DIR" "contracts/" check_file "$QUICKSTART" "quickstart.md" - + if $INCLUDE_TASKS; then check_file "$TASKS" "tasks.md" fi diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh index 70ab89b..dc60f9f 100755 --- a/.specify/scripts/bash/common.sh +++ b/.specify/scripts/bash/common.sh @@ -97,17 +97,26 @@ read_feature_json_feature_directory() { local fj="$repo_root/.specify/feature.json" [[ -f "$fj" ]] || { printf '%s' ''; return 0; } + # Try parsers in order (jq -> python3 -> grep/sed), falling through on + # failure. Selection is by *parse success*, not mere availability: on + # Windows `python3` commonly resolves to the Microsoft Store App Execution + # Alias stub, which passes `command -v` but fails at runtime (exit 49), so + # an availability-gated `elif` would pick python3, swallow its failure, and + # never reach the grep/sed fallback -- leaving feature.json unreadable even + # though it is valid (issue #3304). local _fd='' if command -v jq >/dev/null 2>&1; then if ! _fd=$(jq -r '.feature_directory // empty' "$fj" 2>/dev/null); then _fd='' fi - elif command -v python3 >/dev/null 2>&1; then + fi + if [[ -z "$_fd" ]] && command -v python3 >/dev/null 2>&1; then # Use Python so pretty-printed/multi-line JSON still parses correctly. if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then _fd='' fi - else + fi + if [[ -z "$_fd" ]]; then # Last-resort single-line grep/sed fallback. The `|| true` guards against # grep returning 1 (no match) aborting under `set -e` / `pipefail`. _fd=$( { grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } \ @@ -152,6 +161,15 @@ _persist_feature_json() { } get_feature_paths() { + # Read-only callers (e.g. check-prerequisites.sh --paths-only) pass + # --no-persist so pure path resolution never writes .specify/feature.json, + # which would dirty the working tree or overwrite a pinned value (issue #3025). + local no_persist=false + if [[ "${1:-}" == "--no-persist" ]]; then + no_persist=true + shift + fi + # Split decl/assignment so a SPECIFY_INIT_DIR validation failure in # get_repo_root propagates as a hard error instead of being masked by `local`. local repo_root @@ -168,8 +186,11 @@ get_feature_paths() { feature_dir="$SPECIFY_FEATURE_DIRECTORY" # Normalize relative paths to absolute under repo root [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir" - # Persist to feature.json so future sessions without the env var still work - _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY" + # Persist to feature.json so future sessions without the env var still + # work — unless the caller opted out for read-only resolution (#3025). + if [[ "$no_persist" != true ]]; then + _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY" + fi elif [[ -f "$repo_root/.specify/feature.json" ]]; then local _fd _fd=$(read_feature_json_feature_directory "$repo_root") @@ -186,6 +207,15 @@ get_feature_paths() { return 1 fi + # When no branch context exists (no SPECIFY_FEATURE, feature resolved via + # SPECIFY_FEATURE_DIRECTORY or feature.json), fall back to the feature + # directory basename so CURRENT_BRANCH is a usable identifier rather than + # an empty, misleading value (issue #3026). + if [[ -z "$current_branch" ]]; then + local feature_dir_trimmed="${feature_dir%/}" + current_branch="${feature_dir_trimmed##*/}" + fi + # Use printf '%q' to safely quote values, preventing shell injection # via crafted branch names or paths containing special characters printf 'REPO_ROOT=%q\n' "$repo_root" @@ -214,21 +244,29 @@ get_invoke_separator() { local integration_json="$repo_root/.specify/integration.json" local separator="." - local parsed_with_jq=0 + local parsed=0 if [[ -f "$integration_json" ]]; then + # Try parsers in order (jq -> python3 -> awk), falling through on + # failure. Selection is by *parse success*, not mere availability: on + # Windows `python3` commonly resolves to the Microsoft Store App + # Execution Alias stub, which passes `command -v` but fails at runtime + # (exit 49). An availability-gated branch would pick python3, swallow + # its failure, and — because this function historically had no text + # fallback — silently return "." even for `-`-separator integrations + # (e.g. forge, cline), yielding wrong command hints (issue #3304). if command -v jq >/dev/null 2>&1; then local jq_separator if jq_separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then - parsed_with_jq=1 case "$jq_separator" in - "."|"-") separator="$jq_separator" ;; + "."|"-") separator="$jq_separator"; parsed=1 ;; esac fi fi - if [[ "$parsed_with_jq" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then - if separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null + if [[ "$parsed" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then + local py_separator + if py_separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null import json import sys @@ -244,17 +282,64 @@ try: separator = entry["invoke_separator"] print(separator) except Exception: - print(".") + sys.exit(1) PY ); then - case "$separator" in - "."|"-") ;; - *) separator="." ;; + case "$py_separator" in + "."|"-") separator="$py_separator"; parsed=1 ;; esac - else - separator="." fi fi + + if [[ "$parsed" -eq 0 ]]; then + # Last-resort text fallback for environments with neither jq nor a + # working python3 (e.g. stock Windows + Git Bash). Reads the active + # integration key (default_integration, else integration) and its + # invoke_separator from within the integration_settings object. + # Handles both pretty-printed (the written form) and compact JSON. + # Accumulate all lines into one buffer in END rather than using + # gawk-only whole-file slurp (RS="^$"), so this stays portable to + # the BSD awk on macOS. + local awk_separator + awk_separator=$(awk ' + function keyval(d, name, v) { + if (match(d, "\"" name "\"[ \t\r\n]*:[ \t\r\n]*\"[^\"]*\"")) { + v=substr(d,RSTART,RLENGTH); sub(/^.*:[ \t\r\n]*"/,"",v); sub(/"$/,"",v); return v + } + return "" + } + { doc = doc $0 "\n" } + END { + key=keyval(doc,"default_integration"); if (key=="") key=keyval(doc,"integration") + sep="." + if (key!="") { + settings=doc + if (match(doc, /"integration_settings"[ \t\r\n]*:[ \t\r\n]*[{]/)) { + settings=substr(doc, RSTART+RLENGTH-1) + } + if (match(settings, "\"" key "\"[ \t\r\n]*:[ \t\r\n]*[{]")) { + start=RSTART+RLENGTH-1 + depth=0 + obj="" + for (i=start; i<=length(settings); i++) { + c=substr(settings,i,1) + obj=obj c + if (c=="{") depth++ + else if (c=="}") { depth--; if (depth==0) break } + } + if (match(obj, /"invoke_separator"[ \t\r\n]*:[ \t\r\n]*"[-.]"/)) { + tok=substr(obj,RSTART,RLENGTH); s=substr(tok,length(tok)-1,1) + if (s=="." || s=="-") sep=s + } + } + } + print sep + } + ' "$integration_json" 2>/dev/null) + case "$awk_separator" in + "."|"-") separator="$awk_separator" ;; + esac + fi fi _SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root" diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh index c960976..c1b189d 100755 --- a/.specify/scripts/bash/create-new-feature.sh +++ b/.specify/scripts/bash/create-new-feature.sh @@ -8,6 +8,7 @@ ALLOW_EXISTING=false SHORT_NAME="" BRANCH_NUMBER="" USE_TIMESTAMP=false +NUMBER_EXPLICIT=false ARGS=() i=1 while [ $i -le $# ]; do @@ -48,6 +49,9 @@ while [ $i -le $# ]; do exit 1 fi BRANCH_NUMBER="$next_arg" + if [ -n "$BRANCH_NUMBER" ]; then + NUMBER_EXPLICIT=true + fi ;; --timestamp) USE_TIMESTAMP=true @@ -60,7 +64,7 @@ while [ $i -le $# ]; do echo " --dry-run Compute feature name and paths without creating directories or files" echo " --allow-existing-branch Reuse an existing feature directory if it already exists" echo " --short-name Provide a custom short name (2-4 words) for the feature" - echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --number N Prefer a feature number (auto-corrected if its specs prefix exists)" echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering" echo " --help, -h Show this help message" echo "" @@ -90,11 +94,25 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then exit 1 fi +MAX_FEATURE_NUMBER=9223372036854775807 +MAX_BRANCH_LENGTH=244 + +is_feature_number_in_range() { + local value="$1" + local normalized="${value#"${value%%[!0]*}"}" + [ -n "$normalized" ] || normalized=0 + [ ${#normalized} -lt ${#MAX_FEATURE_NUMBER} ] && return 0 + [ ${#normalized} -gt ${#MAX_FEATURE_NUMBER} ] && return 1 + # Equal-length digit strings must be compared without arithmetic overflow. + # shellcheck disable=SC2071 + [[ "$normalized" < "$MAX_FEATURE_NUMBER" || "$normalized" == "$MAX_FEATURE_NUMBER" ]] +} + # Function to get highest number from specs directory get_highest_from_specs() { local specs_dir="$1" local highest=0 - + if [ -d "$specs_dir" ]; then for dir in "$specs_dir"/*; do [ -d "$dir" ] || continue @@ -102,23 +120,66 @@ get_highest_from_specs() { # Match sequential prefixes (>=3 digits), but skip timestamp dirs. if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then number=$(echo "$dirname" | grep -Eo '^[0-9]+') - number=$((10#$number)) - if [ "$number" -gt "$highest" ]; then - highest=$number + if is_feature_number_in_range "$number"; then + number=$((10#$number)) + if [ "$number" -gt "$highest" ]; then + highest=$number + fi fi fi done fi - + echo "$highest" } +# Return success when a spec directory owns the given numeric prefix. +spec_prefix_exists() { + local specs_dir="$1" + local feature_num="$2" + + for spec_path in "$specs_dir/${feature_num}-"*; do + [ -d "$spec_path" ] && return 0 + done + return 1 +} + # Function to clean and format a branch name clean_branch_name() { local name="$1" echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' } +# Fit a feature prefix and suffix within GitHub's branch-name limit. +fit_branch_name() { + local feature_num="$1" + local branch_suffix="$2" + local branch_name="${feature_num}-${branch_suffix}" + + if [ ${#branch_name} -gt $MAX_BRANCH_LENGTH ]; then + local prefix_length=$(( ${#feature_num} + 1 )) + local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length)) + local truncated_suffix + truncated_suffix=$(printf '%s' "$branch_suffix" | cut -c "1-$max_suffix_length" | sed 's/-$//') + branch_name="${feature_num}-${truncated_suffix}" + fi + + printf '%s' "$branch_name" +} + +# Quote a value for POSIX shell reuse, byte-identical to Python's shlex.quote +# so the persistence hints match the Python variant exactly (printf %q output +# differs between bash versions and from shlex.quote for spaces/metachars). +shell_quote() { + local value="$1" LC_ALL=C + if [[ "$value" =~ ^[A-Za-z0-9_@%+=:,./-]+$ ]]; then + printf '%s' "$value" + else + local q="'\"'\"'" + printf "'%s'" "${value//\'/$q}" + fi +} + # Resolve repository root using common.sh functions which prioritize .specify SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" @@ -135,35 +196,37 @@ fi # Function to generate branch name with stop word filtering and length filtering generate_branch_name() { local description="$1" - + # Common stop words to filter out local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" - + # Convert to lowercase and split into words - local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') - + local clean_name=$(printf '%s' "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') + # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original) local meaningful_words=() for word in $clean_name; do # Skip empty words [ -z "$word" ] && continue - + # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms) if ! echo "$word" | grep -qiE "$stop_words"; then if [ ${#word} -ge 3 ]; then meaningful_words+=("$word") - elif echo "$description" | grep -q "\b${word^^}\b"; then - # Keep short words if they appear as uppercase in original (likely acronyms) + # Keep short words that appear as an uppercase acronym in the original. + # Uppercase via tr and match with grep -w (both portable) rather than + # bash's 4+ "^^" case expansion (breaks on macOS bash 3.2) and \b (non-POSIX). + elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then meaningful_words+=("$word") fi fi done - + # If we have meaningful words, use first 3-4 of them if [ ${#meaningful_words[@]} -gt 0 ]; then local max_words=3 if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi - + local result="" local count=0 for word in "${meaningful_words[@]}"; do @@ -200,34 +263,64 @@ if [ "$USE_TIMESTAMP" = true ]; then FEATURE_NUM=$(date +%Y%m%d-%H%M%S) BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" else + if [ -n "$BRANCH_NUMBER" ] && [[ ! "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then + echo "Error: --number must be an unsigned integer, got '$BRANCH_NUMBER'" >&2 + exit 1 + fi + + # Bash arithmetic is signed 64-bit; reject digit strings that would wrap. + if [ -n "$BRANCH_NUMBER" ] && ! is_feature_number_in_range "$BRANCH_NUMBER"; then + echo "Error: --number must be between 0 and $MAX_FEATURE_NUMBER, got '$BRANCH_NUMBER'" >&2 + exit 1 + fi + # Determine branch number from existing feature directories if [ -z "$BRANCH_NUMBER" ]; then HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + if [ "$HIGHEST" -eq "$MAX_FEATURE_NUMBER" ]; then + echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2 + exit 1 + fi BRANCH_NUMBER=$((HIGHEST + 1)) fi # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") - BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" + + # Treat an explicit number as a preference when its prefix is already used + # by a feature directory. Auto-detected numbers are already conflict-free. + if [ "$NUMBER_EXPLICIT" = true ]; then + SPEC_CONFLICT=false + REQUESTED_BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX") + REQUESTED_DIR="$SPECS_DIR/$REQUESTED_BRANCH_NAME" + if [ "$ALLOW_EXISTING" != true ] || [ ! -d "$REQUESTED_DIR" ]; then + spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" && SPEC_CONFLICT=true + fi + + if [ "$SPEC_CONFLICT" = true ]; then + REQUESTED_NUM="$FEATURE_NUM" + HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + BRANCH_NUMBER=$HIGHEST + while true; do + if [ "$BRANCH_NUMBER" -eq "$MAX_FEATURE_NUMBER" ]; then + echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2 + exit 1 + fi + BRANCH_NUMBER=$((BRANCH_NUMBER + 1)) + FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") + spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" || break + done + >&2 echo "[specify] Warning: --number $REQUESTED_NUM conflicts with an existing spec directory; using $FEATURE_NUM instead" + fi + fi + fi # GitHub enforces a 244-byte limit on branch names # Validate and truncate if necessary -MAX_BRANCH_LENGTH=244 -if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then - # Calculate how much we need to trim from suffix - # Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4 - PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 )) - MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH)) - - # Truncate suffix at word boundary if possible - TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) - # Remove trailing hyphen if truncation created one - TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') - - ORIGINAL_BRANCH_NAME="$BRANCH_NAME" - BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" - +ORIGINAL_BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" +BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX") +if [ "$BRANCH_NAME" != "$ORIGINAL_BRANCH_NAME" ]; then >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" @@ -262,8 +355,8 @@ if [ "$DRY_RUN" != true ]; then _persist_feature_json "$REPO_ROOT" "$FEATURE_DIR" # Inform the user how to set feature state in their own shell - printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2 - printf '# export SPECIFY_FEATURE_DIRECTORY=%q\n' "$FEATURE_DIR" >&2 + printf '# To persist: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" >&2 + printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")" >&2 fi if $JSON_MODE; then @@ -293,7 +386,7 @@ else echo "SPEC_FILE: $SPEC_FILE" echo "FEATURE_NUM: $FEATURE_NUM" if [ "$DRY_RUN" != true ]; then - printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" - printf '# export SPECIFY_FEATURE_DIRECTORY=%q\n' "$FEATURE_DIR" + printf '# To persist in your shell: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" + printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")" fi fi diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh index cb67943..e01dc44 100755 --- a/.specify/scripts/bash/setup-plan.sh +++ b/.specify/scripts/bash/setup-plan.sh @@ -8,17 +8,17 @@ ARGS=() for arg in "$@"; do case "$arg" in - --json) - JSON_MODE=true + --json) + JSON_MODE=true ;; - --help|-h) + --help|-h) echo "Usage: $0 [--json]" echo " --json Output results in JSON format" echo " --help Show this help message" - exit 0 + exit 0 ;; - *) - ARGS+=("$arg") + *) + ARGS+=("$arg") ;; esac done @@ -77,8 +77,7 @@ if $JSON_MODE; then fi else echo "FEATURE_SPEC: $FEATURE_SPEC" - echo "IMPL_PLAN: $IMPL_PLAN" + echo "IMPL_PLAN: $IMPL_PLAN" echo "SPECS_DIR: $FEATURE_DIR" echo "BRANCH: $CURRENT_BRANCH" fi - diff --git a/.specify/templates/checklist-template.md b/.specify/templates/checklist-template.md index c4aa166..e64065d 100644 --- a/.specify/templates/checklist-template.md +++ b/.specify/templates/checklist-template.md @@ -6,16 +6,16 @@ **Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements. - diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md index 92b96c7..be1aa88 100644 --- a/.specify/templates/plan-template.md +++ b/.specify/templates/plan-template.md @@ -4,7 +4,7 @@ **Input**: Feature specification from `/specs/[###-feature-name]/spec.md` -**Note**: This template is filled in by the `/speckit-plan` command. See `.specify/templates/plan-template.md` for the execution workflow. +**Note**: This template is filled in by the `/speckit-plan` command; its definition describes the execution workflow. ## Summary From d589bb7f4fb520df0caabdb56c6b352abc1fef55 Mon Sep 17 00:00:00 2001 From: Rik Dryfoos Date: Thu, 6 Aug 2026 13:49:14 -0400 Subject: [PATCH 2/5] checkpoint before checking out main --- .cursor/rules/cannon-eng02-context.mdc | 40 +++++++++++++ .cursor/rules/newbie-homesflow.mdc | 64 +++++++++++++++++++++ .potato/worktrees/HOM-1 | 1 + .potato/worktrees/HOM-2 | 1 + .potato/worktrees/HOM-3 | 1 + specs/000-cannon-smoketest/plan.md | 45 +++++++++++++++ specs/000-cannon-smoketest/spec.md | 78 ++++++++++++++++++++++++++ 7 files changed, 230 insertions(+) create mode 100644 .cursor/rules/cannon-eng02-context.mdc create mode 100644 .cursor/rules/newbie-homesflow.mdc create mode 160000 .potato/worktrees/HOM-1 create mode 160000 .potato/worktrees/HOM-2 create mode 160000 .potato/worktrees/HOM-3 create mode 100644 specs/000-cannon-smoketest/plan.md create mode 100644 specs/000-cannon-smoketest/spec.md diff --git a/.cursor/rules/cannon-eng02-context.mdc b/.cursor/rules/cannon-eng02-context.mdc new file mode 100644 index 0000000..c324741 --- /dev/null +++ b/.cursor/rules/cannon-eng02-context.mdc @@ -0,0 +1,40 @@ +--- +description: What Potato Cannon / ENG-02 left in this repo, and how to read it +globs: specs/002-smoke-build-info/**,.potato/**,.claude/** +alwaysApply: false +--- + +# What Cannon is doing to this repo + +If you see `specs/002-smoke-build-info/`, a `.potato/` folder, or branches named `potato/HOM-*`, that is not stray product work — it is evidence of ENG-02, run against HomesFlow on purpose. Do not mistake it for real product scope. + +## Short version + +**Potato Cannon** (`potato-cannon` repo) is a kanban + agent orchestrator that runs Claude sessions through workflow phases with human approval gates. Rik uses it as the delivery mechanism for HomesFlow tickets going forward. + +**Golden Thread** is HomesFlow's layer on Spec-Kit: immutable IDs (`FR-`/`AC-` per `traceability.md` §3) and Gate 1 / Gate 2 (`traceability.md` §6). Spec-Kit itself does not know about it. + +**ENG-02** (spudnik) hypothesizes Cannon can drive HomesFlow's *real* installed spec-kit (`.claude/skills/speckit-*`) plus Golden Thread — not a hand-rolled approximation — with a kanban view and approval gates. + +## Reference example: HOM-2 + +HOM-2 ("Stand up the Walking Skeleton") was the first end-to-end run of the `homesflow-speckit` template. Deliberately trivial (`FR-SMOKE-01`, `SMOKE` domain) — *not* real product scope, exempted from PRD-sourcing for that ticket only. Done. Read `specs/002-smoke-build-info/` for what a Cannon-driven ticket produces. + +Notable from that run: real skills invoked (not reimplemented); Gate 1 caught `plan.md` self-certifying IDs missing from `spec.md`; a mid-run `chat_ask` was answered via Cannon's dashboard and the session resumed. + +## Design decisions (Cannon side) + +- Phase gates only where `traceability.md` §6 says work stops (before/after implement). Specify → Clarify → Plan → Tasks → Analyze = one continuous resumed session. +- No adversarial retry loops (`ralphLoop`/`taskLoop`). Real `speckit-implement` / `speckit-converge` already own that judgment. +- Promote/demote resumes the Claude session with a digest of new comments/Q&A/artifacts — not a full context rebuild. + +## Still open + +**Thread Cannon** (fork Cannon into a Spec-Kit + Golden Thread–specific tool) was deferred until the walking skeleton finished; that decision is live again, not settled. + +## Go deeper + +- `potato-cannon` + its CLAUDE.md +- `potato-cannon/apps/daemon/templates/workflows/homesflow-speckit/` +- spudnik `OBJECTIVES.md` (ENG-02), `docs/field-notes-pipeline.md` (FN-05) +- This repo's `traceability.md` diff --git a/.cursor/rules/newbie-homesflow.mdc b/.cursor/rules/newbie-homesflow.mdc new file mode 100644 index 0000000..7b98f9a --- /dev/null +++ b/.cursor/rules/newbie-homesflow.mdc @@ -0,0 +1,64 @@ +--- +description: HomesFlow trial engineer orientation — Spec Kit, Golden Thread, craft/PR hygiene +alwaysApply: true +--- + +# Context Primer — HomesFlow trial engineer + +You are joining HomesFlow as a trial collaborator. Your job is to deliver a thin slice the HomesFlow way: Spec Kit discipline, Golden Thread integrity, then craft/PR hygiene. Do not try to match prior agent diffs or invent product scope. + +## How to work with Rik +- High-signal, direct, verify before asserting. +- Self-serve from the repo. Ask only when the PRD is silent or something is broken/off-repo. + +## Read first (in order) +1. `CLAUDE.md` — entry + hard rules +2. `.specify/memory/constitution.md` — process/architecture law (Hierarchy of Truth) +3. `traceability.md` — Gate 2 mechanics +4. `HomesFlow.prd.md` — **product scope only** +5. `specs/001-mvp/dev-notes.md` — env + operational state +6. `specs/001-mvp/craft-conventions.md` — craft + delivery + +## Hard rules +- **Product scope** comes only from `HomesFlow.prd.md`. Do not invent, extrapolate, or assume requirements. If the PRD is silent, ask or propose a PRD change — do not ship guessed behavior. +- **Process/architecture** comes from the constitution. Do not invent product behavior from the constitution, glossary, story map, or craft docs. +- **Story map** (StoriesOnBoard) is a human planning aid only — not Gate-2-enforced. Promote into the PRD when it becomes product truth. +- **Gate 2** proves the PRD is *true of the system* (IDs ↔ `@covers` ↔ `test_AC_*` or tracked debt). It does not prove the PRD is the right product. + +## Spec Kit loop (expected) +For the assigned thin slice: +1. Confirm the authoritative IDs in `HomesFlow.prd.md` (Rik amends the PRD first if needed). +2. Fan into `specs/001-mvp/` (spec → plan → tasks) without minting new requirement IDs in the feature folder. +3. Implement only what tasks authorize; keep `Traces:` on every task. +4. Prove with `@covers` and `test_AC_*` (or leave unchecked tasks as tracked debt — never silent gaps). +5. Run `bash scripts/check-traceability.sh` before you call the slice done. + +## Golden Thread (commands) +```bash +bash scripts/check-traceability.sh # must PASS +bash scripts/check-traceability.sh --json # per-ID detail +``` +Coverage matrix (`coverage.md`) is a portfolio snapshot; CI enforces the gate script, not file freshness. + +## Craft / PR hygiene +```text +feature branch → PR → craft-gate (+ Sonar) green → merge to main +``` +- Branch naming for cloud agents: `cursor/-7716` +- Fill `.github/pull_request_template.md` (task IDs, Traces, Gate 2 evidence) +- Do not push product/craft changes straight to `main` +- Follow `craft-conventions.md` (SwiftLint, policy objects where logic is testable) + +## Your trial assignment +Rik will give you **one micro acceptance criterion** newly added to the PRD (atomic, unit-testable). Take it through Spec Kit → implementation → Gate 2 green → PR. + +**Success looks like:** clean Spec Kit artifacts, unbroken golden thread for that ID, and a reviewable PR. +**Non-goals:** matching a prior check-in’s code; rebuilding the thread visualizer; expanding scope “while you’re in there.” + +## Local assumptions (confirm if relevant) +- Supabase local: `127.0.0.1:54321`; `Secrets.xcconfig` from `.example` +- Xcode: `cd ios && xcodegen generate` +- Paths: `ios/HomesFlow/`, `ios/HomesFlowTests/` + +## Starting the session +Confirm: (1) you have the micro-AC ID and PRD text, (2) local vs cloud env, (3) any blockers not in `dev-notes`. Then proceed. diff --git a/.potato/worktrees/HOM-1 b/.potato/worktrees/HOM-1 new file mode 160000 index 0000000..9bd6ae5 --- /dev/null +++ b/.potato/worktrees/HOM-1 @@ -0,0 +1 @@ +Subproject commit 9bd6ae50b9968f4d6bdfb7c8239c2fbb65077676 diff --git a/.potato/worktrees/HOM-2 b/.potato/worktrees/HOM-2 new file mode 160000 index 0000000..9bd6ae5 --- /dev/null +++ b/.potato/worktrees/HOM-2 @@ -0,0 +1 @@ +Subproject commit 9bd6ae50b9968f4d6bdfb7c8239c2fbb65077676 diff --git a/.potato/worktrees/HOM-3 b/.potato/worktrees/HOM-3 new file mode 160000 index 0000000..9bd6ae5 --- /dev/null +++ b/.potato/worktrees/HOM-3 @@ -0,0 +1 @@ +Subproject commit 9bd6ae50b9968f4d6bdfb7c8239c2fbb65077676 diff --git a/specs/000-cannon-smoketest/plan.md b/specs/000-cannon-smoketest/plan.md new file mode 100644 index 0000000..049a2a3 --- /dev/null +++ b/specs/000-cannon-smoketest/plan.md @@ -0,0 +1,45 @@ +**Loop 1 pipeline-test exception** — not real product scope. Granted by Rik, 2026-08-04. +See spudnik `OBJECTIVES.md` ENG-02 and `docs/field-notes-pipeline.md` FN-05. + +# Implementation Plan: Cannon Pipeline Smoke Test — Build-Diagnostics Constant + +**Feature Branch**: `000-cannon-smoketest` + +**Spec**: `specs/000-cannon-smoketest/spec.md` + +**Status**: Draft + +## Summary + +Add a single read-only accessor, `BuildInfo.summary`, that combines the app's `CFBundleShortVersionString` and `CFBundleVersion` (from `Info.plist`) into one `" ()"` string, with `"unknown"` substituted per-component when a value is missing. This satisfies `FR-SMOKE-01` / `AC-SMOKE-01a`. No UI wiring, no new dependencies, no persistence. + +## Where the code goes + +- **`ios/HomesFlow/Core/Observability/BuildInfo.swift`** (new file) + Placed alongside `CrashReporting.swift`, the existing diagnostics-flavored accessor in `Core/Observability`, since both read values out of the app bundle for build/crash diagnostics purposes and neither belongs to a Feature module. + - `enum BuildInfo` with a static computed property `summary: String`. + - Reads `CFBundleShortVersionString` and `CFBundleVersion` via `Bundle.main.object(forInfoDictionaryKey:)`, mirroring the existing `Bundle.main.object(forInfoDictionaryKey:) as? String` pattern already used in `CrashReporting.swift`. + - Each component independently falls back to the literal string `"unknown"` when absent, empty, or not castable to `String` (per Clarifications). + - Format: `"\(version) (\(build))"`. + +- **`ios/HomesFlowTests/BuildInfoTests.swift`** (new file) + Follows the existing `HomesFlowTests` convention (`XCTest` + `@testable import HomesFlow`, one `test_AC__...` method per acceptance scenario — see `HomeValidatorTests.swift`). + +## Dependencies + +None beyond `Foundation` (`Bundle`). No changes to `Info.plist` — it already declares `CFBundleShortVersionString` (`1.0`) and `CFBundleVersion` (`1`) at `ios/HomesFlow/Resources/Info.plist`. No changes to the Xcode project structure beyond adding the two new files to their existing targets (`HomesFlow` app target, `HomesFlowTests` test target). + +## Testing approach for AC-SMOKE-01a + +`AC-SMOKE-01a`: given Info.plist declares a bundle short version string and a bundle version, `BuildInfo.summary` returns them combined as `" ()"`. + +- **Test**: `HomesFlowTests/BuildInfoTests.swift::test_AC_SMOKE_01a_summary_combines_version_and_build()` +- Reads `BuildInfo.summary` directly (no UI, no network, no auth — matches the spec's "Independent Test" note). +- Since the test target runs against the real app bundle (`Bundle.main` resolves to the `HomesFlow` app bundle under `HomesFlowTests`), the test asserts against the actual `Info.plist` values already present (`"1.0 (1)"`), rather than mocking `Bundle`. This keeps the test minimal and avoids introducing a `Bundle`-injection seam that nothing else in the codebase needs yet. +- The missing-value fallback (Edge Cases) is implemented in `BuildInfo.swift` but intentionally left untested per Clarifications ("one test, happy path only"). + +## Out of scope (per spec Assumptions) + +- No Settings/About screen wiring. +- No changes to `HomesFlow.prd.md` or `specs/001-mvp/`. +- No design review. diff --git a/specs/000-cannon-smoketest/spec.md b/specs/000-cannon-smoketest/spec.md new file mode 100644 index 0000000..d7d1111 --- /dev/null +++ b/specs/000-cannon-smoketest/spec.md @@ -0,0 +1,78 @@ +**Loop 1 pipeline-test exception** — not real product scope. Granted by Rik, 2026-08-04. +See spudnik `OBJECTIVES.md` ENG-02 and `docs/field-notes-pipeline.md` FN-05. + +# Feature Specification: Cannon Pipeline Smoke Test — Build-Diagnostics Constant + +**Feature Branch**: `000-cannon-smoketest` + +**Created**: 2026-08-04 + +**Status**: Draft + +**Input**: Ticket HOM-1 — "smoke-test the homesflow-sdd Cannon workflow end to end (Specify -> Clarify -> Plan -> Tasks/Gate1 -> Implement -> Gate2), not to ship a real feature." Suggested minimal scope taken as-is: expose a small build-diagnostics value in the iOS app (a `BuildInfo.summary` string combining bundle version + build number), covered by one atomic acceptance criterion and one test. + +> This spec mints `FR-SMOKE-01` / `AC-SMOKE-01a` locally under the `SMOKE` domain, per exception granted above — the normal rule ("IDs are assigned at the PRD level; feature specs inherit them, never mint") does not apply here because there is no PRD entry to inherit from. This is the one place in the pipeline where minting is allowed. + +## Intended Use + +Engineers and CI running the Cannon pipeline use this feature to verify that the full Specify → Clarify → Plan → Tasks/Gate1 → Implement → Gate2 chain produces working, traceable, tested code — not to deliver end-user value. The only "user" of the shipped artifact is a developer or support engineer who wants to read the app's build version and build number as a single string (e.g. for a settings/about screen or a crash report) without duplicating the two individually. + +## Risk & failure modes + +| Failure | User impact | Mitigation / trace | +|---------|-------------|-------------------| +| `summary` reads a missing/malformed Info.plist key | Crash or garbled string in diagnostics output | Provide a safe fallback string when version or build values are absent; covered by AC-SMOKE-01a | +| Pipeline treats this smoke-test scope as real product scope and edits `HomesFlow.prd.md` / `specs/001-mvp/` | Corrupts the golden thread for the real MVP feature | Explicit exception banner at top of this spec; all work confined to `specs/000-cannon-smoketest/` | + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Read a combined build-diagnostics string (Priority: P1) + +As a developer or support engineer, I want a single string that combines the app's bundle version and build number, so I can identify exactly which build produced a given diagnostic or crash report without cross-referencing two separate values. + +**Why this priority**: It is the only story in this smoke-test feature — the pipeline needs one small, self-contained slice to exercise end-to-end. + +**Independent Test**: Can be fully tested by reading `BuildInfo.summary` in isolation (no UI, no network, no auth) and asserting it renders the expected `" ()"` format from known Info.plist values. + +**Acceptance Scenarios**: + +1. **Given** the app's Info.plist declares a bundle short version string and a bundle version (build number), **When** `BuildInfo.summary` is read, **Then** it returns those two values combined into one human-readable string in the form `" ()"`. + +--- + +### Edge Cases + +- What happens when the bundle version or build number is missing from Info.plist (e.g. in a malformed test bundle)? `BuildInfo.summary` MUST NOT crash — it substitutes a placeholder (e.g. `"unknown"`) for the missing component. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-SMOKE-01**: The iOS app MUST expose a `BuildInfo.summary` string that combines the app's bundle short version string and bundle version (build number) into one human-readable value, with a safe fallback when either source value is unavailable. + +### Key Entities *(include if feature involves data)* + +- **BuildInfo**: A read-only diagnostics accessor. Not a persisted entity — it derives `summary` from the app bundle's `Info.plist` (`CFBundleShortVersionString`, `CFBundleVersion`) at read time. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: `BuildInfo.summary` returns a non-empty string in the `" ()"` format for a normally-configured build, verified by one automated unit test traced to `AC-SMOKE-01a`. +- **SC-002**: The full Cannon pipeline (Specify → Clarify → Plan → Tasks/Gate1 → Implement → Gate2) completes for this ticket without touching `HomesFlow.prd.md` or `specs/001-mvp/`. + +## Assumptions + +- This is a pipeline smoke test, not a real feature; no design review, UI surface, or PRD entry is expected or required. +- `BuildInfo.summary` need not be surfaced in any screen for this smoke test — the acceptance criterion is satisfied by the accessor and its unit test alone. Wiring it into a Settings/About screen is out of scope unless a later ticket asks for it. +- The iOS app's `Info.plist` already carries standard `CFBundleShortVersionString` / `CFBundleVersion` keys (confirmed present at `ios/HomesFlow/Resources/Info.plist`), so no new build-configuration work is needed to source these values. + +## Clarifications + +### Session 2026-08-04 + +- **Q**: Edge case wording: when a component (bundle version or build number) is missing from Info.plist, BuildInfo.summary "substitutes a placeholder (e.g. 'unknown') for the missing component." Should the exact placeholder be the literal string "unknown" (e.g. "unknown (42)" or "1.2.3 (unknown)"), or do you want a different fallback value/format? + **A**: Use literal "unknown" per missing component, as drafted + +- **Q**: The ticket scopes this to "one atomic acceptance criterion and one test." The spec's only formal Acceptance Scenario (AC-SMOKE-01a) covers the happy path (both values present). The missing-value fallback is described in Edge Cases as a MUST-NOT-crash requirement, but not as its own AC. Should the single required automated test cover ONLY the happy-path AC-SMOKE-01a (with the fallback behavior left as documented-but-untested), or do you want that one test to also exercise the missing-value fallback case? + **A**: One test, happy path only (AC-SMOKE-01a) — fallback stays documented but untested From 1caa3aa1c9c07d31fb9f5a681239c128bb6f190f Mon Sep 17 00:00:00 2001 From: Rik Dryfoos Date: Thu, 6 Aug 2026 19:10:19 -0400 Subject: [PATCH 3/5] Cold-agent trial: stock Spec Kit re-init + Clewseau bundle, deliver AC-HOME-15 end to end. A fresh agent with no HomesFlow context re-initialized stock Spec Kit, installed the Clewseau bundle, minted US/FR/AC for the home display-name normalization story from plain PRD prose, and carried it through spec, tasks, implementation, tests, and a passing Gate 2 emit. Includes the break/fix probe writeup (renamed proof correctly flipped the row to tracked-debt via an open Traces: task, then back to verified). Co-authored-by: Cursor --- .specify/extensions.yml | 21 +- .specify/extensions/.registry | 14 +- .specify/extensions/agent-context/README.md | 66 --- .../agent-context/agent-context-config.yml | 5 - .../commands/speckit.agent-context.update.md | 27 -- .../extensions/agent-context/extension.yml | 34 -- .../scripts/bash/update-agent-context.sh | 282 ----------- .../powershell/update-agent-context.ps1 | 353 -------------- .../speckit-clewseau-gate-check/SKILL.md | 34 ++ .specify/extensions/clewseau-gate/README.md | 11 + .../clewseau-gate/clewseau-gate-config.yml | 31 ++ .../commands/speckit.clewseau-gate.check.md | 29 ++ .../clewseau-gate/config-template.yml | 31 ++ .../extensions/clewseau-gate/extension.yml | 43 ++ .../scripts/check-traceability.sh | 437 ++++++++++++++++++ .specify/feature.json | 4 +- .specify/integrations/claude.manifest.json | 2 +- .specify/integrations/speckit.manifest.json | 2 +- .specify/memory/.constitution-template.json | 4 + .specify/memory/constitution.md | 97 ++-- .specify/presets/.registry | 15 + .specify/presets/clewseau/GLOSSARY.md | 17 + .specify/presets/clewseau/README.md | 13 + .specify/presets/clewseau/preset.yml | 40 ++ .../templates/constitution-template.md | 28 ++ .../clewseau/templates/spec-template.md | 19 + .../clewseau/templates/tasks-template.md | 11 + .specify/templates/spec-template.md | 25 +- .specify/templates/tasks-template.md | 1 - .specify/workflows/speckit/workflow.yml | 1 + .specify/workflows/workflow-registry.json | 4 +- HomesFlow.prd.md | 12 +- docs/clewseau-break-fix-peer-review.md | 166 +++++++ .../Core/Home/HomeDisplayNameNormalizer.swift | 18 + .../HomeDisplayNameNormalizerTests.swift | 21 + specs/000-cannon-smoketest/plan.md | 45 -- specs/000-cannon-smoketest/spec.md | 78 ---- .../checklists/requirements.md | 35 ++ specs/002-home-name-normalize/plan.md | 64 +++ specs/002-home-name-normalize/spec.md | 65 +++ specs/002-home-name-normalize/tasks.md | 49 ++ 41 files changed, 1279 insertions(+), 975 deletions(-) delete mode 100644 .specify/extensions/agent-context/README.md delete mode 100644 .specify/extensions/agent-context/agent-context-config.yml delete mode 100644 .specify/extensions/agent-context/commands/speckit.agent-context.update.md delete mode 100644 .specify/extensions/agent-context/extension.yml delete mode 100755 .specify/extensions/agent-context/scripts/bash/update-agent-context.sh delete mode 100644 .specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 create mode 100644 .specify/extensions/clewseau-gate/.specify-dev/agent-commands/claude/speckit-clewseau-gate-check/SKILL.md create mode 100644 .specify/extensions/clewseau-gate/README.md create mode 100644 .specify/extensions/clewseau-gate/clewseau-gate-config.yml create mode 100644 .specify/extensions/clewseau-gate/commands/speckit.clewseau-gate.check.md create mode 100644 .specify/extensions/clewseau-gate/config-template.yml create mode 100644 .specify/extensions/clewseau-gate/extension.yml create mode 100755 .specify/extensions/clewseau-gate/scripts/check-traceability.sh create mode 100644 .specify/memory/.constitution-template.json create mode 100644 .specify/presets/.registry create mode 100644 .specify/presets/clewseau/GLOSSARY.md create mode 100644 .specify/presets/clewseau/README.md create mode 100644 .specify/presets/clewseau/preset.yml create mode 100644 .specify/presets/clewseau/templates/constitution-template.md create mode 100644 .specify/presets/clewseau/templates/spec-template.md create mode 100644 .specify/presets/clewseau/templates/tasks-template.md create mode 100644 docs/clewseau-break-fix-peer-review.md create mode 100644 ios/HomesFlow/Core/Home/HomeDisplayNameNormalizer.swift create mode 100644 ios/HomesFlowTests/HomeDisplayNameNormalizerTests.swift delete mode 100644 specs/000-cannon-smoketest/plan.md delete mode 100644 specs/000-cannon-smoketest/spec.md create mode 100644 specs/002-home-name-normalize/checklists/requirements.md create mode 100644 specs/002-home-name-normalize/plan.md create mode 100644 specs/002-home-name-normalize/spec.md create mode 100644 specs/002-home-name-normalize/tasks.md diff --git a/.specify/extensions.yml b/.specify/extensions.yml index 5415714..be8dcc9 100644 --- a/.specify/extensions.yml +++ b/.specify/extensions.yml @@ -1,23 +1,14 @@ installed: -- agent-context +- clewseau-gate settings: auto_execute_hooks: true hooks: - after_specify: - - extension: agent-context - command: speckit.agent-context.update + after_implement: + - extension: clewseau-gate + command: speckit.clewseau-gate.check enabled: true optional: true priority: 10 - prompt: Execute speckit.agent-context.update? - description: Refresh agent context after specification - condition: null - after_plan: - - extension: agent-context - command: speckit.agent-context.update - enabled: true - optional: true - priority: 10 - prompt: Execute speckit.agent-context.update? - description: Refresh agent context after planning + prompt: Run Clewseau Gate 2 traceability check? + description: Refuse silent gaps after implementation; refresh clew.json condition: null diff --git a/.specify/extensions/.registry b/.specify/extensions/.registry index f67f85d..7ab49e9 100644 --- a/.specify/extensions/.registry +++ b/.specify/extensions/.registry @@ -1,21 +1,19 @@ { "schema_version": "1.0", "extensions": { - "agent-context": { - "version": "1.0.0", + "clewseau-gate": { + "version": "0.2.0", "source": "local", - "manifest_hash": "sha256:9a1dc02d2d0139bb03860392ecacef79183be2c442feda2f9ccaa4e5907b1e47", + "manifest_hash": "sha256:c540ad473eb810d0952c51812571222b0d45b9f7aa5828d252d0cb5809110c89", "enabled": true, "priority": 10, "registered_commands": { "claude": [ - "speckit.agent-context.update" + "speckit.clewseau-gate.check" ] }, - "registered_skills": [ - "speckit-agent-context-update" - ], - "installed_at": "2026-06-26T14:27:06.775090+00:00" + "registered_skills": [], + "installed_at": "2026-08-06T20:51:47.837406+00:00" } } } \ No newline at end of file diff --git a/.specify/extensions/agent-context/README.md b/.specify/extensions/agent-context/README.md deleted file mode 100644 index 091e2b4..0000000 --- a/.specify/extensions/agent-context/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Coding Agent Context Extension - -This bundled extension manages the **coding agent context/instruction file** (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`, `GEMINI.md`, …) for the active integration. - -It owns the lifecycle of the managed section delimited by the configurable start/end markers (defaults: `` / ``). - -## Why an extension? - -Not every Spec Kit user wants Spec Kit to write into the coding agent's context file. Extracting this behavior into a dedicated extension lets users: - -- **Opt out** entirely with `specify extension disable agent-context` — Spec Kit will then never create or modify the agent context file. -- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` — both the Python layer and the bundled scripts honor the same `context_markers` value. -- **Synchronize multiple agent anchors** by setting `context_files` when a project intentionally uses more than one coding agent context file, such as `AGENTS.md` and `CLAUDE.md`. -- **Refresh on demand** with `/speckit.agent-context.update`, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`). - -## Commands - -| Command | Description | -|---------|-------------| -| `speckit.agent-context.update` | Refresh the managed section in the agent context file with the current plan path. | - -## Configuration - -All configuration flows through the extension's own config file at -`.specify/extensions/agent-context/agent-context-config.yml`: - -```yaml -# Path to the coding agent context file managed by this extension -context_file: CLAUDE.md - -# Optional list of coding agent context files to manage together. -# When non-empty, this takes precedence over context_file. -context_files: - - AGENTS.md - - CLAUDE.md - -# Delimiters for the managed Spec Kit section -context_markers: - start: "" - end: "" -``` - -- `context_file` — the project-relative path to the coding agent context file, written by `specify init` and `specify integration install`. -- `context_files` — optional project-relative paths to multiple coding agent context files. When non-empty, the list takes precedence over `context_file`. Absolute paths, backslash separators, and `..` path segments are rejected. -- `context_markers.start` / `.end` — the delimiters around the managed section. Edit these to use custom markers. - -## Requirements - -The bundled update scripts require **Python 3** with **PyYAML** for YAML/upsert processing (PowerShell can also use `ConvertFrom-Yaml` when available). - -PyYAML ships with the `specify` CLI and is normally available via the same `python3` interpreter. If a hook reports *"PyYAML is required … not available in the current Python environment"*, it means the system `python3` differs from the one used to install Spec Kit. To resolve, run: - -```bash -pip install pyyaml -# or target the specific interpreter Spec Kit uses: -/path/to/speckit-python -m pip install pyyaml -``` - -## Disable - -```bash -specify extension disable agent-context -``` - -When disabled, Spec Kit skips context file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`). -Disabled projects also ignore stale `context_files` values during command rendering so disabling the extension remains a complete opt-out. diff --git a/.specify/extensions/agent-context/agent-context-config.yml b/.specify/extensions/agent-context/agent-context-config.yml deleted file mode 100644 index d55ff7c..0000000 --- a/.specify/extensions/agent-context/agent-context-config.yml +++ /dev/null @@ -1,5 +0,0 @@ -context_file: CLAUDE.md -context_files: [] -context_markers: - start: - end: diff --git a/.specify/extensions/agent-context/commands/speckit.agent-context.update.md b/.specify/extensions/agent-context/commands/speckit.agent-context.update.md deleted file mode 100644 index a654eb5..0000000 --- a/.specify/extensions/agent-context/commands/speckit.agent-context.update.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -description: "Refresh the managed Spec Kit section in coding agent context file(s)" ---- - -# Update Coding Agent Context - -Refresh the managed Spec Kit section inside the active coding agent's context/instruction file (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`). - -## Behavior - -The script reads the agent-context extension config at -`.specify/extensions/agent-context/agent-context-config.yml` to discover: - -- `context_file` — the path of the coding agent context file to manage. -- `context_files` — optional project-relative paths for multiple coding agent context files. When non-empty, the script updates each listed file and the list takes precedence over `context_file`. -- `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `` and `` when the field is missing. - -It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs//plan.md`). - -If `context_files` and `context_file` are empty, the command reports nothing to do and exits successfully. Context file paths must stay project-relative; absolute paths, Windows drive paths, backslash separators, and `..` path segments are rejected. - -## Execution - -- **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]` -- **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]` - -When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`. diff --git a/.specify/extensions/agent-context/extension.yml b/.specify/extensions/agent-context/extension.yml deleted file mode 100644 index 191069e..0000000 --- a/.specify/extensions/agent-context/extension.yml +++ /dev/null @@ -1,34 +0,0 @@ -schema_version: "1.0" - -extension: - id: agent-context - name: "Coding Agent Context" - version: "1.0.0" - description: "Manages coding agent context/instruction files (e.g., CLAUDE.md, copilot-instructions.md) with project-specific plan references and configurable markers" - author: spec-kit-core - repository: https://github.com/github/spec-kit - license: MIT - -requires: - speckit_version: ">=0.2.0" - -provides: - commands: - - name: speckit.agent-context.update - file: commands/speckit.agent-context.update.md - description: "Refresh the managed Spec Kit section in the coding agent context file" - -hooks: - after_specify: - command: speckit.agent-context.update - optional: true - description: "Refresh agent context after specification" - after_plan: - command: speckit.agent-context.update - optional: true - description: "Refresh agent context after planning" - -tags: - - "agent" - - "context" - - "core" diff --git a/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh b/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh deleted file mode 100755 index 9d57b08..0000000 --- a/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env bash -# update-agent-context.sh -# -# Refresh the managed Spec Kit section in the coding agent's context file(s) -# (e.g. CLAUDE.md, .github/copilot-instructions.md, AGENTS.md). -# -# Reads `context_files` or `context_file`, plus `context_markers.{start,end}`, from the -# agent-context extension config: -# .specify/extensions/agent-context/agent-context-config.yml -# -# Usage: update-agent-context.sh [plan_path] -# -# When `plan_path` is omitted, the script picks the most recently modified -# `specs/*/plan.md` if any exist, otherwise emits the section without a -# concrete plan path. - -set -euo pipefail - -PROJECT_ROOT="$(pwd)" -EXT_CONFIG="$PROJECT_ROOT/.specify/extensions/agent-context/agent-context-config.yml" -DEFAULT_START="" -DEFAULT_END="" - -if [[ ! -f "$EXT_CONFIG" ]]; then - echo "agent-context: $EXT_CONFIG not found; nothing to do." >&2 - exit 0 -fi - -# Locate a Python 3 interpreter with PyYAML available. -_python="" -_python_candidates=() -[[ -n "${SPECKIT_PYTHON:-}" ]] && _python_candidates+=("$SPECKIT_PYTHON") -_python_candidates+=("python3" "python") -for _candidate in "${_python_candidates[@]}"; do - if command -v "$_candidate" >/dev/null 2>&1 \ - && "$_candidate" - <<'PY' >/dev/null 2>&1 -import sys -try: - import yaml # noqa: F401 -except ImportError: - sys.exit(1) -sys.exit(0 if sys.version_info[0] == 3 else 1) -PY - then - _python="$_candidate" - break - fi -done -unset _candidate _python_candidates - -if [[ -z "$_python" ]]; then - echo "agent-context: Python 3 with PyYAML not found on PATH; skipping update." >&2 - echo " To resolve: pip install pyyaml (or install it into the environment used by python3)." >&2 - exit 0 -fi -_case_insensitive_context_files=0 -case "$(uname -s 2>/dev/null || true)" in - MINGW*|MSYS*|CYGWIN*) _case_insensitive_context_files=1 ;; -esac - -# Parse extension config once; emit context files as JSON, followed by marker strings. -if ! _raw_opts="$("$_python" - "$EXT_CONFIG" "$_case_insensitive_context_files" <<'PY' -import json -import sys -try: - import yaml -except ImportError: - print( - "agent-context: PyYAML is required to parse extension config but is not available " - "in the current Python environment.\n" - " To resolve: pip install pyyaml (or install it into the environment used by python3).\n" - " Context file will not be updated until PyYAML is importable.", - file=sys.stderr, - ) - sys.exit(2) -try: - with open(sys.argv[1], "r", encoding="utf-8") as fh: - data = yaml.safe_load(fh) -except Exception as exc: - print( - f"agent-context: unable to parse {sys.argv[1]} ({exc}); cannot update context.", - file=sys.stderr, - ) - sys.exit(2) -if not isinstance(data, dict): - data = {} -def get_str(obj, *keys): - node = obj - for k in keys: - if isinstance(node, dict) and k in node: - node = node[k] - else: - return "" - return node if isinstance(node, str) else "" -context_files = [] -seen_context_files = set() -case_insensitive = sys.argv[2] == "1" or sys.platform.startswith(("win32", "cygwin")) -raw_files = data.get("context_files") -if isinstance(raw_files, list): - for value in raw_files: - if not isinstance(value, str): - continue - candidate = value.strip() - if not candidate: - continue - key = candidate.casefold() if case_insensitive else candidate - if key in seen_context_files: - continue - context_files.append(candidate) - seen_context_files.add(key) -if not context_files: - raw_file = get_str(data, "context_file") - candidate = raw_file.strip() - if candidate: - context_files.append(candidate) -print(json.dumps(context_files)) -print(get_str(data, "context_markers", "start")) -print(get_str(data, "context_markers", "end")) -PY -)"; then - echo "agent-context: skipping update (see above for details)." >&2 - exit 0 -fi - -_opts_lines=() -while IFS= read -r _line || [[ -n "$_line" ]]; do - _opts_lines+=("$_line") -done < <(printf '%s\n' "$_raw_opts") -if (( ${#_opts_lines[@]} < 3 )); then - echo "agent-context: malformed config parser output; expected 3 lines (context_files, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2 - exit 0 -fi -CONTEXT_FILES_JSON="${_opts_lines[0]}" -MARKER_START="${_opts_lines[1]}" -MARKER_END="${_opts_lines[2]}" - -if ! _context_files_raw="$("$_python" - "$CONTEXT_FILES_JSON" <<'PY' -import json -import sys -try: - data = json.loads(sys.argv[1]) -except Exception: - data = [] -if not isinstance(data, list): - data = [] -for value in data: - if isinstance(value, str) and value: - print(value) -PY -)"; then - echo "agent-context: malformed context_files parser output; skipping update." >&2 - exit 0 -fi - -CONTEXT_FILES=() -while IFS= read -r _line || [[ -n "$_line" ]]; do - [[ -n "$_line" ]] && CONTEXT_FILES+=("$_line") -done < <(printf '%s\n' "$_context_files_raw") - -if (( ${#CONTEXT_FILES[@]} == 0 )); then - echo "agent-context: context_files/context_file not set in extension config; nothing to do." >&2 - exit 0 -fi - -for CONTEXT_FILE in "${CONTEXT_FILES[@]}"; do - # Reject absolute paths, backslash separators, and '..' path segments in context files - if [[ "$CONTEXT_FILE" == /* ]] || [[ "$CONTEXT_FILE" =~ ^[A-Za-z]: ]]; then - echo "agent-context: context files must be project-relative paths; got '$CONTEXT_FILE'." >&2 - exit 1 - fi - if [[ "$CONTEXT_FILE" == *\\* ]]; then - echo "agent-context: context files must not contain backslash separators; got '$CONTEXT_FILE'." >&2 - exit 1 - fi - IFS='/' read -ra _cf_parts <<< "$CONTEXT_FILE" - for _seg in "${_cf_parts[@]}"; do - if [[ "$_seg" == ".." ]]; then - echo "agent-context: context files must not contain '..' path segments; got '$CONTEXT_FILE'." >&2 - exit 1 - fi - done - if ! "$_python" - "$PROJECT_ROOT" "$CONTEXT_FILE" <<'PY' -import sys -from pathlib import Path - -root = Path(sys.argv[1]).resolve() -target = (root / sys.argv[2]).resolve(strict=False) -try: - target.relative_to(root) -except ValueError: - sys.exit(1) -PY - then - echo "agent-context: context file path resolves outside the project root; got '$CONTEXT_FILE'." >&2 - exit 1 - fi -done -unset _cf_parts _seg - -[[ -z "$MARKER_START" ]] && MARKER_START="$DEFAULT_START" -[[ -z "$MARKER_END" ]] && MARKER_END="$DEFAULT_END" - -PLAN_PATH="${1:-}" -if [[ -z "$PLAN_PATH" ]]; then - # Pick the most recently modified plan.md one level deep (specs//plan.md). - # Use find + sort by modification time to avoid ls/head fragility with - # spaces in paths or SIGPIPE from pipefail. - _plan_abs="$("$_python" - "$PROJECT_ROOT" <<'PY' -import sys, os -from pathlib import Path -specs = Path(sys.argv[1]) / "specs" -plans = sorted( - specs.glob("*/plan.md"), - key=lambda p: p.stat().st_mtime, - reverse=True, -) -print(plans[0] if plans else "") -PY -)" - if [[ -n "$_plan_abs" ]]; then - PLAN_PATH="${_plan_abs#"$PROJECT_ROOT/"}" - fi -fi - -# Build the managed section -TMP_SECTION="$(mktemp)" -trap 'rm -f "$TMP_SECTION"' EXIT -{ - echo "$MARKER_START" - echo "For additional context about technologies to be used, project structure," - echo "shell commands, and other important information, read the current plan" - if [[ -n "$PLAN_PATH" ]]; then - echo "at $PLAN_PATH" - fi - echo "$MARKER_END" -} > "$TMP_SECTION" - -for CONTEXT_FILE in "${CONTEXT_FILES[@]}"; do - CTX_PATH="$PROJECT_ROOT/$CONTEXT_FILE" - mkdir -p "$(dirname "$CTX_PATH")" - - "$_python" - "$CTX_PATH" "$MARKER_START" "$MARKER_END" "$TMP_SECTION" <<'PY' -import sys, os -ctx_path, start, end, section_path = sys.argv[1:5] -with open(section_path, "r", encoding="utf-8") as fh: - section = fh.read().rstrip("\n") + "\n" - -if os.path.exists(ctx_path): - with open(ctx_path, "r", encoding="utf-8-sig") as fh: - content = fh.read() - s = content.find(start) - e = content.find(end, s if s != -1 else 0) - if s != -1 and e != -1 and e > s: - end_of_marker = e + len(end) - if end_of_marker < len(content) and content[end_of_marker] == "\r": - end_of_marker += 1 - if end_of_marker < len(content) and content[end_of_marker] == "\n": - end_of_marker += 1 - new_content = content[:s] + section + content[end_of_marker:] - elif s != -1: - new_content = content[:s] + section - elif e != -1: - end_of_marker = e + len(end) - if end_of_marker < len(content) and content[end_of_marker] == "\r": - end_of_marker += 1 - if end_of_marker < len(content) and content[end_of_marker] == "\n": - end_of_marker += 1 - new_content = section + content[end_of_marker:] - else: - if content and not content.endswith("\n"): - content += "\n" - new_content = (content + "\n" + section) if content else section -else: - new_content = section - -new_content = new_content.replace("\r\n", "\n").replace("\r", "\n") -with open(ctx_path, "wb") as fh: - fh.write(new_content.encode("utf-8")) -PY - - echo "agent-context: updated $CONTEXT_FILE" -done diff --git a/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 b/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 deleted file mode 100644 index d31fcd6..0000000 --- a/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +++ /dev/null @@ -1,353 +0,0 @@ -#!/usr/bin/env pwsh -# update-agent-context.ps1 -# -# Refresh the managed Spec Kit section in the coding agent's context file(s) -# (e.g. CLAUDE.md, .github/copilot-instructions.md, AGENTS.md). -# -# Reads `context_files` or `context_file`, plus `context_markers.{start,end}`, from the -# agent-context extension config: -# .specify/extensions/agent-context/agent-context-config.yml -# -# Usage: update-agent-context.ps1 [plan_path] - -[CmdletBinding()] -param( - [Parameter(Position = 0)] - [string]$PlanPath -) - -function Get-ConfigValue { - param( - [AllowNull()][object]$Object, - [Parameter(Mandatory = $true)][string]$Key - ) - - if ($null -eq $Object) { - return $null - } - if ($Object -is [System.Collections.IDictionary]) { - return $Object[$Key] - } - $prop = $Object.PSObject.Properties[$Key] - if ($prop) { - return $prop.Value - } - return $null -} - -function Test-ConfigObject { - param( - [AllowNull()][object]$Object - ) - - if ($null -eq $Object) { - return $false - } - if ($Object -is [System.Collections.IDictionary]) { - return $true - } - if ($Object -is [System.Management.Automation.PSCustomObject]) { - return $true - } - return $false -} - -function Resolve-ContextPath { - param( - [Parameter(Mandatory = $true)][string]$Root, - [Parameter(Mandatory = $true)][string]$RelativePath - ) - - $rootFull = [System.IO.Path]::GetFullPath($Root) - $segments = $RelativePath -split '/' - $resolved = $rootFull - - foreach ($segment in $segments) { - if ([string]::IsNullOrWhiteSpace($segment) -or $segment -eq '.') { - continue - } - - $candidate = [System.IO.Path]::GetFullPath((Join-Path $resolved $segment)) - if (Test-Path -LiteralPath $candidate) { - $item = Get-Item -LiteralPath $candidate -Force - if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { - $target = $item.Target - if ($target -is [System.Array]) { - $target = $target[0] - } - if ($target) { - if ([System.IO.Path]::IsPathRooted($target)) { - $candidate = [System.IO.Path]::GetFullPath($target) - } else { - $candidate = [System.IO.Path]::GetFullPath( - (Join-Path (Split-Path -Parent $candidate) $target) - ) - } - } - } - } - $resolved = $candidate - } - - return $resolved -} - -function Test-IsSubPath { - param( - [Parameter(Mandatory = $true)][string]$Root, - [Parameter(Mandatory = $true)][string]$Path - ) - - $comparison = if ([System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { - [System.StringComparison]::OrdinalIgnoreCase - } else { - [System.StringComparison]::Ordinal - } - $rootFull = [System.IO.Path]::GetFullPath($Root).TrimEnd( - [System.IO.Path]::DirectorySeparatorChar, - [System.IO.Path]::AltDirectorySeparatorChar - ) - $pathFull = [System.IO.Path]::GetFullPath($Path) - return $pathFull.Equals($rootFull, $comparison) -or - $pathFull.StartsWith($rootFull + [System.IO.Path]::DirectorySeparatorChar, $comparison) -} - -$ErrorActionPreference = 'Stop' -$DefaultStart = '' -$DefaultEnd = '' -$ProjectRoot = (Get-Location).Path -$ExtConfig = Join-Path $ProjectRoot '.specify/extensions/agent-context/agent-context-config.yml' - -if (-not (Test-Path -LiteralPath $ExtConfig)) { - Write-Warning "agent-context: $ExtConfig not found; nothing to do." - exit 0 -} - -$Options = $null -if (Get-Command ConvertFrom-Yaml -ErrorAction SilentlyContinue) { - try { - $Options = Get-Content -LiteralPath $ExtConfig -Raw | ConvertFrom-Yaml -ErrorAction Stop - } catch { - # fall through to Python fallback - } -} - -if ($null -eq $Options) { - # ConvertFrom-Yaml unavailable or failed; fall back to Python+PyYAML. - $pythonCmd = $null - $pythonCandidates = @() - if ($env:SPECKIT_PYTHON) { - $pythonCandidates += $env:SPECKIT_PYTHON - } - $pythonCandidates += @('python3', 'python') - foreach ($candidate in $pythonCandidates) { - if (Get-Command $candidate -ErrorAction SilentlyContinue) { - # Verify it is Python 3 with PyYAML available. - $null = & $candidate -c "import sys; import yaml; sys.exit(0 if sys.version_info[0] == 3 else 1)" 2>$null - if ($LASTEXITCODE -eq 0) { - $pythonCmd = $candidate - break - } - } - } - - if ($pythonCmd) { - $pyScript = $null - try { - $pyScript = [System.IO.Path]::GetTempFileName() - Set-Content -LiteralPath $pyScript -Encoding UTF8 -Value @' -import json -import sys -try: - import yaml -except ImportError: - print( - "agent-context: PyYAML is required to parse extension config; cannot update context.", - file=sys.stderr, - ) - sys.exit(2) - -try: - with open(sys.argv[1], "r", encoding="utf-8") as fh: - data = yaml.safe_load(fh) -except Exception as exc: - print( - f"agent-context: unable to parse {sys.argv[1]} ({exc}); cannot update context.", - file=sys.stderr, - ) - sys.exit(2) - -if not isinstance(data, dict): - data = {} - -print(json.dumps(data)) -'@ - $jsonOut = & $pythonCmd $pyScript $ExtConfig - if ($LASTEXITCODE -eq 0 -and $jsonOut) { - $Options = $jsonOut | ConvertFrom-Json -ErrorAction Stop - } - } catch { - $Options = $null - } finally { - if ($pyScript -and (Test-Path -LiteralPath $pyScript)) { - Remove-Item -LiteralPath $pyScript -Force -ErrorAction SilentlyContinue - } - } - } - - if (-not $Options) { - Write-Warning "agent-context: unable to parse $ExtConfig; skipping update." - exit 0 - } -} - -if (-not (Test-ConfigObject -Object $Options)) { - Write-Warning "agent-context: $ExtConfig must contain a YAML mapping; skipping update." - exit 0 -} - -$ConfiguredContextFiles = Get-ConfigValue -Object $Options -Key 'context_files' -$ContextFiles = @() -if ($null -ne $ConfiguredContextFiles) { - foreach ($item in @($ConfiguredContextFiles)) { - if ($item -is [string] -and -not [string]::IsNullOrWhiteSpace($item)) { - $ContextFiles += $item.Trim() - } - } -} -if ($ContextFiles.Count -eq 0) { - $ContextFile = Get-ConfigValue -Object $Options -Key 'context_file' - if ($ContextFile -is [string] -and -not [string]::IsNullOrWhiteSpace($ContextFile)) { - $ContextFiles += $ContextFile.Trim() - } -} -$pathComparison = if ([System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { - [System.StringComparer]::OrdinalIgnoreCase -} else { - [System.StringComparer]::Ordinal -} -$seenContextFiles = [System.Collections.Generic.HashSet[string]]::new($pathComparison) -$dedupedContextFiles = @() -foreach ($ContextFile in $ContextFiles) { - if ($seenContextFiles.Add($ContextFile)) { - $dedupedContextFiles += $ContextFile - } -} -$ContextFiles = $dedupedContextFiles -if ($ContextFiles.Count -eq 0) { - Write-Warning 'agent-context: context_files/context_file not set in extension config; nothing to do.' - exit 0 -} - -foreach ($ContextFile in $ContextFiles) { - # Reject absolute paths, drive-qualified paths, backslash separators, and '..' path segments in context files - if ($ContextFile -match '^[A-Za-z]:') { - Write-Warning "agent-context: context files must be project-relative paths; got '$ContextFile'." - exit 1 - } - if ([System.IO.Path]::IsPathRooted($ContextFile)) { - Write-Warning "agent-context: context files must be project-relative paths; got '$ContextFile'." - exit 1 - } - if ($ContextFile.Contains('\')) { - Write-Warning "agent-context: context files must not contain backslash separators; got '$ContextFile'." - exit 1 - } - $cfSegments = $ContextFile -split '[/\\]' - if ($cfSegments -contains '..') { - Write-Warning "agent-context: context files must not contain '..' path segments; got '$ContextFile'." - exit 1 - } - $resolvedTarget = Resolve-ContextPath -Root $ProjectRoot -RelativePath $ContextFile - if (-not (Test-IsSubPath -Root $ProjectRoot -Path $resolvedTarget)) { - Write-Warning "agent-context: context file path resolves outside the project root; got '$ContextFile'." - exit 1 - } -} - -$MarkerStart = $DefaultStart -$MarkerEnd = $DefaultEnd -$cm = Get-ConfigValue -Object $Options -Key 'context_markers' -if ($cm) { - $cmStart = Get-ConfigValue -Object $cm -Key 'start' - if ($cmStart -is [string] -and $cmStart) { - $MarkerStart = $cmStart - } - $cmEnd = Get-ConfigValue -Object $cm -Key 'end' - if ($cmEnd -is [string] -and $cmEnd) { - $MarkerEnd = $cmEnd - } -} - -if (-not $PlanPath) { - # Discover plan.md exactly one level deep (specs//plan.md), - # matching the bash glob specs/*/plan.md. Wrap in try/catch so access errors under - # $ErrorActionPreference = 'Stop' don't abort the script. - try { - $specsDir = Join-Path $ProjectRoot 'specs' - $candidate = Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue | - ForEach-Object { Get-Item -LiteralPath (Join-Path $_.FullName 'plan.md') -ErrorAction SilentlyContinue } | - Where-Object { $_ } | - Sort-Object LastWriteTime -Descending | - Select-Object -First 1 - if ($candidate) { - $PlanPath = [System.IO.Path]::GetRelativePath($ProjectRoot, $candidate.FullName).Replace('\','/') - } - } catch { - # Non-fatal: continue without a plan path. - } -} - -$lines = @($MarkerStart, - 'For additional context about technologies to be used, project structure,', - 'shell commands, and other important information, read the current plan') -if ($PlanPath) { - $lines += "at $PlanPath" -} -$lines += $MarkerEnd -$Section = ($lines -join "`n") + "`n" - -foreach ($ContextFile in $ContextFiles) { - $CtxPath = Join-Path $ProjectRoot $ContextFile - $CtxDir = Split-Path -Parent $CtxPath - if ($CtxDir -and -not (Test-Path -LiteralPath $CtxDir)) { - New-Item -ItemType Directory -Path $CtxDir -Force | Out-Null - } - - if (Test-Path -LiteralPath $CtxPath) { - $rawBytes = [System.IO.File]::ReadAllBytes($CtxPath) - # Strip UTF-8 BOM if present - if ($rawBytes.Length -ge 3 -and $rawBytes[0] -eq 0xEF -and $rawBytes[1] -eq 0xBB -and $rawBytes[2] -eq 0xBF) { - $content = [System.Text.Encoding]::UTF8.GetString($rawBytes, 3, $rawBytes.Length - 3) - } else { - $content = [System.Text.Encoding]::UTF8.GetString($rawBytes) - } - - $s = $content.IndexOf($MarkerStart) - $e = if ($s -ge 0) { $content.IndexOf($MarkerEnd, $s) } else { $content.IndexOf($MarkerEnd) } - - if ($s -ge 0 -and $e -ge 0 -and $e -gt $s) { - $endOfMarker = $e + $MarkerEnd.Length - if ($endOfMarker -lt $content.Length -and $content[$endOfMarker] -eq "`r") { $endOfMarker++ } - if ($endOfMarker -lt $content.Length -and $content[$endOfMarker] -eq "`n") { $endOfMarker++ } - $newContent = $content.Substring(0, $s) + $Section + $content.Substring($endOfMarker) - } elseif ($s -ge 0) { - $newContent = $content.Substring(0, $s) + $Section - } elseif ($e -ge 0) { - $endOfMarker = $e + $MarkerEnd.Length - if ($endOfMarker -lt $content.Length -and $content[$endOfMarker] -eq "`r") { $endOfMarker++ } - if ($endOfMarker -lt $content.Length -and $content[$endOfMarker] -eq "`n") { $endOfMarker++ } - $newContent = $Section + $content.Substring($endOfMarker) - } else { - if ($content -and -not $content.EndsWith("`n")) { $content += "`n" } - if ($content) { $newContent = $content + "`n" + $Section } else { $newContent = $Section } - } - } else { - $newContent = $Section - } - - $newContent = $newContent.Replace("`r`n", "`n").Replace("`r", "`n") - [System.IO.File]::WriteAllText($CtxPath, $newContent, (New-Object System.Text.UTF8Encoding($false))) - - Write-Host "agent-context: updated $ContextFile" -} diff --git a/.specify/extensions/clewseau-gate/.specify-dev/agent-commands/claude/speckit-clewseau-gate-check/SKILL.md b/.specify/extensions/clewseau-gate/.specify-dev/agent-commands/claude/speckit-clewseau-gate-check/SKILL.md new file mode 100644 index 0000000..27f0619 --- /dev/null +++ b/.specify/extensions/clewseau-gate/.specify-dev/agent-commands/claude/speckit-clewseau-gate-check/SKILL.md @@ -0,0 +1,34 @@ +--- +name: speckit-clewseau-gate-check +description: Run Clewseau Gate 2 — fail on silent gaps; emit clew.json +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: github-spec-kit + source: clewseau-gate:commands/speckit.clewseau-gate.check.md +--- + +# Clewseau Gate 2 check + +Run the portable Gate 2 script shipped with this extension. It checks that: + +- registry IDs ≡ IDs in specs ≡ IDs in tasks (**exact-set** drift), +- durable IDs are either verified (named proof) or tracked as debt (open task) for **ACs**, +- coverage annotations / test-encoded IDs are not orphans, +- checkbox tasks declare `Traces:`, + +and **always writes a clew** (default `clew.json`, path configurable) — the Clewseau matrix. The clew is written even when the gate fails, so GAPs and `gate.failures` are visible. + +`clew.json` is Clewseau-native (`format: "clew"`, schemaVersion 3). It is not ReqIF/OSLC. clewloupe (the viewer) consumes this file; it does not re-scan the target. + +## Steps + +1. Confirm `.specify/extensions/clewseau-gate/clewseau-gate-config.yml` exists (copy from `config-template.yml` if missing) and points at this project's registry, specs, tasks, and source/test trees. Set `clew_path` if you do not want `clew.json` at the project root. +2. From the project root, run: + +```bash +bash .specify/extensions/clewseau-gate/scripts/check-traceability.sh +``` + +3. Report the script's exit code, any `FAIL:` lines, and confirm `clew.json` (or configured `clew_path`) was written. +4. Do **not** weaken the gate. If something is unfinished, it belongs as tracked debt (unchecked task with `Traces:`), not as a silenced gap. +5. Reminder: **verified** means a named proof exists — not that a full suite was asserted green by this script. \ No newline at end of file diff --git a/.specify/extensions/clewseau-gate/README.md b/.specify/extensions/clewseau-gate/README.md new file mode 100644 index 0000000..2bacdfb --- /dev/null +++ b/.specify/extensions/clewseau-gate/README.md @@ -0,0 +1,11 @@ +# Clewseau Gate + +Gate 2: compare the ID registry to specs, tasks, coverage annotations, and test names. **Exact-set** registry ≡ specs ≡ tasks. Silent AC gaps and untraced scope fail. **Always writes a clew** (default `clew.json`, configurable via `clew_path`) for clewloupe or any matrix consumer. + +Install: + +```bash +specify extension add --dev /path/to/clewseau/extensions/clewseau-gate +``` + +Install scaffolds `clewseau-gate-config.yml` from `config-template.yml`. Edit `registry`, `clew_path`, `specs`, `tasks`, `src_globs`, and `test_globs` for your repo. diff --git a/.specify/extensions/clewseau-gate/clewseau-gate-config.yml b/.specify/extensions/clewseau-gate/clewseau-gate-config.yml new file mode 100644 index 0000000..1d9652a --- /dev/null +++ b/.specify/extensions/clewseau-gate/clewseau-gate-config.yml @@ -0,0 +1,31 @@ +# Clewseau Gate config (edit after install) + +# Authoritative ID registry (usually the PRD) +registry: "HomesFlow.prd.md" + +# Optional display name in clew.json (defaults to project directory name) +target_name: "HomesFlow" + +# Where Gate 2 writes the clew (Clewseau native matrix; format: clew) +clew_path: "clew.json" + +# Spec Kit feature artifacts (globs relative to project root) +specs: "specs/**/spec.md" +tasks: "specs/**/tasks.md" + +# Implementation + test trees (adjust per language/layout) +src_globs: + - "src/**" + - "ios/**/*.swift" +test_globs: + - "tests/**" + - "ios/**/*Tests*/**/*.swift" + +# ID grammar (HomesFlow-compatible default) +id_regex: "(FR|NFR|AC|US)-[A-Z][A-Z0-9]{1,5}-[0-9]{2,}[a-z]?" + +# How coverage annotations appear in source +covers_regex: "@covers[[:space:]]+.*" + +# How AC IDs appear inside test names (e.g. test_AC_SYNC_04_...) +test_ac_regex: "AC_[A-Z][A-Z0-9]{1,5}_[0-9]{2,}[a-z]?" diff --git a/.specify/extensions/clewseau-gate/commands/speckit.clewseau-gate.check.md b/.specify/extensions/clewseau-gate/commands/speckit.clewseau-gate.check.md new file mode 100644 index 0000000..53a79d9 --- /dev/null +++ b/.specify/extensions/clewseau-gate/commands/speckit.clewseau-gate.check.md @@ -0,0 +1,29 @@ +--- +description: Run Clewseau Gate 2 — fail on silent gaps; emit clew.json +--- + +# Clewseau Gate 2 check + +Run the portable Gate 2 script shipped with this extension. It checks that: + +- registry IDs ≡ IDs in specs ≡ IDs in tasks (**exact-set** drift), +- durable IDs are either verified (named proof) or tracked as debt (open task) for **ACs**, +- coverage annotations / test-encoded IDs are not orphans, +- checkbox tasks declare `Traces:`, + +and **always writes a clew** (default `clew.json`, path configurable) — the Clewseau matrix. The clew is written even when the gate fails, so GAPs and `gate.failures` are visible. + +`clew.json` is Clewseau-native (`format: "clew"`, schemaVersion 3). It is not ReqIF/OSLC. clewloupe (the viewer) consumes this file; it does not re-scan the target. + +## Steps + +1. Confirm `.specify/extensions/clewseau-gate/clewseau-gate-config.yml` exists (copy from `config-template.yml` if missing) and points at this project's registry, specs, tasks, and source/test trees. Set `clew_path` if you do not want `clew.json` at the project root. +2. From the project root, run: + +```bash +bash .specify/extensions/clewseau-gate/scripts/check-traceability.sh +``` + +3. Report the script's exit code, any `FAIL:` lines, and confirm `clew.json` (or configured `clew_path`) was written. +4. Do **not** weaken the gate. If something is unfinished, it belongs as tracked debt (unchecked task with `Traces:`), not as a silenced gap. +5. Reminder: **verified** means a named proof exists — not that a full suite was asserted green by this script. diff --git a/.specify/extensions/clewseau-gate/config-template.yml b/.specify/extensions/clewseau-gate/config-template.yml new file mode 100644 index 0000000..71d53f9 --- /dev/null +++ b/.specify/extensions/clewseau-gate/config-template.yml @@ -0,0 +1,31 @@ +# Clewseau Gate config (edit after install) + +# Authoritative ID registry (usually the PRD) +registry: "PRD.md" + +# Optional display name in clew.json (defaults to project directory name) +target_name: "" + +# Where Gate 2 writes the clew (Clewseau native matrix; format: clew) +clew_path: "clew.json" + +# Spec Kit feature artifacts (globs relative to project root) +specs: "specs/**/spec.md" +tasks: "specs/**/tasks.md" + +# Implementation + test trees (adjust per language/layout) +src_globs: + - "src/**" + - "ios/**/*.swift" +test_globs: + - "tests/**" + - "ios/**/*Tests*/**/*.swift" + +# ID grammar (HomesFlow-compatible default) +id_regex: "(FR|NFR|AC|US)-[A-Z][A-Z0-9]{1,5}-[0-9]{2,}[a-z]?" + +# How coverage annotations appear in source +covers_regex: "@covers[[:space:]]+.*" + +# How AC IDs appear inside test names (e.g. test_AC_SYNC_04_...) +test_ac_regex: "AC_[A-Z][A-Z0-9]{1,5}_[0-9]{2,}[a-z]?" diff --git a/.specify/extensions/clewseau-gate/extension.yml b/.specify/extensions/clewseau-gate/extension.yml new file mode 100644 index 0000000..8a2a400 --- /dev/null +++ b/.specify/extensions/clewseau-gate/extension.yml @@ -0,0 +1,43 @@ +schema_version: "1.0" + +extension: + id: "clewseau-gate" + name: "Clewseau Gate" + version: "0.2.0" + description: "Gate 2 — refuse silent gaps and emit a clew (`clew.json`)." + category: "visibility" + effect: "read-write" + author: "Rik Dryfoos / Dryfoos Consulting" + license: "MIT" + homepage: "https://github.com/rdryfoos/clewseau" + repository: "https://github.com/rdryfoos/clewseau" + +requires: + speckit_version: ">=0.14.0" + tools: [] + +provides: + commands: + - name: "speckit.clewseau-gate.check" + file: "commands/speckit.clewseau-gate.check.md" + description: "Run Clewseau Gate 2 (silent gaps fail; write clew.json)" + + config: + - name: "clewseau-gate-config.yml" + template: "config-template.yml" + description: "Registry path, source/test globs, and ID grammar" + required: true + +hooks: + after_implement: + command: "speckit.clewseau-gate.check" + optional: true + prompt: "Run Clewseau Gate 2 traceability check?" + description: "Refuse silent gaps after implementation; refresh clew.json" + +tags: + - "traceability" + - "gate" + - "ci" + - "governance" + - "sdd" diff --git a/.specify/extensions/clewseau-gate/scripts/check-traceability.sh b/.specify/extensions/clewseau-gate/scripts/check-traceability.sh new file mode 100755 index 0000000..5c5c4a1 --- /dev/null +++ b/.specify/extensions/clewseau-gate/scripts/check-traceability.sh @@ -0,0 +1,437 @@ +#!/usr/bin/env bash +# Clewseau Gate 2 — portable traceability check + clew.json matrix emitter. +# Exact-set registry ≡ specs ≡ tasks. Silent AC gaps and untraced scope fail. +# Tracked debt is allowed. US/FR/NFR without own carrier are backlog (planning altitude) — not silent-gap candidates. +# Always writes clew.json (even on failure) so the clew reflects GAPs + gate. +set -euo pipefail + +export LC_ALL=C + +EXT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +PROJECT_ROOT="${CLEWSEAU_PROJECT_ROOT:-}" +if [[ -z "$PROJECT_ROOT" ]]; then + # Spec Kit install: /.specify/extensions// → three levels up. + # Clewseau repo layout: /extensions// → two levels up. + parent_dir="$(basename "$(dirname "$EXT_DIR")")" + grandparent_dir="$(basename "$(dirname "$(dirname "$EXT_DIR")")")" + if [[ "$parent_dir" == "extensions" && "$grandparent_dir" == ".specify" ]]; then + PROJECT_ROOT="$(cd "$EXT_DIR/../../.." && pwd)" + else + PROJECT_ROOT="$(cd "$EXT_DIR/../.." && pwd)" + fi +fi +cd "$PROJECT_ROOT" + +CONFIG="${CLEWSEAU_CONFIG:-$EXT_DIR/clewseau-gate-config.yml}" +if [[ ! -f "$CONFIG" ]]; then + if [[ -f "$EXT_DIR/config-template.yml" ]]; then + CONFIG="$EXT_DIR/config-template.yml" + echo "WARN: using config-template.yml; copy to clewseau-gate-config.yml for real projects" >&2 + else + echo "FAIL: no clewseau-gate-config.yml (looked in $EXT_DIR)" >&2 + exit 2 + fi +fi + +yaml_scalar() { + local key="$1" + awk -v k="$key" ' + $0 ~ "^"k":[[:space:]]*" { + sub("^[^:]+:[[:space:]]*", "") + gsub(/^"/, ""); gsub(/"$/, "") + print + exit + } + ' "$CONFIG" +} + +yaml_list() { + local key="$1" + awk -v k="$key" ' + $0 ~ "^"k":[[:space:]]*$" { inlist=1; next } + inlist && /^[^[:space:]-]/ { exit } + inlist && /^[[:space:]]*-[[:space:]]*/ { + sub(/^[[:space:]]*-[[:space:]]*/, "") + gsub(/^"/, ""); gsub(/"$/, "") + print + } + ' "$CONFIG" +} + +REGISTRY="$(yaml_scalar registry)" +SPECS_GLOB="$(yaml_scalar specs)" +TASKS_GLOB="$(yaml_scalar tasks)" +ID_RE="$(yaml_scalar id_regex)" +COVERS_RE="$(yaml_scalar covers_regex)" +TEST_AC_RE="$(yaml_scalar test_ac_regex)" +CLEW_OUT="$(yaml_scalar clew_path)" +TARGET_NAME="$(yaml_scalar target_name)" + +[[ -n "$REGISTRY" ]] || { echo "FAIL: config missing registry" >&2; exit 2; } +[[ -n "$ID_RE" ]] || ID_RE='(FR|NFR|AC|US)-[A-Z][A-Z0-9]{1,5}-[0-9]{2,}[a-z]?' +[[ -n "$COVERS_RE" ]] || COVERS_RE='@covers[[:space:]]+.*' +[[ -n "$TEST_AC_RE" ]] || TEST_AC_RE='AC_[A-Z][A-Z0-9]{1,5}_[0-9]{2,}[a-z]?' +[[ -n "$SPECS_GLOB" ]] || SPECS_GLOB='specs/**/spec.md' +[[ -n "$TASKS_GLOB" ]] || TASKS_GLOB='specs/**/tasks.md' +[[ -n "$CLEW_OUT" ]] || CLEW_OUT='clew.json' +[[ -n "$TARGET_NAME" ]] || TARGET_NAME="$(basename "$PROJECT_ROOT")" + +fail=0 +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +: > "$tmp/failures.jsonl" + +# kind | id-or-empty | detail — collected for clew.json gate.failures +record_fail() { + local kind="$1" + local id="${2:-}" + local detail="$3" + echo "FAIL: $detail" >&2 + fail=1 + python3 -c ' +import json,sys +kind, id_, detail = sys.argv[1], sys.argv[2], sys.argv[3] +row = {"kind": kind, "detail": detail} +if id_: + row["id"] = id_ +print(json.dumps(row, ensure_ascii=False)) +' "$kind" "$id" "$detail" >> "$tmp/failures.jsonl" +} + +if [[ ! -f "$REGISTRY" ]]; then + record_fail "registry-missing" "" "registry not found: $REGISTRY" + # Still try to emit an empty-ish clew below if possible; exit after emit. +fi + +if [[ -f "$REGISTRY" ]]; then + grep -Eoh "$ID_RE" "$REGISTRY" | sort -u > "$tmp/registry.txt" +else + : > "$tmp/registry.txt" +fi + +expand_glob() { + local pattern="$1" + local dir base + if [[ "$pattern" == *\*\*/* ]]; then + dir="${pattern%%/\*\*/*}" + base="${pattern##*/}" + [[ -d "$dir" ]] || return 0 + find "$dir" -type f -name "$base" 2>/dev/null + elif [[ "$pattern" == */* ]]; then + dir="$(dirname "$pattern")" + base="$(basename "$pattern")" + if [[ "$base" == "**" ]]; then + [[ -d "$dir" ]] || return 0 + find "$dir" -type f 2>/dev/null + return 0 + fi + [[ -d "$dir" ]] || return 0 + find "$dir" -type f -name "$base" 2>/dev/null + else + find . -type f -name "$pattern" 2>/dev/null + fi +} + +: > "$tmp/spec.txt" +while IFS= read -r f; do + [[ -f "$f" ]] || continue + grep -Eoh "$ID_RE" "$f" | sort -u >> "$tmp/spec.txt" || true +done < <(expand_glob "$SPECS_GLOB") +sort -u "$tmp/spec.txt" -o "$tmp/spec.txt" + +: > "$tmp/tasks.txt" +: > "$tmp/pending.txt" +: > "$tmp/pending_hits.txt" +while IFS= read -r f; do + [[ -f "$f" ]] || continue + grep -Eoh "$ID_RE" "$f" | sort -u >> "$tmp/tasks.txt" || true + # Open checkbox tasks that name registry IDs (usually via Traces:) — debt carriers. + grep -nE '^- \[ \]' "$f" 2>/dev/null | while IFS= read -r line; do + lineno="${line%%:*}" + rest="${line#*:}" + excerpt="$(printf '%s' "$rest" | tr '\t' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | cut -c1-200)" + while IFS= read -r id; do + [[ -n "$id" ]] || continue + printf '%s|%s|%s|%s\n' "$f" "$lineno" "$id" "$excerpt" + done < <(grep -Eo "$ID_RE" <<<"$rest" || true) + done >> "$tmp/pending_hits.txt" || true +done < <(expand_glob "$TASKS_GLOB") +sort -u "$tmp/tasks.txt" -o "$tmp/tasks.txt" +cut -d'|' -f3 "$tmp/pending_hits.txt" 2>/dev/null | sort -u > "$tmp/pending.txt" || : > "$tmp/pending.txt" + + +# covers: path|line|id|excerpt +: > "$tmp/covers_hits.txt" +while IFS= read -r g; do + [[ -z "$g" ]] && continue + while IFS= read -r f; do + [[ -f "$f" ]] || continue + grep -nE "$COVERS_RE" "$f" 2>/dev/null | while IFS= read -r line; do + lineno="${line%%:*}" + rest="${line#*:}" + excerpt="$(printf '%s' "$rest" | tr '\t' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | cut -c1-160)" + # One hit per ID — @covers may list several (e.g. FR-HOME-04, AC-HOME-15). + while IFS= read -r id; do + [[ -n "$id" ]] || continue + printf '%s|%s|%s|%s\n' "$f" "$lineno" "$id" "$excerpt" + done < <(grep -Eo "$ID_RE" <<<"$rest" || true) + done + done < <(expand_glob "$g") +done < <(yaml_list src_globs) >> "$tmp/covers_hits.txt" || true + +cut -d'|' -f3 "$tmp/covers_hits.txt" 2>/dev/null | sort -u > "$tmp/covers.txt" || true + +# proofs: path|line|id|name +: > "$tmp/proof_hits.txt" +while IFS= read -r g; do + [[ -z "$g" ]] && continue + while IFS= read -r f; do + [[ -f "$f" ]] || continue + grep -nE "$TEST_AC_RE" "$f" 2>/dev/null | while IFS= read -r line; do + lineno="${line%%:*}" + rest="${line#*:}" + raw="$(grep -Eo "$TEST_AC_RE" <<<"$rest" | head -1 || true)" + [[ -n "$raw" ]] || continue + id="$(printf '%s' "$raw" | tr '_' '-')" + name="$(grep -Eo 'test_[A-Za-z0-9_]+|func test_[A-Za-z0-9_]+' <<<"$rest" | head -1 | sed 's/^func //' || true)" + [[ -n "$name" ]] || name="$raw" + printf '%s|%s|%s|%s\n' "$f" "$lineno" "$id" "$name" + done + done < <(expand_glob "$g") +done < <(yaml_list test_globs) >> "$tmp/proof_hits.txt" || true + +cut -d'|' -f3 "$tmp/proof_hits.txt" 2>/dev/null | sort -u > "$tmp/test_acs.txt" || true + +# 1) Exact-set drift: registry ≡ specs, registry ≡ tasks (HomesFlow Gate 2 parity). +# Specs/tasks may not invent IDs; registry IDs may not sit unclaimed in either artifact. +while IFS= read -r id; do + [[ -z "$id" ]] && continue + record_fail "spec-orphan" "$id" "spec references ID not in registry: $id" +done < <(comm -13 "$tmp/registry.txt" "$tmp/spec.txt") + +while IFS= read -r id; do + [[ -z "$id" ]] && continue + record_fail "spec-unclaimed" "$id" "registry ID missing from specs: $id" +done < <(comm -23 "$tmp/registry.txt" "$tmp/spec.txt") + +while IFS= read -r id; do + [[ -z "$id" ]] && continue + record_fail "task-orphan" "$id" "tasks reference ID not in registry: $id" +done < <(comm -13 "$tmp/registry.txt" "$tmp/tasks.txt") + +while IFS= read -r id; do + [[ -z "$id" ]] && continue + record_fail "task-unclaimed" "$id" "registry ID missing from tasks: $id" +done < <(comm -23 "$tmp/registry.txt" "$tmp/tasks.txt") + +# 2) Every task with a checkbox should have Traces +while IFS= read -r f; do + [[ -f "$f" ]] || continue + while IFS= read -r line; do + if [[ "$line" =~ ^-\ \[[x\ ]\]\ T ]]; then + if ! grep -Eq '\*\*Traces\*\*:' <<<"$line"; then + record_fail "missing-traces" "" "task missing Traces field: ${line:0:80}" + fi + fi + done < "$f" +done < <(expand_glob "$TASKS_GLOB") + +# 3) Untraced scope +while IFS= read -r id; do + [[ -z "$id" ]] && continue + grep -qx "$id" "$tmp/registry.txt" || record_fail "orphan-covers" "$id" "untraced scope (@covers): $id not in registry" +done < "$tmp/covers.txt" + +while IFS= read -r id; do + [[ -z "$id" ]] && continue + grep -qx "$id" "$tmp/registry.txt" || record_fail "orphan-test" "$id" "untraced scope (test name): $id not in registry" +done < "$tmp/test_acs.txt" + +# 4) Silent gaps — ACs only (coverage altitude: AC is the atomic proof unit) +while IFS= read -r id; do + [[ -z "$id" ]] && continue + [[ "${id%%-*}" == "AC" ]] || continue + if grep -qx "$id" "$tmp/test_acs.txt"; then + continue + fi + if grep -qx "$id" "$tmp/pending.txt"; then + continue + fi + record_fail "silent-gap" "$id" "silent gap: $id has no test and no open tracked-debt task" +done < "$tmp/registry.txt" + +# --- Emit clew.json (always; clew should show GAPs even when Gate fails) --- +export CLEW_OUT REGISTRY TARGET_NAME PROJECT_ROOT +export CLEW_TMP="$tmp" +export CLEW_FAIL="$fail" +python3 - <<'PY' +import json, os, re, datetime +from pathlib import Path + +tmp = Path(os.environ["CLEW_TMP"]) +registry_path = Path(os.environ["REGISTRY"]) +out_path = Path(os.environ["CLEW_OUT"]) +target = os.environ.get("TARGET_NAME") or "project" +repo = os.environ.get("PROJECT_ROOT") or str(Path.cwd()) +gate_failed = os.environ.get("CLEW_FAIL", "0") != "0" + +ids = [ln.strip() for ln in (tmp / "registry.txt").read_text().splitlines() if ln.strip()] +pending = {ln.strip() for ln in (tmp / "pending.txt").read_text().splitlines() if ln.strip()} +tested = {ln.strip() for ln in (tmp / "test_acs.txt").read_text().splitlines() if ln.strip()} +covered = {ln.strip() for ln in (tmp / "covers.txt").read_text().splitlines() if ln.strip()} + +failures = [] +fail_path = tmp / "failures.jsonl" +if fail_path.exists(): + for ln in fail_path.read_text().splitlines(): + if ln.strip(): + failures.append(json.loads(ln)) + +statements = {} +if registry_path.is_file(): + reg_text = registry_path.read_text(encoding="utf-8", errors="replace").splitlines() +else: + reg_text = [] +for id_ in ids: + statements[id_] = id_ + for line in reg_text: + if id_ in line: + s = re.sub(r"^[\s#\-*\[\]xX]+", "", line).strip() + statements[id_] = s or id_ + break + +impl_by = {i: [] for i in ids} +covers_file = tmp / "covers_hits.txt" +if covers_file.exists(): + for ln in covers_file.read_text().splitlines(): + if not ln.strip(): + continue + parts = ln.split("|", 3) + if len(parts) < 3: + continue + path, line, id_ = parts[0], parts[1], parts[2] + excerpt = parts[3] if len(parts) > 3 else "" + if id_ not in impl_by: + continue + try: + line_n = int(line) + except ValueError: + line_n = 0 + impl_by[id_].append({"path": path, "line": line_n, "excerpt": excerpt}) + +proof_by = {i: [] for i in ids} +proofs_file = tmp / "proof_hits.txt" +if proofs_file.exists(): + for ln in proofs_file.read_text().splitlines(): + if not ln.strip(): + continue + parts = ln.split("|", 3) + if len(parts) < 3: + continue + path, line, id_ = parts[0], parts[1], parts[2] + name = parts[3] if len(parts) > 3 else id_ + if id_ not in proof_by: + continue + try: + line_n = int(line) + except ValueError: + line_n = 0 + proof_by[id_].append({"name": name, "path": path, "line": line_n}) + +debt_by = {i: [] for i in ids} +pending_hits = tmp / "pending_hits.txt" +seen_debt = set() +if pending_hits.exists(): + for ln in pending_hits.read_text().splitlines(): + if not ln.strip(): + continue + parts = ln.split("|", 3) + if len(parts) < 3: + continue + path, line, id_ = parts[0], parts[1], parts[2] + excerpt = parts[3] if len(parts) > 3 else "" + if id_ not in debt_by: + continue + key = (path, line, id_) + if key in seen_debt: + continue + seen_debt.add(key) + try: + line_n = int(line) + except ValueError: + line_n = 0 + debt_by[id_].append({"path": path, "line": line_n, "excerpt": excerpt}) + +def status_for(id_: str) -> str: + typ = id_.split("-", 1)[0] + if typ == "AC": + if id_ in tested: + return "verified" + if id_ in pending: + return "tracked-debt" + return "GAP" + # US/FR/NFR: planning altitude — backlog when no own carrier; not silent-gap / GAP. + if id_ in covered or id_ in tested: + return "verified" + if id_ in pending: + return "tracked-debt" + return "backlog" + +rows = [] +status_counts = {"verified": 0, "tracked-debt": 0, "GAP": 0, "backlog": 0} +ac_count = 0 +covered_count = 0 +for id_ in ids: + typ = id_.split("-", 1)[0] + st = status_for(id_) + status_counts[st] += 1 + if typ == "AC": + ac_count += 1 + if st == "verified": + covered_count += 1 + rows.append({ + "id": id_, + "type": typ, + "statement": statements.get(id_, id_), + "status": st, + "implementations": impl_by.get(id_, []), + "proofs": proof_by.get(id_, []), + "debtTasks": debt_by.get(id_, []), + "attestedBy": None, + }) + +doc = { + "schemaVersion": 3, + "format": "clew", + "emitter": "clewseau-gate", + "targetName": target, + "repoPath": repo, + "generatedAt": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", + "gate": { + "ok": not gate_failed and len(failures) == 0, + "failures": failures, + }, + "totals": { + "registryIdCount": len(ids), + "acCount": ac_count, + "coveredCount": covered_count, + }, + "statusCounts": status_counts, + "rows": rows, +} + +# Prefer structured failures if bash recorded any; else respect CLEW_FAIL. +if failures: + doc["gate"]["ok"] = False + +out_path.parent.mkdir(parents=True, exist_ok=True) +out_path.write_text(json.dumps(doc, indent=2) + "\n", encoding="utf-8") +print(f"Wrote {out_path} ({len(rows)} rows) gate.ok={doc['gate']['ok']}", flush=True) +PY + +if [[ "$fail" -ne 0 ]]; then + echo "Clewseau Gate 2: FAILED" >&2 + exit 1 +fi + +echo "Clewseau Gate 2: OK ($(wc -l < "$tmp/registry.txt" | tr -d ' ') registry IDs)" diff --git a/.specify/feature.json b/.specify/feature.json index 393529c..6113c0e 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1 +1,3 @@ -{"feature_directory":"specs/001-mvp"} +{ + "feature_directory": "specs/002-home-name-normalize" +} diff --git a/.specify/integrations/claude.manifest.json b/.specify/integrations/claude.manifest.json index 308ba69..593ce62 100644 --- a/.specify/integrations/claude.manifest.json +++ b/.specify/integrations/claude.manifest.json @@ -1,7 +1,7 @@ { "integration": "claude", "version": "0.15.3.dev0", - "installed_at": "2026-08-04T18:24:39.287860+00:00", + "installed_at": "2026-08-06T20:51:33.845561+00:00", "files": { ".claude/skills/speckit-analyze/SKILL.md": "e1f8323285ed23a9ec6b74a6436d7ebd23a3511778d5cc181bd27afa2b545427", ".claude/skills/speckit-clarify/SKILL.md": "f4b3f2c95087ac2343c0b67faff67f7223d34213ca1816aa25908db5b9aff0ac", diff --git a/.specify/integrations/speckit.manifest.json b/.specify/integrations/speckit.manifest.json index 2580bf7..215aca7 100644 --- a/.specify/integrations/speckit.manifest.json +++ b/.specify/integrations/speckit.manifest.json @@ -1,7 +1,7 @@ { "integration": "speckit", "version": "0.15.3.dev0", - "installed_at": "2026-06-26T14:27:06.744456+00:00", + "installed_at": "2026-08-06T20:51:33.855852+00:00", "files": { ".specify/scripts/bash/common.sh": "6ff86bf39f6b4684b0f80927dc7a1dadec26b4671988a3fe4d6c2523cbd3aa22", ".specify/scripts/bash/setup-plan.sh": "4469b22960f43c07c33dca00de6dedb252145e9a9ce8fbb0e63be82e02b082ab", diff --git a/.specify/memory/.constitution-template.json b/.specify/memory/.constitution-template.json new file mode 100644 index 0000000..7ff59f4 --- /dev/null +++ b/.specify/memory/.constitution-template.json @@ -0,0 +1,4 @@ +{ + "sha256": "ce246ad01b25a37decf728ba20c5c23f770ba4cfc981a55224f5ce1e6c9f52dd", + "source": "clewseau v0.1.0" +} diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 897ae2b..89f6e8f 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,63 +1,80 @@ -# HomesFlow Constitution - -Non-negotiable laws for this codebase. Product requirements live in `HomesFlow.prd.md`. Traceability mechanics live in `traceability.md`. +# [PROJECT_NAME] Constitution + ## Core Principles -### I. Spec-Driven Development (NON-NEGOTIABLE) - -No production code without an approved feature spec, plan, and tasks under `specs/`. Run `/speckit.analyze` and resolve all violations before `/speckit.implement`. - -### II. Native iOS First - -HomesFlow ships as a native iOS app (Swift / SwiftUI) for iPhone and iPad. No Android, web, or desktop in the initial release (per PRD non-goals). +### [PRINCIPLE_1_NAME] + +[PRINCIPLE_1_DESCRIPTION] + -### III. Offline-Capable Sync (NON-NEGOTIABLE) +### [PRINCIPLE_2_NAME] + +[PRINCIPLE_2_DESCRIPTION] + -Local caching and offline sync are core from day one. Conflict resolution follows the PRD's data-type-aware model (AC-SYNC-01, AC-SYNC-05…07): conflicts resolve automatically by most-recent timestamp, except that terminal step statuses (Complete / N/A) are never silently regressed; the losing user is always notified with guidance to re-apply. Structural actions require connectivity. The PRD's Offline sync AC section is authoritative for the per-data-type rules. +### [PRINCIPLE_3_NAME] + +[PRINCIPLE_3_DESCRIPTION] + -### IV. Role-Based Access +### [PRINCIPLE_4_NAME] + +[PRINCIPLE_4_DESCRIPTION] + -Every data operation respects Owner / Manager / Guest roles per home. UI and API must fail closed — insufficient permission blocks the action with a clear message. +### [PRINCIPLE_5_NAME] + +[PRINCIPLE_5_DESCRIPTION] + -### V. End-to-End Traceability (NON-NEGOTIABLE) +## [SECTION_2_NAME] + -Every FR, NFR, and AC carries a durable ID assigned at the PRD level (`--`). Each AC is atomic and maps to at least one automated test. Every task in `tasks.md` declares `Traces: `. `/speckit.analyze` MUST report zero traceability violations before implementation. +[SECTION_2_CONTENT] + -Full mechanics: `traceability.md`. +## [SECTION_3_NAME] + -### VI. Accessible by Design +[SECTION_3_CONTENT] + -UI MUST respect iOS accessibility settings — especially Dynamic Type, VoiceOver, Reduce Motion, and sufficient contrast. Layouts MUST remain usable at all supported content size categories (**NFR-A11Y-01**). Accessibility is MVP scope, not post-launch polish. - -## Technology Constraints +## Governance + -- **Client**: Swift / SwiftUI (iOS) -- **Backend**: Supabase (PostgreSQL, auth, real-time sync) — see `specs/001-mvp/dev-notes.md` for rationale -- **Process**: Spec Kit (`.specify/`, `specs/`) -- **Testing**: XCTest / XCUITest when source exists +[GOVERNANCE_RULES] + -## Hierarchy of Truth +**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] + -When documents conflict, higher layers win: -1. This constitution (`.specify/memory/constitution.md`) -2. `glossary.md` -3. `HomesFlow.prd.md` (product requirements and AC registry) -4. `specs//spec.md` (feature slice derived from PRD) -5. `specs//plan.md` and `tasks.md` -6. Source code -**Scope vs process.** Product scope and acceptance criteria are authoritative only in `HomesFlow.prd.md`. This constitution governs *how* we build (process, architecture, traceability). If the PRD is silent on a product behavior, do not invent scope here — amend the PRD. If documents conflict on *engineering law*, this constitution wins; on *what the product must do*, the PRD wins. + -External planning aids (e.g. StoriesOnBoard story maps) may inform humans when drafting or revising the PRD. They are not part of this hierarchy and are not Gate-2-enforced. +### Article: End-to-End Traceability (NON-NEGOTIABLE) -## Governance +Every functional requirement, non-functional requirement, and acceptance criterion carries a durable unique ID of the form `--` (e.g. `FR-LOG-01`, `AC-OFFL-03`). IDs are assigned once at the PRD level and are never reused or renumbered; retired IDs are tombstoned, not recycled. -Amendments require a version bump and brief rationale. Traceability mechanics live in `traceability.md`; engineering and toolchain notes in `specs/001-mvp/dev-notes.md`. Neither overrides this file or the PRD. +1. Each acceptance criterion is **atomic** — one independently testable assertion — and maps to at least one automated test *or* an explicitly tracked debt entry. Silent-gap refusal is at **AC altitude**; US/FR/NFR IDs are planning labels (clew status `backlog`), not silent-gap candidates. +2. Every task in `tasks.md` MUST declare the ID(s) it implements via a `Traces:` field. +3. Every verifying test MUST encode the AC ID it protects. Every requirement-bearing source module MUST carry a coverage annotation naming the ID. +4. Coverage is **bidirectional** and machine-checked: no silent AC gaps, no untraced scope, and exact-set registry ≡ specs ≡ tasks. CI fails the build on any of these. +5. `/speckit.analyze` MUST report zero Clewseau traceability violations before `/speckit.implement` runs. -**Version**: 1.2.1 | **Ratified**: 2026-06-28 | **Last Amended**: 2026-07-17 +### Article: Clewseau vocabulary -**1.2.1 rationale**: Clarified Hierarchy of Truth so agents do not blur process law (constitution) with product scope (PRD). Story maps and similar tools stay outside the machine-checked chain. +Use these terms; do not invent synonyms (especially not “dossier”). -**1.2.0 rationale**: Principle III previously hardcoded blanket timestamp-wins. Amended to defer to the PRD's data-type-aware conflict model (story-map decision 2026-07-03): automatic resolution with loser notification, terminal-status protection, connectivity-gated structural actions. Keeps the constitution stable as per-data-type rules evolve in the PRD. +| Term | Meaning | +|------|---------| +| **clew** | The Gate-emitted traceability artifact (`format: "clew"`). Default filename `clew.json`. | +| **clew.json** | Usual on-disk path for a clew (configurable via Gate `clew_path`). | +| **Clewseau** | Spec Kit overlay: durable IDs, Gate 2, clew emission. Not Thorsten Schlathölter’s open-source `clew` tool. | +| **clewloupe** | Viewer that consumes a clew only — no target re-scan. | +| **verified** | Named carrier exists (AC proof and/or `@covers` / proof for US/FR/NFR). | +| **tracked-debt** | Incomplete, but declared on an open task with `Traces:`. | +| **GAP** | Silent AC gap — neither proof nor open debt; Gate refuses; thread frays. | +| **backlog** | US/FR/NFR with no own carrier — planning altitude, not a silent gap. | +| **Gate 2** | Deterministic Clewseau check + clew emit (`speckit.clewseau-gate.check`). | diff --git a/.specify/presets/.registry b/.specify/presets/.registry new file mode 100644 index 0000000..205e799 --- /dev/null +++ b/.specify/presets/.registry @@ -0,0 +1,15 @@ +{ + "schema_version": "1.0", + "presets": { + "clewseau": { + "version": "0.1.0", + "source": "local", + "manifest_hash": "sha256:b00105f1e0ebc2caa5ece6fd23f1d6431053cf8ce76a531a47b5dcd99b10c82b", + "enabled": true, + "priority": 10, + "registered_commands": {}, + "registered_skills": {}, + "installed_at": "2026-08-06T20:51:47.715671+00:00" + } + } +} \ No newline at end of file diff --git a/.specify/presets/clewseau/GLOSSARY.md b/.specify/presets/clewseau/GLOSSARY.md new file mode 100644 index 0000000..74a5ee1 --- /dev/null +++ b/.specify/presets/clewseau/GLOSSARY.md @@ -0,0 +1,17 @@ +# Clewseau vocabulary + +Copy into the project glossary (e.g. `glossary.md`) when the repo keeps one. The same terms are appended to the constitution via the Clewseau preset. + +| Term | Meaning | +|------|---------| +| **clew** | The Gate-emitted traceability artifact (`format: "clew"`). Default filename `clew.json`. | +| **clew.json** | Usual on-disk path for a clew (configurable via Gate `clew_path`). | +| **Clewseau** | Spec Kit overlay: durable IDs, Gate 2, clew emission. Not Thorsten Schlathölter’s open-source `clew` tool. | +| **clewloupe** | Viewer that consumes a clew only — no target re-scan. | +| **verified** | Named carrier exists (AC proof and/or `@covers` / proof for US/FR/NFR). | +| **tracked-debt** | Incomplete, but declared on an open task with `Traces:`. | +| **GAP** | Silent AC gap — neither proof nor open debt; Gate refuses; thread frays. | +| **backlog** | US/FR/NFR with no own carrier — planning altitude, not a silent gap. | +| **Gate 2** | Deterministic Clewseau check + clew emit (`speckit.clewseau-gate.check`). | + +Do not use **dossier** for this artifact — in Clewseau it is a **clew**. diff --git a/.specify/presets/clewseau/README.md b/.specify/presets/clewseau/README.md new file mode 100644 index 0000000..ca4ded8 --- /dev/null +++ b/.specify/presets/clewseau/README.md @@ -0,0 +1,13 @@ +# Clewseau preset + +Appends Clewseau's durable-ID rules and vocabulary onto Spec Kit core templates via `append` strategy. Does not replace Spec Kit's workflow. + +Install: + +```bash +specify preset add --dev /path/to/clewseau/presets/clewseau +``` + +Vocabulary (clew, statuses, Gate 2) lands in the constitution template. For projects that keep a separate glossary, also merge [`GLOSSARY.md`](./GLOSSARY.md). + +See the repo root [`PROMOTION-CONTRACT.md`](../../PROMOTION-CONTRACT.md). diff --git a/.specify/presets/clewseau/preset.yml b/.specify/presets/clewseau/preset.yml new file mode 100644 index 0000000..6916589 --- /dev/null +++ b/.specify/presets/clewseau/preset.yml @@ -0,0 +1,40 @@ +schema_version: "1.0" + +preset: + id: "clewseau" + name: "Clewseau" + version: "0.1.0" + description: "Appends durable-ID, Traces, and Clewseau vocabulary onto Spec Kit spec, tasks, and constitution templates." + author: "Rik Dryfoos / Dryfoos Consulting" + license: "MIT" + homepage: "https://github.com/rdryfoos/clewseau" + repository: "https://github.com/rdryfoos/clewseau" + +requires: + speckit_version: ">=0.14.0" + +provides: + templates: + - type: "template" + name: "spec-template" + file: "templates/spec-template.md" + description: "Require inherited durable IDs and a risk table traced to FR/AC IDs" + strategy: "append" + + - type: "template" + name: "tasks-template" + file: "templates/tasks-template.md" + description: "Require Traces: on every task" + strategy: "append" + + - type: "template" + name: "constitution-template" + file: "templates/constitution-template.md" + description: "Append End-to-End Traceability article and Clewseau vocabulary" + strategy: "append" + +tags: + - "traceability" + - "durable-ids" + - "governance" + - "sdd" diff --git a/.specify/presets/clewseau/templates/constitution-template.md b/.specify/presets/clewseau/templates/constitution-template.md new file mode 100644 index 0000000..098e20f --- /dev/null +++ b/.specify/presets/clewseau/templates/constitution-template.md @@ -0,0 +1,28 @@ + + + +### Article: End-to-End Traceability (NON-NEGOTIABLE) + +Every functional requirement, non-functional requirement, and acceptance criterion carries a durable unique ID of the form `--` (e.g. `FR-LOG-01`, `AC-OFFL-03`). IDs are assigned once at the PRD level and are never reused or renumbered; retired IDs are tombstoned, not recycled. + +1. Each acceptance criterion is **atomic** — one independently testable assertion — and maps to at least one automated test *or* an explicitly tracked debt entry. Silent-gap refusal is at **AC altitude**; US/FR/NFR IDs are planning labels (clew status `backlog`), not silent-gap candidates. +2. Every task in `tasks.md` MUST declare the ID(s) it implements via a `Traces:` field. +3. Every verifying test MUST encode the AC ID it protects. Every requirement-bearing source module MUST carry a coverage annotation naming the ID. +4. Coverage is **bidirectional** and machine-checked: no silent AC gaps, no untraced scope, and exact-set registry ≡ specs ≡ tasks. CI fails the build on any of these. +5. `/speckit.analyze` MUST report zero Clewseau traceability violations before `/speckit.implement` runs. + +### Article: Clewseau vocabulary + +Use these terms; do not invent synonyms (especially not “dossier”). + +| Term | Meaning | +|------|---------| +| **clew** | The Gate-emitted traceability artifact (`format: "clew"`). Default filename `clew.json`. | +| **clew.json** | Usual on-disk path for a clew (configurable via Gate `clew_path`). | +| **Clewseau** | Spec Kit overlay: durable IDs, Gate 2, clew emission. Not Thorsten Schlathölter’s open-source `clew` tool. | +| **clewloupe** | Viewer that consumes a clew only — no target re-scan. | +| **verified** | Named carrier exists (AC proof and/or `@covers` / proof for US/FR/NFR). | +| **tracked-debt** | Incomplete, but declared on an open task with `Traces:`. | +| **GAP** | Silent AC gap — neither proof nor open debt; Gate refuses; thread frays. | +| **backlog** | US/FR/NFR with no own carrier — planning altitude, not a silent gap. | +| **Gate 2** | Deterministic Clewseau check + clew emit (`speckit.clewseau-gate.check`). | diff --git a/.specify/presets/clewseau/templates/spec-template.md b/.specify/presets/clewseau/templates/spec-template.md new file mode 100644 index 0000000..b81fa00 --- /dev/null +++ b/.specify/presets/clewseau/templates/spec-template.md @@ -0,0 +1,19 @@ + + + + +## Clewseau — durable IDs (required) + +Every requirement and acceptance criterion MUST carry a durable ID per the project Clewseau / traceability standard. + +- IDs are assigned **once at the PRD (registry) level**. This feature spec **inherits** them; do **not** mint new IDs here. +- Grammar: `--` where TYPE is `US` | `FR` | `NFR` | `AC` (example: `AC-SYNC-04`). +- Acceptance criteria are **atomic** — one independently testable assertion each. Split compounds before specify. + +## Risk & failure modes (required) + +List material failures, user impact, and mitigations traced to FR/AC IDs: + +| Failure | User impact | Mitigation / trace | +|---------|-------------|-------------------| +| [e.g. sync conflict] | [user-visible effect] | [e.g. AC-SYNC-01] | diff --git a/.specify/presets/clewseau/templates/tasks-template.md b/.specify/presets/clewseau/templates/tasks-template.md new file mode 100644 index 0000000..a257fb5 --- /dev/null +++ b/.specify/presets/clewseau/templates/tasks-template.md @@ -0,0 +1,11 @@ + + + + +## Clewseau — Traces (required) + +Every task MUST declare the registry ID(s) it implements: + +- Format on each task line: `**Traces**: AC-…, FR-…` (one or more IDs from the PRD registry). +- Do not invent IDs in tasks. If an ID is missing from the registry, stop and fix the PRD first. +- Test tasks that verify an AC SHOULD encode that AC in the test name (e.g. `test_AC_SYNC_04_…`). diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md index 804427e..ceb2877 100644 --- a/.specify/templates/spec-template.md +++ b/.specify/templates/spec-template.md @@ -8,20 +8,6 @@ **Input**: User description: "$ARGUMENTS" -> Every requirement and AC MUST carry a durable ID per `traceability.md`. IDs are assigned at the PRD level in `HomesFlow.prd.md` — feature specs inherit them; do not mint new IDs here. - -## Intended Use - -[One paragraph: who uses this feature, for what purpose, in what context.] - -## Risk & failure modes - -Per `traceability.md` §9.3 — list material failures, user impact, and mitigations traced to FR/AC IDs. - -| Failure | User impact | Mitigation / trace | -|---------|-------------|-------------------| -| [e.g. sync conflict on step status] | [e.g. surprising checklist state] | [e.g. AC-PROC-03, AC-SYNC-01] | - ## User Scenarios & Testing *(mandatory)*