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
289 lines
14 KiB
Bash
289 lines
14 KiB
Bash
#!/bin/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 SessionStart hook: Load project context at session start
|
|
# Outputs context information that Claude sees when a session begins
|
|
#
|
|
# Input schema (SessionStart): stdin IS supplied -- VERIFIED, not asserted.
|
|
# Observed on real harness events as a single line of JSON with session_id,
|
|
# transcript_path, cwd, hook_event_name and `source` ("clear", "compact").
|
|
# Do NOT read stdin here unboundedly -- the harness may leave it open, and a
|
|
# blocking read costs this hook its whole 10s budget on every session start.
|
|
#
|
|
# THIS HOOK MUST FINISH INSIDE ITS BUDGET. If it is killed partway, the
|
|
# session-state block never reaches context and the session starts blind --
|
|
# and nothing visible from inside the session says so. The early-exit gating
|
|
# and bounded reads below are what keep it inside 2s; treat them as load-
|
|
# bearing, not as optimisation.
|
|
#
|
|
# If that ever recurs, the diagnostic that works is a FIRE/DONE pair appended
|
|
# to a gitignored log at the top and bottom of this script -- "did it run at
|
|
# all" and "did it get here" are opposite failures with opposite fixes, and
|
|
# nothing else distinguishes them. /compact in particular CANNOT be used as
|
|
# evidence: PostCompact runs post-compact.sh, which prints the checkpoint
|
|
# independently, so seeing the checkpoint proves nothing about this script.
|
|
|
|
echo "=== Claude Code Game Studios — Session Context ==="
|
|
|
|
# Current branch
|
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
if [ -n "$BRANCH" ]; then
|
|
echo "Branch: $BRANCH"
|
|
|
|
# Recent commits
|
|
echo ""
|
|
echo "Recent commits:"
|
|
git log --oneline -5 2>/dev/null | while read -r line; do
|
|
echo " $line"
|
|
done
|
|
fi
|
|
|
|
# Resolve review_mode the same way skills do: resolve_setting applies the FULL
|
|
# chain (project.local.yaml -> project.yaml -> legacy review-mode.txt -> modes.rigor
|
|
# expansion -> default). modes.review_mode is rigor-fronted and locally overridable,
|
|
# so only resolve_setting reflects both a rigor-derived value (a project that set
|
|
# only `rigor: minimal` shows `solo`) and a local override. get_effective_yaml_key
|
|
# returns empty for a rigor-only project, which would leave the banner showing a
|
|
# stale `lean`.
|
|
REVIEW_MODE=""
|
|
if [ -f "project.yaml" ] && [ -f ".claude/hooks/yaml-helper.sh" ]; then
|
|
source .claude/hooks/yaml-helper.sh
|
|
REVIEW_MODE=$(resolve_setting modes.review_mode 2>/dev/null | cut -f1)
|
|
fi
|
|
if [ -z "$REVIEW_MODE" ] && [ -f "production/review-mode.txt" ]; then
|
|
REVIEW_MODE=$(head -1 production/review-mode.txt 2>/dev/null | tr -d '[:space:]')
|
|
fi
|
|
if [ -z "$REVIEW_MODE" ]; then
|
|
REVIEW_MODE="lean"
|
|
fi
|
|
echo ""
|
|
echo "Review mode: $REVIEW_MODE"
|
|
|
|
# Schema validation — surface invalid enum values in
|
|
# project.yaml and project.local.yaml so typos like `automation: chaotic`
|
|
# surface at session start rather than failing inside a skill.
|
|
if [ -f ".claude/hooks/yaml-helper.sh" ]; then
|
|
if ! type validate_yaml_enum >/dev/null 2>&1; then
|
|
source .claude/hooks/yaml-helper.sh
|
|
fi
|
|
# Hard-error guard: project.local.yaml requires a project.yaml base
|
|
if ! BASE_ERR=$(validate_local_yaml_base 2>&1); then
|
|
echo ""
|
|
echo "[!] $BASE_ERR"
|
|
fi
|
|
if [ -f "project.yaml" ]; then
|
|
SCHEMA_ERRORS=$(validate_yaml_enum project.yaml 2>&1)
|
|
if [ -n "$SCHEMA_ERRORS" ]; then
|
|
echo ""
|
|
echo "[!] project.yaml schema errors:"
|
|
echo "$SCHEMA_ERRORS" | sed 's/^/ /'
|
|
fi
|
|
fi
|
|
if [ -f "project.local.yaml" ]; then
|
|
SCHEMA_ERRORS=$(validate_yaml_enum project.local.yaml 2>&1)
|
|
if [ -n "$SCHEMA_ERRORS" ]; then
|
|
echo ""
|
|
echo "[!] project.local.yaml schema errors:"
|
|
echo "$SCHEMA_ERRORS" | sed 's/^/ /'
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# --- Stage source agreement ---
|
|
# `project.stage` in project.yaml is authoritative; production/stage.txt is a
|
|
# legacy mirror kept for unmigrated hooks. Twelve consumers read the mirror --
|
|
# including detect-gaps.sh and yaml-helper.sh -- so when the two disagree, part
|
|
# of the system acts on one stage and part on the other, silently.
|
|
#
|
|
# Warn, never reconcile. Only /gate-check on a PASS may change a stage, so a
|
|
# hook that "helpfully" rewrote the mirror would be advancing a phase gate
|
|
# nobody passed. Helpers emit observations, never verdicts (CLAUDE.md).
|
|
STAGE_YAML=""
|
|
STAGE_TXT=""
|
|
if [ -f "project.yaml" ] && command -v get_yaml_key >/dev/null 2>&1; then
|
|
STAGE_YAML=$(get_yaml_key project.yaml project.stage 2>/dev/null)
|
|
fi
|
|
if [ -f "production/stage.txt" ]; then
|
|
STAGE_TXT=$(head -1 production/stage.txt 2>/dev/null | tr -d '\r' | sed 's/[[:space:]]*$//')
|
|
fi
|
|
if [ -n "$STAGE_YAML" ] && [ -n "$STAGE_TXT" ] && [ "$STAGE_YAML" != "$STAGE_TXT" ]; then
|
|
echo ""
|
|
echo "[!] Stage sources disagree:"
|
|
echo " project.yaml project.stage : $STAGE_YAML (authoritative)"
|
|
echo " production/stage.txt : $STAGE_TXT (legacy mirror)"
|
|
echo " Twelve consumers read the mirror, so part of the system is acting on"
|
|
echo " each value. Only /gate-check on a PASS should change a stage — do not"
|
|
echo " hand-edit either file to silence this."
|
|
fi
|
|
|
|
# Current sprint (find most recent sprint file)
|
|
LATEST_SPRINT=$(ls -t production/sprints/sprint-*.md 2>/dev/null | head -1)
|
|
if [ -n "$LATEST_SPRINT" ]; then
|
|
echo ""
|
|
echo "Active sprint: $(basename "$LATEST_SPRINT" .md)"
|
|
fi
|
|
|
|
# Current milestone
|
|
LATEST_MILESTONE=$(ls -t production/milestones/*.md 2>/dev/null | head -1)
|
|
if [ -n "$LATEST_MILESTONE" ]; then
|
|
echo "Active milestone: $(basename "$LATEST_MILESTONE" .md)"
|
|
fi
|
|
|
|
# Open bug count
|
|
BUG_COUNT=0
|
|
for dir in tests/playtest production; do
|
|
if [ -d "$dir" ]; then
|
|
count=$(find "$dir" -name "BUG-*.md" 2>/dev/null | wc -l)
|
|
BUG_COUNT=$((BUG_COUNT + count))
|
|
fi
|
|
done
|
|
if [ "$BUG_COUNT" -gt 0 ]; then
|
|
echo "Open bugs: $BUG_COUNT"
|
|
fi
|
|
|
|
# Code health quick check.
|
|
#
|
|
# Reads the ENGINE-SPECIFIC code root, not a literal `src/`. Hardcoding `src/`
|
|
# made this line Godot-only: on a Unity or Unreal project the directory test
|
|
# failed, the banner printed no code-health line at all, and a clean project and
|
|
# an unreadable one looked identical. See resolve_code_root in yaml-helper.sh.
|
|
#
|
|
# Sourcing here is defensive: the two source sites above are both conditional,
|
|
# so by this point the function may or may not exist.
|
|
if [ -f .claude/hooks/yaml-helper.sh ] && ! command -v resolve_code_root >/dev/null 2>&1; then
|
|
. .claude/hooks/yaml-helper.sh 2>/dev/null
|
|
fi
|
|
if command -v resolve_code_root >/dev/null 2>&1; then
|
|
CODE_ROOT=$(resolve_code_root 2>/dev/null | cut -f1)
|
|
else
|
|
CODE_ROOT=""
|
|
fi
|
|
if [ -n "$CODE_ROOT" ] && [ -d "$CODE_ROOT" ]; then
|
|
TODO_COUNT=$(grep -r "TODO" "$CODE_ROOT/" 2>/dev/null | wc -l)
|
|
FIXME_COUNT=$(grep -r "FIXME" "$CODE_ROOT/" 2>/dev/null | wc -l)
|
|
if [ "$TODO_COUNT" -gt 0 ] || [ "$FIXME_COUNT" -gt 0 ]; then
|
|
echo ""
|
|
echo "Code health: ${TODO_COUNT} TODOs, ${FIXME_COUNT} FIXMEs in ${CODE_ROOT}/"
|
|
fi
|
|
fi
|
|
|
|
# --- Active session state recovery ---
|
|
# Only THIS block is gated by features.session_state, not the whole hook: the
|
|
# sprint/milestone/git context above is not part of the session-state pipeline
|
|
# and a user who turns that pipeline off still wants it. Gating the whole hook
|
|
# (as the original spec's "exit early at the top" wording implies) would take
|
|
# the branch and stage context away with it.
|
|
#
|
|
# Fail OPEN: this hook only sources yaml-helper.sh conditionally, so
|
|
# session_state_enabled may be undefined. An undefined function is falsey, which
|
|
# would silently suppress the recovery checkpoint -- the one piece of output
|
|
# whose absence the user cannot notice. Show the state unless we positively
|
|
# determined the flag is off.
|
|
STATE_FILE="production/session-state/active.md"
|
|
if [ -f .claude/hooks/yaml-helper.sh ] && ! command -v session_state_enabled >/dev/null 2>&1; then
|
|
. .claude/hooks/yaml-helper.sh
|
|
fi
|
|
if [ -f "$STATE_FILE" ] && { ! command -v session_state_enabled >/dev/null 2>&1 || session_state_enabled; }; then
|
|
echo ""
|
|
echo "=== ACTIVE SESSION STATE DETECTED ==="
|
|
echo "A previous session left state at: $STATE_FILE"
|
|
echo "Read this file to recover context and continue where you left off."
|
|
echo ""
|
|
# The CHECKPOINT region -- the same region pre-compact.sh injects.
|
|
#
|
|
# Previewing `tail -20` here while pre-compact takes `head -100` would put two
|
|
# consumers on opposite ends of one file, so which slice you got would
|
|
# depend on which hook happened to fire. Neither was wrong, because
|
|
# nothing defined where the recoverable state lived. The schema in
|
|
# .claude/docs/templates/session-state.md defines it; both read it now.
|
|
CHECKPOINT=$(sed -n '/<!-- CHECKPOINT -->/,/<!-- \/CHECKPOINT -->/p' "$STATE_FILE" 2>/dev/null \
|
|
| grep -v '<!-- /\?CHECKPOINT -->')
|
|
TOTAL_LINES=$(wc -l < "$STATE_FILE" 2>/dev/null | tr -d ' ')
|
|
# A sed range whose END address never matches runs to EOF. So a file with an
|
|
# opening marker and NO closing one produced a non-empty capture of the whole
|
|
# remaining file and took the healthy branch below -- silently previewing
|
|
# narrative under a "Checkpoint:" heading, with no warning and no template
|
|
# named. Emptiness cannot distinguish "no block" from
|
|
# "unterminated block"; only the markers can, so test them directly.
|
|
# rotate-session-state.sh already checks the CLOSING marker and refuses.
|
|
# Two consumers of one region must agree on what malformed means.
|
|
# `<!-- CHECKPOINT -->` cannot match `<!-- /CHECKPOINT -->` -- the slash sits
|
|
# where the space would be -- so these two counts are independent.
|
|
CP_OPEN=$(grep -c '<!-- CHECKPOINT -->' "$STATE_FILE" 2>/dev/null | tr -d ' ')
|
|
CP_CLOSE=$(grep -c '<!-- /CHECKPOINT -->' "$STATE_FILE" 2>/dev/null | tr -d ' ')
|
|
if [ "${CP_OPEN:-0}" -gt 0 ] && [ "${CP_CLOSE:-0}" -eq 0 ]; then
|
|
echo " [!] CHECKPOINT block is not terminated — no <!-- /CHECKPOINT --> marker."
|
|
echo " Not previewing it: without the closing marker the checkpoint"
|
|
echo " cannot be told from the narrative, and everything to the end"
|
|
echo " of the file would be shown as if it were recoverable state."
|
|
echo " Re-create from .claude/docs/templates/session-state.md."
|
|
echo " ... ($TOTAL_LINES total lines — read the full file to continue)"
|
|
elif [ -n "$CHECKPOINT" ]; then
|
|
echo "Checkpoint:"
|
|
printf '%s\n' "$CHECKPOINT"
|
|
echo " ... ($TOTAL_LINES total lines — read the full file for detail)"
|
|
else
|
|
echo "Quick summary (first 20 lines — no CHECKPOINT block in this file):"
|
|
head -20 "$STATE_FILE" 2>/dev/null
|
|
echo " ... ($TOTAL_LINES total lines — read the full file to continue)"
|
|
echo " NOTE: re-create from .claude/docs/templates/session-state.md so"
|
|
echo " recovery reads a bounded checkpoint instead of a slice."
|
|
fi
|
|
# Rotation is an OBSERVATION, never an action: helpers in .claude/scripts/
|
|
# emit observations, never verdicts (CLAUDE.md). The user decides.
|
|
if [ "${TOTAL_LINES:-0}" -gt 200 ] 2>/dev/null; then
|
|
echo " Note: $TOTAL_LINES lines. Narrative can be rotated into"
|
|
echo " production/session-logs/ — bash .claude/scripts/rotate-session-state.sh"
|
|
fi
|
|
echo "=== END SESSION STATE PREVIEW ==="
|
|
fi
|
|
|
|
# --- engine reference vs configured engine -----------------------------------
|
|
# /setup-engine is the only thing that rewrites CLAUDE.md's ENGINE-REFERENCE-IMPORT
|
|
# line. Edit engine.name in project.yaml by hand and the import keeps loading the
|
|
# previous engine's reference as project instructions -- every session, silently.
|
|
# An OBSERVATION, never an action (CLAUDE.md): it reports, the user decides.
|
|
if [ -f project.yaml ] && [ -f CLAUDE.md ]; then
|
|
ERI=$(grep -m1 -E '^@docs/engine-reference/[a-z]+/VERSION\.md' CLAUDE.md 2>/dev/null \
|
|
| sed -n 's|^@docs/engine-reference/\([a-z]*\)/VERSION\.md.*|\1|p')
|
|
CFG=$(sed -n 's/^[[:space:]]*name:[[:space:]]*"\{0,1\}\([A-Za-z]*\)"\{0,1\}[[:space:]]*$/\1/p' \
|
|
project.yaml 2>/dev/null | head -1 | tr '[:upper:]' '[:lower:]')
|
|
if [ -n "$ERI" ] && [ -n "$CFG" ] && [ "$ERI" != "$CFG" ]; then
|
|
echo ""
|
|
echo "!! ENGINE REFERENCE MISMATCH"
|
|
echo " project.yaml engine.name : $CFG"
|
|
echo " CLAUDE.md imports : docs/engine-reference/$ERI/VERSION.md"
|
|
echo " Every session is loading the $ERI reference on a $CFG project."
|
|
echo " Fix: re-run /setup-engine, or edit the ENGINE-REFERENCE-IMPORT line."
|
|
fi
|
|
fi
|
|
|
|
echo "==================================="
|
|
exit 0
|