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
+197 -18
View File
@@ -1,4 +1,32 @@
#!/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
# Hook: detect-gaps.sh
# Event: SessionStart
# Purpose: Detect missing documentation when code/prototypes exist
@@ -12,24 +40,78 @@ echo "=== Checking for Documentation Gaps ==="
# --- Check 0: Fresh project detection (suggests /start) ---
FRESH_PROJECT=true
# Check if engine is configured
if [ -f ".claude/docs/technical-preferences.md" ]; then
ENGINE_LINE=$(grep -E "^\- \*\*Engine\*\*:" .claude/docs/technical-preferences.md 2>/dev/null)
# Check if engine is configured (project.yaml first, fall back to technical-preferences.md)
if [ -f "project.yaml" ] && [ -f ".claude/hooks/yaml-helper.sh" ]; then
source .claude/hooks/yaml-helper.sh
ENGINE_NAME=$(get_yaml_key project.yaml engine.name 2>/dev/null)
if [ -n "$ENGINE_NAME" ]; then
FRESH_PROJECT=false
fi
fi
if [ "$FRESH_PROJECT" = true ] && [ -f ".claude/docs/technical-preferences.md" ]; then
ENGINE_LINE=$(grep -E "^[[:space:]]*-[[:space:]]+\*\*Engine\*\*:" .claude/docs/technical-preferences.md 2>/dev/null)
if [ -n "$ENGINE_LINE" ] && ! echo "$ENGINE_LINE" | grep -q "TO BE CONFIGURED" 2>/dev/null; then
FRESH_PROJECT=false
fi
fi
# Check if game concept exists
if [ -f "design/gdd/game-concept.md" ]; then
# Check if a design record exists.
#
# TWO FILES, NOT ONE. `game-concept.md` is the `standard`/`full` concept doc;
# at `minimal` the design record is the one-page `design/game-brief.md`, which
# `/brainstorm` writes in its place. workflow-modes.md states the rule this
# check has to satisfy: any check, gate or glob for the minimal design artifact
# must target `design/game-brief.md`.
#
# Testing only for `game-concept.md` means a minimal-tier project that has
# already run `/start` AND `/brainstorm` is greeted with "NEW PROJECT ... Run:
# /start" at every session start -- the banner wrong about the one artifact
# that tier produces.
if [ -f "design/gdd/game-concept.md" ] || [ -f "design/game-brief.md" ]; then
FRESH_PROJECT=false
fi
# Check if source code exists
if [ -d "src" ]; then
SRC_CHECK=$(find src -type f \( -name "*.gd" -o -name "*.cs" -o -name "*.cpp" -o -name "*.c" -o -name "*.h" -o -name "*.hpp" -o -name "*.rs" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \) 2>/dev/null | head -1)
# Check if source code exists.
#
# All three code roots, not just `src/`. Per .claude/docs/directory-structure.md
# `src/` is the Godot row; Unity builds from `Assets/` and Unreal from
# `Source/`. Testing only `src/` meant a Unity project with a full codebase
# could still be greeted with "NEW PROJECT ... Run: /start".
#
# Tested directly rather than through resolve_code_root: this check runs before
# the config helper is guaranteed to be sourced, and "is there any code at all"
# does not need the engine to be known.
for _root in src Assets Source; do
[ -d "$_root" ] || continue
SRC_CHECK=$(find "$_root" -type f \( -name "*.gd" -o -name "*.cs" -o -name "*.cpp" -o -name "*.c" -o -name "*.h" -o -name "*.hpp" -o -name "*.rs" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \) 2>/dev/null | head -1)
if [ -n "$SRC_CHECK" ]; then
FRESH_PROJECT=false
break
fi
done
# --- Check 0b: v1.0 project — legacy config present, project.yaml absent ---
# The migration story depends on users discovering /adopt; UPGRADING.md only
# reaches people who read it. An unconfigured technical-preferences.md template
# does not count as legacy data (same rule as migrate-v1-config.sh), and legacy
# config also means the project is not fresh — the NEW PROJECT banner below
# would be wrong for it.
if [ ! -f "project.yaml" ]; then
LEGACY_FILES=""
[ -f "production/stage.txt" ] && LEGACY_FILES="production/stage.txt"
[ -f "production/review-mode.txt" ] && LEGACY_FILES="${LEGACY_FILES:+$LEGACY_FILES, }production/review-mode.txt"
if [ -f ".claude/docs/technical-preferences.md" ]; then
TP_ENGINE=$(grep -E "^[[:space:]]*-[[:space:]]+\*\*Engine\*\*:" .claude/docs/technical-preferences.md 2>/dev/null)
if [ -n "$TP_ENGINE" ] && ! echo "$TP_ENGINE" | grep -q "TO BE CONFIGURED" 2>/dev/null; then
LEGACY_FILES="${LEGACY_FILES:+$LEGACY_FILES, }.claude/docs/technical-preferences.md"
fi
fi
if [ -n "$LEGACY_FILES" ]; then
FRESH_PROJECT=false
echo ""
echo "⬆️ v1.0 PROJECT: legacy config ($LEGACY_FILES) but no project.yaml."
echo " Run: /adopt — it detects this and drives the migration (non-destructive)."
echo " Preview first: bash .claude/scripts/migrate-v1-config.sh --dry-run"
fi
fi
@@ -43,10 +125,75 @@ if [ "$FRESH_PROJECT" = true ]; then
exit 0
fi
# --- Resolve the workflow tier once, for the checks below ---
#
# Checks 1, 3 and 4 all ask "where is the design/architecture document for this
# code?". At `minimal` the answer is that there deliberately is none:
# workflow-modes.md requires only engine choice and a filled
# `design/game-brief.md` before code starts, and says everything else can be
# skipped. Reporting their absence as a GAP nags the user about work their own
# configuration told them to skip -- and a warning that fires when nothing is
# wrong trains the user to ignore the ones that matter.
#
# resolve_setting, NOT get_yaml_key: `modes.workflow` is rigor-fronted, so a
# project that set `rigor: minimal` and nothing else has no `modes.workflow` key
# to read. Only the full chain sees the expansion. Same pattern and same reason
# as validate-commit.sh.
#
# Resolved AFTER the fresh-project early-exit above, so a brand-new project
# never pays for the lookup.
WORKFLOW="standard"
if [ -f .claude/hooks/yaml-helper.sh ]; then
. .claude/hooks/yaml-helper.sh 2>/dev/null
W=$(resolve_setting modes.workflow 2>/dev/null | cut -f1)
[ -n "$W" ] && WORKFLOW="$W"
fi
# Check 2 is NOT gated: an undocumented prototype is a gap about the prototype
# itself, not about the design pipeline the tier scales.
if [ "$WORKFLOW" = "minimal" ]; then
DOC_CHECKS=false
else
DOC_CHECKS=true
fi
# --- Resolve the engine-specific code root for checks 1, 3 and 4 ------------
#
# Checks 1, 3 and 4 were all hardcoded to `src/`, which .claude/docs/directory-
# structure.md defines as the GODOT row of the code-root table -- Unity uses
# `Assets/`, Unreal `Source/`. On those two engines every directory test below
# failed, the source-file count came back 0, and NONE of the three checks ran.
# A project with 200 undocumented Unity systems produced the same empty output
# as a fully documented one.
#
# Only the ROOT is engine-specific. The `core/`, `engine/` and `gameplay/`
# sub-layout is CCGS's own convention, unchanged by the engine, so it stays
# literal.
#
# Resolved AFTER the fresh-project early-exit, so a brand-new project never
# pays for the lookup.
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
# A doc check that cannot locate the code root has not established that there
# are no gaps -- it has established nothing (obligation 1 and 3 of
# .claude/rules/skill-authoring.md). Announce it, once, and only when the checks
# would otherwise have had somewhere to look.
if [ "$DOC_CHECKS" = true ] && [ -z "$CODE_ROOT" ]; then
if [ -d src ] || [ -d Assets ] || [ -d Source ]; then
echo "ℹ️ NOT CHECKED: code exists but the code root is ambiguous (engine.name unset, and the tree does not name one unambiguously)."
echo " Checks 1, 3 and 4 (design and architecture docs for existing code) did NOT run."
echo " Suggested action: /setup-engine, or set engine.name in project.yaml"
fi
fi
# --- Check 1: Substantial codebase but sparse design docs ---
if [ -d "src" ]; then
if [ -n "$CODE_ROOT" ] && [ -d "$CODE_ROOT" ]; then
# Count source files (cross-platform, handles Windows paths)
SRC_FILES=$(find src -type f \( -name "*.gd" -o -name "*.cs" -o -name "*.cpp" -o -name "*.c" -o -name "*.h" -o -name "*.hpp" -o -name "*.rs" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \) 2>/dev/null | wc -l)
SRC_FILES=$(find "$CODE_ROOT" -type f \( -name "*.gd" -o -name "*.cs" -o -name "*.cpp" -o -name "*.c" -o -name "*.h" -o -name "*.hpp" -o -name "*.rs" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \) 2>/dev/null | wc -l)
else
SRC_FILES=0
fi
@@ -61,9 +208,9 @@ fi
SRC_FILES=$(echo "$SRC_FILES" | tr -d ' ')
DESIGN_FILES=$(echo "$DESIGN_FILES" | tr -d ' ')
if [ "$SRC_FILES" -gt 50 ] && [ "$DESIGN_FILES" -lt 5 ]; then
if [ "$DOC_CHECKS" = true ] && [ "$SRC_FILES" -gt 50 ] && [ "$DESIGN_FILES" -lt 5 ]; then
echo "⚠️ GAP: Substantial codebase ($SRC_FILES source files) but sparse design docs ($DESIGN_FILES files)"
echo " Suggested action: /reverse-document design src/[system]"
echo " Suggested action: /reverse-document design ${CODE_ROOT:-<code root>}/[system]"
echo " Or run: /project-stage-detect to get full analysis"
fi
@@ -95,7 +242,7 @@ if [ -d "prototypes" ]; then
fi
# --- Check 3: Core systems without architecture docs ---
if [ -d "src/core" ] || [ -d "src/engine" ]; then
if [ "$DOC_CHECKS" = true ] && [ -n "$CODE_ROOT" ] && { [ -d "$CODE_ROOT/core" ] || [ -d "$CODE_ROOT/engine" ]; }; then
if [ ! -d "docs/architecture" ]; then
echo "⚠️ GAP: Core engine/systems exist but no docs/architecture/ directory"
echo " Suggested action: Create docs/architecture/ and run /architecture-decision"
@@ -105,15 +252,15 @@ if [ -d "src/core" ] || [ -d "src/engine" ]; then
if [ "$ADR_COUNT" -lt 3 ]; then
echo "⚠️ GAP: Core systems exist but only $ADR_COUNT ADR(s) documented"
echo " Suggested action: /reverse-document architecture src/core/[system]"
echo " Suggested action: /reverse-document architecture $CODE_ROOT/core/[system]"
fi
fi
fi
# --- Check 4: Gameplay systems without design docs ---
if [ -d "src/gameplay" ]; then
if [ "$DOC_CHECKS" = true ] && [ -n "$CODE_ROOT" ] && [ -d "$CODE_ROOT/gameplay" ]; then
# Find major gameplay subdirectories (those with 5+ files)
GAMEPLAY_SYSTEMS=$(find src/gameplay -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
GAMEPLAY_SYSTEMS=$(find "$CODE_ROOT/gameplay" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
if [ -n "$GAMEPLAY_SYSTEMS" ]; then
while IFS= read -r system_dir; do
@@ -129,9 +276,9 @@ if [ -d "src/gameplay" ]; then
design_doc_2="design/gdd/${system_name}.md"
if [ ! -f "$design_doc_1" ] && [ ! -f "$design_doc_2" ]; then
echo "⚠️ GAP: Gameplay system 'src/gameplay/$system_name/' ($file_count files) has no design doc"
echo "⚠️ GAP: Gameplay system '$CODE_ROOT/gameplay/$system_name/' ($file_count files) has no design doc"
echo " Expected: design/gdd/${system_name}-system.md or design/gdd/${system_name}.md"
echo " Suggested action: /reverse-document design src/gameplay/$system_name"
echo " Suggested action: /reverse-document design $CODE_ROOT/gameplay/$system_name"
fi
fi
done <<< "$GAMEPLAY_SYSTEMS"
@@ -147,6 +294,38 @@ if [ "$SRC_FILES" -gt 100 ]; then
fi
fi
# --- Check 6: project.stage is behind what is actually on disk ---
#
# On the `minimal` path nothing ever advances the stage. Only
# /gate-check writes it, and the four-step jam route never invokes one -- so a
# project with stories and running game code keeps reporting `Concept` to the
# status line, to /help, and to every gate checklist that branches on stage.
#
# This is an OBSERVATION, never a write. CLAUDE.md is explicit that the stage
# advances on a /gate-check PASS with the user confirming, so a hook that
# advanced it silently would be the worse bug. Say what is inconsistent and name
# the skill that resolves it.
STAGE=""
if [ -f .claude/hooks/yaml-helper.sh ]; then
STAGE=$(get_yaml_key project.stage 2>/dev/null)
fi
[ -z "$STAGE" ] && [ -f production/stage.txt ] && STAGE=$(head -1 production/stage.txt 2>/dev/null | tr -d '
' | tr -d ' ')
if [ -n "$STAGE" ]; then
STORY_COUNT=$(find production/epics -name "story-*.md" 2>/dev/null | wc -l | tr -d ' ')
case "$STAGE" in
Concept|concept|Pre-Production|"Pre-Production")
if [ "$STORY_COUNT" -gt 0 ] && [ "$SRC_FILES" -gt 0 ]; then
echo "⚠️ GAP: project.stage says '$STAGE', but $STORY_COUNT stories and $SRC_FILES source files exist"
echo " The stage only advances on a /gate-check PASS, and nothing on the"
echo " minimal path runs one -- so the status line can sit at '$STAGE' indefinitely."
echo " Suggested action: /gate-check (it asks before advancing; this hook never writes the stage)"
fi
;;
esac
fi
# --- Summary ---
echo ""
echo "💡 To get a comprehensive project analysis, run: /project-stage-detect"