mirror of
https://github.com/Donchitos/Claude-Code-Game-Studios.git
synced 2026-10-16 01:37:05 +00:00
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:
co-authored by
Claude Opus 5
parent
984023ddac
commit
d05699707f
@@ -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
|
||||
|
||||
# Claude Code PreToolUse hook: Validates git commit commands
|
||||
# Receives JSON on stdin with tool_input.command
|
||||
# Exit 0 = allow, Exit 2 = block (stderr shown to Claude)
|
||||
@@ -12,7 +40,7 @@ INPUT=$(cat)
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
||||
else
|
||||
COMMAND=$(echo "$INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/"command"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||
COMMAND=$(echo "$INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"command"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||
fi
|
||||
|
||||
# Only process git commit commands
|
||||
@@ -20,6 +48,14 @@ if ! echo "$COMMAND" | grep -qE '^git[[:space:]]+commit'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Resolve a Python interpreter ONCE for every batched check below. Both the
|
||||
# blocking JSON scan and the advisory design scan must not spawn one process per
|
||||
# item; they spawn one each, and share this lookup.
|
||||
_VC_PY=""
|
||||
for _c in python python3 py; do
|
||||
if command -v "$_c" >/dev/null 2>&1; then _VC_PY="$_c"; break; fi
|
||||
done
|
||||
|
||||
# Get staged files
|
||||
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
||||
if [ -z "$STAGED" ]; then
|
||||
@@ -28,69 +64,266 @@ fi
|
||||
|
||||
WARNINGS=""
|
||||
|
||||
# Check design documents for required sections
|
||||
DESIGN_FILES=$(echo "$STAGED" | grep -E '^design/gdd/')
|
||||
if [ -n "$DESIGN_FILES" ]; then
|
||||
while IFS= read -r file; do
|
||||
if [[ "$file" == *.md ]] && [ -f "$file" ]; then
|
||||
for section in "Overview" "Player Fantasy" "Detailed" "Formulas" "Edge Cases" "Dependencies" "Tuning Knobs" "Acceptance Criteria"; do
|
||||
if ! grep -qi "$section" "$file"; then
|
||||
WARNINGS="$WARNINGS\nDESIGN: $file missing required section: $section"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done <<< "$DESIGN_FILES"
|
||||
fi
|
||||
# Section findings are collected through a file, not a variable: the loops below
|
||||
# run inside pipelines, and a subshell cannot assign to WARNINGS in the parent.
|
||||
# The scratch file must actually be WRITABLE, and that has to be tested rather
|
||||
# than assumed. `mktemp` and the `${TMPDIR:-/tmp}` fallback beside it both aim
|
||||
# at /tmp, which does not exist or is not writable in several environments this
|
||||
# hook runs in -- notably sandboxed Windows shells. When the redirect failed,
|
||||
# `[ -s "$TMP_DESIGN" ]` was false, the design-section scan emitted nothing, and
|
||||
# the hook exited 0: a check that could not run, reporting clean.
|
||||
#
|
||||
# So: try mktemp, then TMPDIR, then the repo own .git directory (writable by
|
||||
# definition -- `git diff --cached` has already succeeded against it), and PROVE
|
||||
# each candidate by writing to it. If none works, TMP_DESIGN stays empty and the
|
||||
# scan announces that it did not run instead of skipping in silence.
|
||||
TMP_DESIGN=""
|
||||
for _cand in "$(mktemp 2>/dev/null)" "${TMPDIR:+$TMPDIR/ccgs-commit-$$}" \
|
||||
"$(git rev-parse --git-dir 2>/dev/null)/ccgs-commit-$$"; do
|
||||
[ -z "$_cand" ] && continue
|
||||
if : > "$_cand" 2>/dev/null; then TMP_DESIGN="$_cand"; break; fi
|
||||
done
|
||||
trap '[ -n "$TMP_DESIGN" ] && rm -f "$TMP_DESIGN"' EXIT
|
||||
|
||||
# Validate JSON data files -- block invalid JSON
|
||||
# ---------------------------------------------------------------------------
|
||||
# BLOCKING CHECK FIRST. Everything below this block is advisory:
|
||||
# it appends to $WARNINGS and the hook still exits 0. This block is the only one
|
||||
# that can exit 2, and exit 2 is the entire reason the hook is registered.
|
||||
#
|
||||
# Running it THIRD, after the design-section scan, and validating one file
|
||||
# per `python -m json.tool` invocation -- one interpreter spawn each, ~110ms on
|
||||
# Windows -- gets the hook killed by its 15s timeout on a large commit before
|
||||
# it reaches the corrupt file, so whether a bad file is caught depends on where
|
||||
# its NAME SORTED. Measured with 201 staged files and one corrupt:
|
||||
#
|
||||
# corrupt sorts FIRST -> 730ms, rc=2, BLOCKED emitted
|
||||
# corrupt sorts LAST -> 15217ms, rc=124, nothing emitted, commit not blocked
|
||||
#
|
||||
# Two independent fixes, because either alone is a mitigation rather than a
|
||||
# guarantee:
|
||||
# (a) ORDER. The blocking check runs before any advisory work, so a starved
|
||||
# hook has already done the part that matters.
|
||||
# (b) COST. One interpreter validates every staged JSON file, so the check is
|
||||
# constant in file count instead of linear. The one-subprocess-per-item
|
||||
# shape has caused this same starvation more than once; batching is the
|
||||
# only fix that holds.
|
||||
#
|
||||
# Making it merely faster is not enough -- the cost stays linear and the hook
|
||||
# starves again at a larger file count. Batch it, or it returns.
|
||||
# ---------------------------------------------------------------------------
|
||||
DATA_FILES=$(echo "$STAGED" | grep -E '^assets/data/.*\.json$')
|
||||
if [ -n "$DATA_FILES" ]; then
|
||||
# Find a working Python command
|
||||
PYTHON_CMD=""
|
||||
for cmd in python python3 py; do
|
||||
if command -v "$cmd" >/dev/null 2>&1; then
|
||||
PYTHON_CMD="$cmd"
|
||||
break
|
||||
PYTHON_CMD="$_VC_PY"
|
||||
if [ -n "$PYTHON_CMD" ]; then
|
||||
# One spawn, all files. The file list arrives on stdin so an unbounded
|
||||
# number of paths cannot overflow the argument limit.
|
||||
BAD_JSON=$(printf '%s\n' "$DATA_FILES" | "$PYTHON_CMD" -c '
|
||||
import json, sys, os
|
||||
bad = []
|
||||
for line in sys.stdin.read().splitlines():
|
||||
path = line.strip()
|
||||
if not path or not os.path.isfile(path):
|
||||
continue
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8-sig") as fh:
|
||||
json.load(fh)
|
||||
except Exception:
|
||||
bad.append(path)
|
||||
# Bytes, not text. Python text-mode stdout on Windows rewrites \n as \r\n,
|
||||
# putting a CR inside every path but the last. Invisible in a terminal and in
|
||||
# a line-by-line diff -- only a byte comparison against the pre-batch
|
||||
# implementation surfaced it. get_yaml_key writes bytes for the same reason.
|
||||
sys.stdout.buffer.write("\n".join(bad).encode("utf-8"))
|
||||
' 2>/dev/null)
|
||||
if [ -n "$BAD_JSON" ]; then
|
||||
# Every offender at once. The per-file loop reported only the first
|
||||
# and exited, so a commit with several bad files took several
|
||||
# round-trips to clean up.
|
||||
echo "$BAD_JSON" | while IFS= read -r bad; do
|
||||
[ -n "$bad" ] && echo "BLOCKED: $bad is not valid JSON" >&2
|
||||
done
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "WARNING: Cannot validate JSON (python not found)" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
while IFS= read -r file; do
|
||||
if [ -f "$file" ]; then
|
||||
if [ -n "$PYTHON_CMD" ]; then
|
||||
if ! "$PYTHON_CMD" -m json.tool "$file" > /dev/null 2>&1; then
|
||||
echo "BLOCKED: $file is not valid JSON" >&2
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo "WARNING: Cannot validate JSON (python not found): $file" >&2
|
||||
fi
|
||||
# Check design documents for the sections REQUIRED AT THIS PROJECT'S TIER.
|
||||
#
|
||||
# Demanding all 8 sections unconditionally would contradict
|
||||
# .claude/docs/coding-standards.md and workflow-modes.md: the required count is
|
||||
# a function of `modes.workflow` -- 8 at `full`, 5 (+ conditional Formulas) at
|
||||
# `standard`, and no GDD requirement at all at `minimal`. A jam project on the
|
||||
# recommended `minimal` tier was being warned about six sections its own
|
||||
# configuration says it does not need, which trains the user to ignore the hook.
|
||||
# Source the config helper ONCE, ahead of every consumer below. It was
|
||||
# previously sourced inside the design-doc branch, so the code scans further
|
||||
# down -- which now need resolve_code_root -- could not reach it on a commit
|
||||
# that staged no GDD.
|
||||
# Detect success by asking whether the function EXISTS, not by the source's exit
|
||||
# status. A sourced file returns the status of its last statement, which here is
|
||||
# incidental -- gating on it left _VC_HELPER at 0 on a perfectly good source, the
|
||||
# workflow tier silently stayed at its "standard" fallback, and the GDD section
|
||||
# check stopped scaling with the tier (caught by test LL.9).
|
||||
_VC_HELPER=0
|
||||
if [ -f .claude/hooks/yaml-helper.sh ]; then
|
||||
. .claude/hooks/yaml-helper.sh 2>/dev/null || true
|
||||
command -v resolve_setting >/dev/null 2>&1 && _VC_HELPER=1
|
||||
fi
|
||||
|
||||
DESIGN_FILES=$(echo "$STAGED" | grep -E '^design/gdd/')
|
||||
if [ -n "$DESIGN_FILES" ]; then
|
||||
WORKFLOW="standard"
|
||||
if [ "$_VC_HELPER" = 1 ]; then
|
||||
W=$(resolve_setting modes.workflow 2>/dev/null | cut -f1)
|
||||
[ -n "$W" ] && WORKFLOW="$W"
|
||||
fi
|
||||
|
||||
# "Detailed" (not "Detailed Rules") because the canonical heading has a
|
||||
# documented alias, "Detailed Design" -- workflow-modes.md says not to treat
|
||||
# one as missing when the other is present.
|
||||
case "$WORKFLOW" in
|
||||
minimal) REQUIRED="" ;;
|
||||
full) REQUIRED="Overview|Player Fantasy|Detailed|Formulas|Edge Cases|Dependencies|Tuning Knobs|Acceptance Criteria" ;;
|
||||
*) REQUIRED="Overview|Detailed|Edge Cases|Dependencies|Acceptance Criteria" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$REQUIRED" ]; then
|
||||
# One pass for every file x section pair.
|
||||
#
|
||||
# A nested shell loop -- for each staged design doc, `echo |
|
||||
# tr | while read` then one `grep -qi` per required section -- does not
|
||||
# scale. At
|
||||
# workflow=full that is 8 greps per file, and each is a process. 50
|
||||
# design docs measured 14644ms -- 97% of this hook's 15s budget, from
|
||||
# the ADVISORY half alone. 50 GDDs is an ordinary project, not a
|
||||
# stress case.
|
||||
#
|
||||
# Matching is unchanged on purpose: case-insensitive SUBSTRING anywhere
|
||||
# in the file, exactly what `grep -qi "$section"` did. It is looser than
|
||||
# a heading check, and tightening it here would silently change which
|
||||
# documents warn -- a behaviour change smuggled inside a performance
|
||||
# fix. If that wants tightening it should be its own change with its
|
||||
# own test.
|
||||
printf '%s\n' "$DESIGN_FILES" | "${_VC_PY:-python}" -c '
|
||||
import sys, os
|
||||
argv = sys.argv
|
||||
required = [x for x in argv[1].split("|") if x]
|
||||
workflow = argv[2]
|
||||
out = []
|
||||
for line in sys.stdin.read().splitlines():
|
||||
path = line.strip()
|
||||
if not path or not path.endswith(".md") or not os.path.isfile(path):
|
||||
continue
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
||||
body = fh.read().lower()
|
||||
except OSError:
|
||||
continue
|
||||
for section in required:
|
||||
if section.lower() not in body:
|
||||
out.append("DESIGN: %s missing section required at workflow=%s: %s"
|
||||
% (path, workflow, section))
|
||||
if out:
|
||||
# Bytes, not text -- see the CRLF note in the JSON scan above.
|
||||
sys.stdout.buffer.write(("\n".join(out) + "\n").encode("utf-8"))
|
||||
' "$REQUIRED" "$WORKFLOW" > "$TMP_DESIGN" 2>/dev/null
|
||||
if [ -z "$TMP_DESIGN" ]; then
|
||||
# Obligation 3 of .claude/rules/skill-authoring.md: a skipped step
|
||||
# announces itself. Silence here is indistinguishable from a GDD
|
||||
# that already has every section its tier requires.
|
||||
WARNINGS="$WARNINGS\nSKIPPED: no writable scratch location, so the GDD section check did NOT run on the staged design doc(s)."
|
||||
elif [ -s "$TMP_DESIGN" ]; then
|
||||
WARNINGS="$WARNINGS\n$(cat "$TMP_DESIGN")"
|
||||
fi
|
||||
done <<< "$DATA_FILES"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for hardcoded gameplay values in gameplay code
|
||||
# Uses grep -E (POSIX extended) instead of grep -P (Perl) for cross-platform compatibility
|
||||
CODE_FILES=$(echo "$STAGED" | grep -E '^src/gameplay/')
|
||||
#
|
||||
# Batched through xargs. Run one `grep` per staged file -- a process each,
|
||||
# ~60ms on Windows -- and 400 staged gameplay files blow a 15s budget on their
|
||||
# own. `xargs` packs as many
|
||||
# paths into each grep as ARG_MAX allows, so the spawn count is bounded by total
|
||||
# path length rather than by file count.
|
||||
#
|
||||
# `-l` not `-q`: with many files per invocation the exit status can no longer
|
||||
# say WHICH matched, and the warning names the file. It stays a test, not a
|
||||
# report -- `-l` prints only names, never matched lines, which is what the -q
|
||||
# comment here was guarding against.
|
||||
#
|
||||
# NUL-delimited, NOT `-d '\n'`: both `-d` and `-r` are GNU-only extensions.
|
||||
# BSD xargs (macOS) rejects `-d`, and because this pipeline ends in
|
||||
# `2>/dev/null || true` the rejection is SILENT -- the check returns empty and
|
||||
# reports nothing found, which is indistinguishable from a clean scan. README
|
||||
# claims these hooks run on macOS, so a silent no-op there is a false pass.
|
||||
# `-0` exists in both GNU and BSD xargs. `-r` is dropped, not replaced: both
|
||||
# callers below reach xargs only inside `if [ -n ... ]`, so the empty-input case
|
||||
# it guarded cannot occur. `|| true` stays -- grep exits 1 on no match, which is
|
||||
# the normal case and not an error.
|
||||
# --- resolve the code root before either scan below ------------------------
|
||||
#
|
||||
# Both scans used to be hardcoded to `^src/gameplay/` and `^src/`. Per
|
||||
# .claude/docs/directory-structure.md, `src/` is the GODOT row of the code-root
|
||||
# table -- Unity compiles only `Assets/`, Unreal builds only from `Source/`. So
|
||||
# on two of the three supported engines both greps matched nothing on every
|
||||
# commit, both checks silently did not run, and the hook exited 0. A scan that
|
||||
# cannot run and a scan that found nothing produced the identical artifact.
|
||||
#
|
||||
# The `/gameplay/` narrowing is dropped rather than translated: it describes a
|
||||
# Godot source layout and has no Unity or Unreal equivalent, so keeping it would
|
||||
# leave the check Godot-only by a second route. The hardcoded-value scan instead
|
||||
# widens to the whole code root and is bounded by source-code EXTENSION, which
|
||||
# is what actually keeps it precise -- without it, a Unity commit would drag
|
||||
# `.meta`, `.asset` and `.prefab` YAML (all full of `speed: 5`) into a scan
|
||||
# meant for code, and a hook that cries wolf gets muted rather than fixed.
|
||||
CODE_ROOT=""; CODE_ROOT_SRC="unset"
|
||||
if [ "$_VC_HELPER" = 1 ]; then
|
||||
_CR=$(resolve_code_root 2>/dev/null)
|
||||
CODE_ROOT=$(printf '%s' "$_CR" | cut -f1)
|
||||
CODE_ROOT_SRC=$(printf '%s' "$_CR" | cut -f2)
|
||||
fi
|
||||
|
||||
# Obligation 3 of .claude/rules/skill-authoring.md -- a skipped step announces
|
||||
# itself. Only fires when source code is actually being committed, so a
|
||||
# docs-only or config-only commit stays quiet and the notice keeps its meaning.
|
||||
if [ -z "$CODE_ROOT" ]; then
|
||||
if echo "$STAGED" | grep -qiE '\.(gd|cs|cpp|cc|hpp|h|c|py|js|ts|rs|java|kt|lua|gdshader|shader|hlsl)$'; then
|
||||
WARNINGS="$WARNINGS\nSKIPPED: source files are staged but no code root could be resolved (engine.name unset, and the tree does not name one unambiguously). The hardcoded-value and TODO-owner scans did NOT run. Run /setup-engine, or set engine.name in project.yaml."
|
||||
fi
|
||||
fi
|
||||
|
||||
CODE_FILES=""
|
||||
[ -n "$CODE_ROOT" ] && CODE_FILES=$(echo "$STAGED" \
|
||||
| grep -E "^${CODE_ROOT}/" \
|
||||
| grep -iE '\.(gd|cs|cpp|cc|hpp|h|c|py|js|ts|rs|java|kt|lua)$')
|
||||
if [ -n "$CODE_FILES" ]; then
|
||||
while IFS= read -r file; do
|
||||
if [ -f "$file" ]; then
|
||||
if grep -nE '(damage|health|speed|rate|chance|cost|duration)[[:space:]]*[:=][[:space:]]*[0-9]+' "$file" 2>/dev/null; then
|
||||
WARNINGS="$WARNINGS\nCODE: $file may contain hardcoded gameplay values. Use data files."
|
||||
fi
|
||||
fi
|
||||
done <<< "$CODE_FILES"
|
||||
HARDCODED=$(printf '%s\n' "$CODE_FILES" | tr '\n' '\0' \
|
||||
| xargs -0 grep -lE '(damage|health|speed|rate|chance|cost|duration)[[:space:]]*[:=][[:space:]]*[0-9]+' 2>/dev/null || true)
|
||||
if [ -n "$HARDCODED" ]; then
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] && WARNINGS="$WARNINGS\nCODE: $file may contain hardcoded gameplay values. Use data files."
|
||||
done <<< "$HARDCODED"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for TODO/FIXME without assignee -- uses grep -E instead of grep -P
|
||||
SRC_FILES=$(echo "$STAGED" | grep -E '^src/')
|
||||
# Same code root as the scan above. Not extension-bounded: an unowned TODO is
|
||||
# worth flagging in a shader or a build script too, and this scan greps for a
|
||||
# literal marker rather than a numeric pattern, so it does not misfire on
|
||||
# serialized asset YAML the way the hardcoded-value scan would.
|
||||
SRC_FILES=""
|
||||
[ -n "$CODE_ROOT" ] && SRC_FILES=$(echo "$STAGED" | grep -E "^${CODE_ROOT}/")
|
||||
if [ -n "$SRC_FILES" ]; then
|
||||
while IFS= read -r file; do
|
||||
if [ -f "$file" ]; then
|
||||
if grep -nE '(TODO|FIXME|HACK)[^(]' "$file" 2>/dev/null; then
|
||||
WARNINGS="$WARNINGS\nSTYLE: $file has TODO/FIXME without owner tag. Use TODO(name) format."
|
||||
fi
|
||||
fi
|
||||
done <<< "$SRC_FILES"
|
||||
UNOWNED=$(printf '%s\n' "$SRC_FILES" | tr '\n' '\0' \
|
||||
| xargs -0 grep -lE '(TODO|FIXME|HACK)[^(]' 2>/dev/null || true)
|
||||
if [ -n "$UNOWNED" ]; then
|
||||
while IFS= read -r file; do
|
||||
[ -n "$file" ] && WARNINGS="$WARNINGS\nSTYLE: $file has TODO/FIXME without owner tag. Use TODO(name) format."
|
||||
done <<< "$UNOWNED"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Print warnings (non-blocking) and allow commit
|
||||
|
||||
Reference in New Issue
Block a user