Release 1.1.2 — the default path works end to end

Fix release for 1.1.1: the minimal path end to end, engine commands that
run on Godot, Unity and Unreal, hook warnings you can see, and verdicts
that say when they could not check. Details in CHANGELOG.md; upgrade
steps in UPGRADING.md (v1.1.1 → v1.1.2).

Fixes #67
Fixes #131

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuJn2FdynEybfL5KC697nJ
This commit is contained in:
Donchitos
2026-09-29 21:32:19 +10:00
co-authored by Claude Opus 5.5
parent 7ed2c3e9c4
commit b21fa0f7f2
383 changed files with 22888 additions and 10618 deletions
+99 -20
View File
@@ -35,14 +35,17 @@ fi
input=$(cat)
# --- Parse JSON (jq with grep fallback) ---
# project_dir first: current_dir follows every `cd`, so after `cd src` the
# status line looked for project.yaml in src/ and fell back to guessing.
if command -v jq &>/dev/null; then
model=$(echo "$input" | jq -r '.model.display_name // "Unknown"')
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // ""')
cwd=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""')
else
model=$(echo "$input" | grep -oE '"display_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"//')
used_pct=$(echo "$input" | grep -oE '"used_percentage"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | sed 's/.*: *//')
cwd=$(echo "$input" | grep -oE '"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"//')
cwd=$(echo "$input" | grep -oE '"project_dir"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"//')
[ -z "$cwd" ] && cwd=$(echo "$input" | grep -oE '"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"//')
[ -z "$model" ] && model="Unknown"
fi
@@ -57,13 +60,26 @@ else
ctx_label="ctx: --"
fi
# --- Config helper, sourced ONCE, interpreter resolved ONCE ---
# Every read is `$(get_yaml_key ...)`, a subshell, so the interpreter the
# helper caches on first use never reached this shell, and every read probed
# for Python again first -- one interpreter spawn per setting read instead of
# one for the whole render. Resolving it here, in this shell, lets every
# subshell inherit it. Success is whether the function exists, not the
# source's exit status.
project_yaml="$cwd/project.yaml"
yaml_helper="$cwd/.claude/hooks/yaml-helper.sh"
helper=false
if [ -f "$yaml_helper" ]; then
source "$yaml_helper" 2>/dev/null
command -v get_yaml_key >/dev/null 2>&1 && helper=true
[ "$helper" = true ] && [ -f "$project_yaml" ] && _yaml_helper_resolve_python >/dev/null 2>&1
fi
# --- Production stage ---
# Priority 1: project.stage from project.yaml
stage=""
project_yaml="$cwd/project.yaml"
yaml_helper="$cwd/.claude/hooks/yaml-helper.sh"
if [ -f "$project_yaml" ] && [ -f "$yaml_helper" ]; then
source "$yaml_helper"
if [ -f "$project_yaml" ] && [ "$helper" = true ]; then
stage=$(get_yaml_key "$project_yaml" project.stage 2>/dev/null)
fi
# Priority 2: legacy stage.txt fallback
@@ -89,9 +105,7 @@ if [ -z "$stage" ]; then
[ -f "$systems_file" ] && has_systems=true
# Check if engine is configured (project.yaml first, fall back to technical-preferences.md)
if [ -f "$project_yaml" ] && [ -f "$yaml_helper" ]; then
# yaml-helper may have been sourced above for stage; sourcing again is idempotent
source "$yaml_helper"
if [ -f "$project_yaml" ] && [ "$helper" = true ]; then
engine_name=$(get_yaml_key "$project_yaml" engine.name 2>/dev/null)
[ -n "$engine_name" ] && engine_configured=true
fi
@@ -108,9 +122,17 @@ if [ -z "$stage" ]; then
fi
fi
# Count source files (language-agnostic)
if [ -d "$cwd/src" ]; then
src_count=$(find "$cwd/src" -type f \( -name "*.gd" -o -name "*.cs" -o -name "*.cpp" -o -name "*.h" -o -name "*.py" -o -name "*.rs" -o -name "*.lua" -o -name "*.tscn" -o -name "*.tres" \) 2>/dev/null | wc -l | tr -d ' ')
# Count source files (language-agnostic) under the engine's code root --
# src/, Assets/ or Source/ (directory-structure.md). Counting src/ alone left
# every Unity and Unreal project at 0, so this ladder never reached
# Production. Capped at the threshold below: the status line runs on every
# turn, and Unity's Assets/ can hold thousands of files, so find stops at 10.
code_root=""
if [ "$helper" = true ] && command -v resolve_code_root >/dev/null 2>&1; then
code_root=$(cd "$cwd" 2>/dev/null && resolve_code_root 2>/dev/null | cut -f1)
fi
if [ -n "$code_root" ] && [ -d "$cwd/$code_root" ]; then
src_count=$(find "$cwd/$code_root" -type f \( -name "*.gd" -o -name "*.cs" -o -name "*.cpp" -o -name "*.h" -o -name "*.py" -o -name "*.rs" -o -name "*.lua" -o -name "*.tscn" -o -name "*.tres" \) 2>/dev/null | head -n 10 | wc -l | tr -d ' ')
fi
# Check for ADRs (signals Pre-Production phase)
@@ -137,11 +159,11 @@ fi
# --- Process posture (modes.rigor) ---
# Locked to project.yaml (not locally overridable) with a plain terminal default
# of 'standard', so a direct get_yaml_key read + default is exact. Deliberately
# NOT resolve_setting: that assumes PWD is the project root, unsafe here since the
# status line works from an absolute $cwd and never cd's.
# in _yaml_helper_defaults, so a direct get_yaml_key read + default is exact.
# Deliberately NOT resolve_setting: that assumes PWD is the project root, unsafe
# here since the status line works from an absolute $cwd and never cd's.
#
# The 'standard' default applies ONLY when nothing contradicts it. If `rigor` is
# The default applies ONLY when nothing contradicts it. If `rigor` is
# unset but a knob it fronts is set explicitly, the project's real process weight
# is whatever that knob says, and printing 'standard' actively misreports it —
# migration is the common case, writing `modes.review_mode` and no `modes.rigor`,
@@ -150,11 +172,21 @@ fi
# config, no claim. Suppression is correct rather than lossy — the fronted knobs
# disagree with each other in this state, so there is no single honest posture.
rigor=""
if [ -f "$project_yaml" ] && [ -f "$yaml_helper" ]; then
source "$yaml_helper"
rigor_val="" # the resolved rigor, kept even when the displayed posture is suppressed
if [ -f "$project_yaml" ] && [ "$helper" = true ]; then
rigor=$(get_yaml_key "$project_yaml" modes.rigor 2>/dev/null)
rigor_val="$rigor"
if [ -z "$rigor" ]; then
rigor="standard"
# The default comes from its single source. This line hard-coded 'standard'
# and kept printing it after 1.1.0 flipped the default to 'minimal' --
# 'Concept · standard' on the shipped project.yaml while every skill resolved
# minimal. Read in-process: a $(get_yaml_default ...) is a spawn every turn.
while IFS= read -r _d; do
case "$_d" in "modes.rigor::"*) rigor="${_d#modes.rigor::}"; break ;; esac
done <<EOF
$_yaml_helper_defaults
EOF
rigor_val="$rigor"
# Cheap pre-filter first. This hook runs every turn, and the common case
# (nothing fronted set) must not cost a get_yaml_key subprocess per key.
# The grep is a deliberate SUPERSET — it matches the leaf names anywhere at
@@ -174,6 +206,52 @@ if [ -f "$project_yaml" ] && [ -f "$yaml_helper" ]; then
fi
fi
# --- Minimal path position (workflow: minimal) ---
# Nothing on the minimal path runs /gate-check, so project.stage never leaves
# Concept there: 'Concept · minimal' for a project with most of its stories
# built. At that tier the path position is the honest stage -- stories Complete
# of stories written, from the header Status lines /dev-story and /story-done
# keep. project.stage itself is left alone: only a /gate-check PASS (or a
# CONCERNS whose risks you accepted) moves it.
# Gated on the RESOLVED workflow: an explicit modes.workflow wins, otherwise it
# follows rigor. Gating on rigor alone left an explicit `workflow: minimal`
# under another rigor showing Concept. Builtins only -- no spawn per story.
path_pos=""
_wf=""
if [ -f "$project_yaml" ] && [ "$helper" = true ]; then
_wf="$rigor_val"
IFS= read -r -d '' _yaml < "$project_yaml"
case "$_yaml" in
*[[:space:]]workflow:*)
_w=$(get_yaml_key "$project_yaml" modes.workflow 2>/dev/null)
[ -n "$_w" ] && _wf="$_w" ;;
esac
fi
if [ "$_wf" = "minimal" ]; then
_total=0; _done=0
for _s in "$cwd"/production/epics/*/story-*.md; do
[ -f "$_s" ] || continue
_total=$((_total+1))
while IFS= read -r _line || [ -n "$_line" ]; do
# Normalise `> **Status**: X`, `**Status:** X`, `- Status: X` to
# `Status: X`, then match the value's leading word EXACTLY, in any case:
# a substring match on *Done* counted "Not Done". The word ends at the
# first non-letter, so `Done.` and `Complete, verified` count -- the
# rule story-status.sh applies, which /help and /sprint-status read.
# `|| [ -n ]` reads a last line with no newline, which read -r drops.
_l="${_line%$'\r'}"; _l="${_l#"${_l%%[![:space:]]*}"}"; _l="${_l#> }"; _l="${_l#- }"; _l="${_l//\*/}"
case "$_l" in
Status:*)
_v="${_l#Status:}"; _v="${_v#"${_v%%[![:space:]]*}"}"; _v="${_v%%[!A-Za-z]*}"
case "$_v" in [Cc][Oo][Mm][Pp][Ll][Ee][Tt][Ee]|[Dd][Oo][Nn][Ee]) _done=$((_done+1)) ;; esac
break ;;
esac
done < "$_s"
done
if [ "$_total" -gt 0 ]; then path_pos="Minimal · ${_done}/${_total} stories"
else path_pos="Minimal · brief"; fi
fi
# --- Epic/Feature/Task breadcrumb (Production+ only) ---
breadcrumb=""
if [ "$stage" = "Production" ] || [ "$stage" = "Polish" ] || [ "$stage" = "Release" ]; then
@@ -206,4 +284,5 @@ if [ "$stage" = "Production" ] || [ "$stage" = "Polish" ] || [ "$stage" = "Relea
fi
# --- Assemble ---
printf "%s" "${ctx_label} | ${model} | ${stage}${rigor:+ · $rigor}${breadcrumb}"
if [ -n "$path_pos" ]; then segment="$path_pos"; else segment="${stage}${rigor:+ · $rigor}"; fi
printf "%s" "${ctx_label} | ${model} | ${segment}${breadcrumb}"