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
+137
View File
@@ -0,0 +1,137 @@
#!/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 write the WRONG TREE -- 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 InstructionsLoaded hook: record WHICH instruction file loaded and
# WHY.
#
# WHAT THIS ANSWERS
# `.claude/rules/*.md` files carry `paths:` frontmatter, and a path-scoped rule
# loads only when Claude READS a file matching the pattern -- not on every tool
# use, and not when it CREATES one. This framework's agents predominantly create
# files: a new GDD, a new source file, a new test. So "does the rule for
# src/gameplay/** ever actually reach the model?" has a real answer, and before
# this hook the only way to get it was to infer it from behaviour afterwards.
#
# That inference was made once here, expensively: a data-file rule specified one
# key casing, agents wrote the other, and nothing surfaced the disagreement.
# Measurement replaces the inference.
#
# MEASURED, same repo, one session:
# session_start CLAUDE.md
# include its four @-imports, each carrying parent_file_path
# path_glob_match .claude/rules/skill-authoring.md, after READING a file
# under .claude/agents/** -- one of its declared paths
# (nothing) after CREATING a file under prototypes/** -- a declared
# path of .claude/rules/prototype-code.md
# Read fires, create does not. Keep this hook wired: the behaviour is upstream
# and can change in either direction, and this is the only thing here that would
# notice.
#
# SCHEMA CONFIRMED, NOT ASSUMED
# The common fields are documented; the event-specific ones are not. `pick()`
# tries the confirmed name first and keeps alternates behind it, so a schema
# change degrades to a fallback rather than to a blank column -- and the RAW
# line carries the answer regardless. That is how `load_reason` was found: the
# first guess (`reason`) missed, and the raw payload named the real field.
#
# NOT GATED ON features.session_state, deliberately. This is a diagnostic wired
# up on purpose; silencing it from an unrelated flag would make it do nothing
# while still looking installed -- the failure shape
# `.claude/rules/skill-authoring.md` exists to prevent.
#
# The exit code is ignored for this event and it cannot block a load. Fail
# silent, stay cheap: it fires once per instruction file per load.
LOG_DIR="production/session-logs"
LOG="$LOG_DIR/instructions-loaded.log"
# stdin may be absent; an unbounded read would then hang until the hook timeout.
[ -t 0 ] && exit 0
if command -v timeout >/dev/null 2>&1; then
INPUT=$(timeout 2 cat 2>/dev/null)
else
INPUT=$(cat 2>/dev/null)
fi
[ -n "$INPUT" ] || exit 0
mkdir -p "$LOG_DIR" 2>/dev/null || exit 0
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# `head -1` on every grep: an unanchored match can hit the same key nested
# deeper in the payload, and a multi-line value would break the one-record-per-
# line format this log depends on.
pick() { # <json> <key>...
_j="$1"; shift
for _k in "$@"; do
_v=$(printf '%s' "$_j" \
| grep -oE "\"$_k\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
| head -1 \
| sed "s/\"$_k\"[[:space:]]*:[[:space:]]*\"//;s/\"$//")
if [ -n "$_v" ]; then printf '%s' "$_v"; return 0; fi
done
printf ''
}
FILE_VAL=$(pick "$INPUT" file_path path file source_path instruction_path filePath)
REASON_VAL=$(pick "$INPUT" load_reason reason loadReason matcher trigger source)
PARENT_VAL=$(pick "$INPUT" parent_file_path parentFilePath parent)
# Paths arrive as absolute Windows paths with JSON-escaped separators. Left raw,
# the readable line is the least readable thing in the log. Unescape, normalise
# to forward slashes, strip the repo prefix -- so the column reads
# `.claude/rules/gameplay-code.md`, which is what anyone scanning this is
# looking for. The RAW line below keeps the original bytes untouched.
# TWO forms of the repo root, because the payload and the shell disagree about
# what a path looks like on Windows. Git Bash reports $PWD as `/c/Users/...`
# while the payload carries `C:\Users\...`; stripping only the shell's form
# silently strips nothing and the column stays absolute -- which is why this
# needs checking on real payloads rather than a fixture. `pwd -W` gives the Windows
# form where it exists and is empty elsewhere, so both are tried and neither is
# required.
_PWD_FWD=$(printf '%s' "$PWD" | sed 's|\\|/|g')
_PWD_WIN=$(pwd -W 2>/dev/null | sed 's|\\|/|g')
shorten() {
[ -n "$1" ] || return 0
printf '%s' "$1" | sed 's|\\\\|/|g; s|\\|/|g' \
| sed "s|^${_PWD_FWD}/||" \
| { if [ -n "$_PWD_WIN" ]; then sed "s|^${_PWD_WIN}/||"; else cat; fi; }
}
FILE_SHORT=$(shorten "$FILE_VAL")
PARENT_SHORT=$(shorten "$PARENT_VAL")
{
printf '%s | %-16s | %s%s\n' \
"$TIMESTAMP" "${REASON_VAL:-?}" "${FILE_SHORT:-?}" \
"${PARENT_SHORT:+ (via $PARENT_SHORT)}"
# Raw payload on the following line, prefixed so the log stays greppable and
# so an unrecognised schema is still recoverable from the file itself.
printf '%s RAW %s\n' "$TIMESTAMP" "$INPUT"
} >> "$LOG" 2>/dev/null
exit 0