mirror of
https://github.com/Donchitos/Claude-Code-Game-Studios.git
synced 2026-10-16 01:37:05 +00:00
`project.yaml` at the repo root is now the single source of truth for a project's configuration, replacing `production/stage.txt`, `production/review-mode.txt` and `.claude/docs/technical-preferences.md` (all three still read as a fallback; nothing is force-deleted). **`modes.rigor` — one question instead of six.** `minimal`, `standard` or `full` fronts six knobs: `modes.workflow`, `docs.density`, `qa.level`, `modes.story_granularity`, `modes.review_mode` and `team.size`. It defaults to `minimal`, which is a change from 1.0's behaviour: the heavier tier was measured costing several times more to reach working code without producing a better result, and returning nothing when a fresh developer inherited the project. Pin `modes.rigor: standard` to keep 1.0's process level. Also in this release: - `/settings` to view or change any setting, and `project.local.yaml` for per-developer preferences that should not be committed. - `system_overrides` to hold one system to a higher standard than the rest of the project, and `testing.strict` to set per test type whether missing evidence blocks a story or warns. - A story that changes anything the player sees is not closed until the game has been launched, observed, and a screenshot retained in `production/qa/evidence/`. A parse check is not a run. This holds at every rigor level, including `minimal`, where automated tests are waived but the look is not. - The code root is engine-specific and enforced as such: Godot `src/`, Unity `Assets/`, Unreal `Source/<Module>/`. Two of the three will not build a project laid out any other way. - Migration tooling for existing projects: `.claude/scripts/migrate-v1-config.sh` converts a 1.0 layout, non-destructively by default, with a report to review before `--finalize` deletes anything. - Ask-before-write holds regardless of personal automation settings. Upgrading from 1.0 or earlier: see UPGRADING.md. Full list of changes: CHANGELOG.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016xvzG3i7pWQg4LjyDwWMXd
210 lines
8.7 KiB
Bash
210 lines
8.7 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# --- work from the project root ----------------------------------------------
|
|
# Every path below is repo-relative, so a hook invoked with a working directory
|
|
# that is not the repo root would silently read and write the WRONG TREE --
|
|
# returning a near-empty result instead of the session-recovery block, and
|
|
# creating stray trees such as docs/production/session-logs/ on write.
|
|
#
|
|
# PRECEDENCE IS LOAD-BEARING. A cwd that IS a project root carries real
|
|
# information and must win: a caller sitting inside another project means that
|
|
# project, not this one. Resolving to the script's own location first would
|
|
# override them. So, in order:
|
|
# 1. cwd holds project.yaml -> cwd (a project root)
|
|
# 2. cwd holds .claude/ -> cwd (a project root not yet configured)
|
|
# 3. CLAUDE_PROJECT_DIR -> that (populated in the hook environment)
|
|
# 4. this script's location -> <root>/.claude/hooks/../.. by construction
|
|
# Rule 4 always works and needs no environment at all; rules 1-2 stop it from
|
|
# overriding a caller that legitimately means somewhere else.
|
|
#
|
|
# NOT an upward search: that resolves a nested project to its parent's config.
|
|
if [ -f "project.yaml" ] || [ -d ".claude" ]; then
|
|
CCGS_ROOT="$PWD"
|
|
elif [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR}" ]; then
|
|
CCGS_ROOT="$CLAUDE_PROJECT_DIR"
|
|
else
|
|
CCGS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)"
|
|
fi
|
|
[ -n "$CCGS_ROOT" ] && cd "$CCGS_ROOT" 2>/dev/null || true
|
|
|
|
# Claude Code Game Studios — Status Line
|
|
# Receives JSON on stdin, outputs a single-line status.
|
|
#
|
|
# Segments: ctx% | model | production stage [| Epic > Feature > Task]
|
|
|
|
input=$(cat)
|
|
|
|
# --- Parse JSON (jq with grep fallback) ---
|
|
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 // ""')
|
|
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/"//')
|
|
[ -z "$model" ] && model="Unknown"
|
|
fi
|
|
|
|
# Normalize Windows paths
|
|
cwd=$(echo "$cwd" | sed 's|\\|/|g')
|
|
[ -z "$cwd" ] && cwd="."
|
|
|
|
# --- Context usage ---
|
|
if [ -n "$used_pct" ]; then
|
|
ctx_label="ctx: ${used_pct}%"
|
|
else
|
|
ctx_label="ctx: --"
|
|
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"
|
|
stage=$(get_yaml_key "$project_yaml" project.stage 2>/dev/null)
|
|
fi
|
|
# Priority 2: legacy stage.txt fallback
|
|
if [ -z "$stage" ]; then
|
|
stage_file="$cwd/production/stage.txt"
|
|
if [ -f "$stage_file" ]; then
|
|
stage=$(head -1 "$stage_file" | tr -d '\r\n')
|
|
fi
|
|
fi
|
|
|
|
# Priority 3: Auto-detect from artifacts
|
|
if [ -z "$stage" ]; then
|
|
concept_file="$cwd/design/gdd/game-concept.md"
|
|
systems_file="$cwd/design/gdd/systems-index.md"
|
|
tech_prefs="$cwd/.claude/docs/technical-preferences.md"
|
|
|
|
has_concept=false
|
|
has_systems=false
|
|
engine_configured=false
|
|
src_count=0
|
|
|
|
[ -f "$concept_file" ] && has_concept=true
|
|
[ -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"
|
|
engine_name=$(get_yaml_key "$project_yaml" engine.name 2>/dev/null)
|
|
[ -n "$engine_name" ] && engine_configured=true
|
|
fi
|
|
if [ "$engine_configured" = false ] && [ -f "$tech_prefs" ]; then
|
|
# Leading whitespace tolerated, matching detect-gaps.sh and the migrator.
|
|
# This is the THIRD copy of "is the engine configured in
|
|
# technical-preferences.md" in the tree, and it was the last one still
|
|
# anchored to column 0 -- an indented bullet read as unconfigured here while
|
|
# the other two read it as configured, so the auto-detect ladder below
|
|
# dropped the project to an earlier stage than the rest of the system saw.
|
|
engine_line=$(grep -m1 -E '^[[:space:]]*-[[:space:]]+\*\*Engine\*\*:' "$tech_prefs" 2>/dev/null || true)
|
|
if [ -n "$engine_line" ] && ! echo "$engine_line" | grep -q "TO BE CONFIGURED"; then
|
|
engine_configured=true
|
|
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 ' ')
|
|
fi
|
|
|
|
# Check for ADRs (signals Pre-Production phase)
|
|
has_adrs=false
|
|
if ls "$cwd/docs/architecture/"adr-*.md 2>/dev/null | head -1 | grep -q .; then
|
|
has_adrs=true
|
|
fi
|
|
|
|
# Determine stage (check from most-advanced backward)
|
|
if [ "$src_count" -ge 10 ] 2>/dev/null; then
|
|
stage="Production"
|
|
elif [ "$has_adrs" = true ]; then
|
|
stage="Pre-Production"
|
|
elif [ "$engine_configured" = true ]; then
|
|
stage="Technical Setup"
|
|
elif [ "$has_systems" = true ]; then
|
|
stage="Systems Design"
|
|
elif [ "$has_concept" = true ]; then
|
|
stage="Concept"
|
|
else
|
|
stage="Concept"
|
|
fi
|
|
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.
|
|
#
|
|
# The 'standard' 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`,
|
|
# so every v1.0 upgrader running full director reviews read 'Production · standard'.
|
|
# Suppress instead of guessing, matching the unconfigured-project behaviour: no
|
|
# 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=$(get_yaml_key "$project_yaml" modes.rigor 2>/dev/null)
|
|
if [ -z "$rigor" ]; then
|
|
rigor="standard"
|
|
# 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
|
|
# depth, so a false positive only costs the precise checks below, while a
|
|
# miss is impossible. Never let it decide on its own: a bare `size:` under
|
|
# some unrelated block would suppress the posture with no reason.
|
|
_fronted='^[[:space:]]+(review_mode|workflow|density|level|story_granularity|size):[[:space:]]*[^[:space:]#]'
|
|
for _f in "$project_yaml" "$cwd/project.local.yaml"; do
|
|
[ -f "$_f" ] || continue
|
|
grep -qE "$_fronted" "$_f" 2>/dev/null || continue
|
|
for _k in modes.review_mode modes.workflow docs.density qa.level \
|
|
modes.story_granularity team.size; do
|
|
if [ -n "$(get_yaml_key "$_f" "$_k" 2>/dev/null)" ]; then rigor=""; break; fi
|
|
done
|
|
[ -z "$rigor" ] && break
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# --- Epic/Feature/Task breadcrumb (Production+ only) ---
|
|
breadcrumb=""
|
|
if [ "$stage" = "Production" ] || [ "$stage" = "Polish" ] || [ "$stage" = "Release" ]; then
|
|
state_file="$cwd/production/session-state/active.md"
|
|
if [ -f "$state_file" ]; then
|
|
# Parse structured STATUS block
|
|
in_block=false
|
|
epic="" feature="" task=""
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
*"<!-- STATUS -->"*) in_block=true; continue ;;
|
|
*"<!-- /STATUS -->"*) break ;;
|
|
esac
|
|
if [ "$in_block" = true ]; then
|
|
case "$line" in
|
|
Epic:*) epic=$(echo "$line" | sed 's/^Epic: *//') ;;
|
|
Feature:*) feature=$(echo "$line" | sed 's/^Feature: *//') ;;
|
|
Task:*) task=$(echo "$line" | sed 's/^Task: *//') ;;
|
|
esac
|
|
fi
|
|
done < "$state_file"
|
|
|
|
# Build breadcrumb from whatever is set
|
|
parts=""
|
|
[ -n "$epic" ] && parts="$epic"
|
|
[ -n "$feature" ] && parts="${parts:+$parts > }$feature"
|
|
[ -n "$task" ] && parts="${parts:+$parts > }$task"
|
|
[ -n "$parts" ] && breadcrumb=" | $parts"
|
|
fi
|
|
fi
|
|
|
|
# --- Assemble ---
|
|
printf "%s" "${ctx_label} | ${model} | ${stage}${rigor:+ · $rigor}${breadcrumb}"
|