Release 1.1.0 — one config file, and a process weight you choose

`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
This commit is contained in:
Donchitos
2026-09-23 12:25:11 +10:00
co-authored by Claude Opus 5
parent 984023ddac
commit d05699707f
291 changed files with 24374 additions and 7740 deletions
+100 -12
View File
@@ -1,4 +1,32 @@
#!/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.
#
@@ -12,9 +40,9 @@ if command -v jq &>/dev/null; then
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"\s*:\s*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"//')
used_pct=$(echo "$input" | grep -oE '"used_percentage"\s*:\s*[0-9]+' | head -1 | sed 's/.*: *//')
cwd=$(echo "$input" | grep -oE '"current_dir"\s*:\s*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"//')
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
@@ -30,14 +58,23 @@ else
fi
# --- Production stage ---
# Priority 1: Explicit stage from stage.txt
stage_file="$cwd/production/stage.txt"
# Priority 1: project.stage from project.yaml
stage=""
if [ -f "$stage_file" ]; then
stage=$(head -1 "$stage_file" | tr -d '\r\n')
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 2: Auto-detect from artifacts
# 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"
@@ -51,9 +88,21 @@ if [ -z "$stage" ]; then
[ -f "$concept_file" ] && has_concept=true
[ -f "$systems_file" ] && has_systems=true
# Check if engine is configured (not placeholder)
if [ -f "$tech_prefs" ]; then
engine_line=$(grep -m1 '^\*\*Engine\*\*:' "$tech_prefs" 2>/dev/null || 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
@@ -86,6 +135,45 @@ if [ -z "$stage" ]; then
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
@@ -118,4 +206,4 @@ if [ "$stage" = "Production" ] || [ "$stage" = "Polish" ] || [ "$stage" = "Relea
fi
# --- Assemble ---
printf "%s" "${ctx_label} | ${model} | ${stage}${breadcrumb}"
printf "%s" "${ctx_label} | ${model} | ${stage}${rigor:+ · $rigor}${breadcrumb}"