T-SGIN-check-entities-gates-staged-content
Status: open/ready · Impact: medium · Complexity: small
AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-07-18 because the task is autonomy: autonomous/pr. Review the Goal, Approach, Today, Files-to-touch, and Acceptance-criteria carefully before trusting it.
.claude/skills/project-check/check_entities.ts (wired as a pre-commit hook
in lefthook.yml) reads entity files from the working tree, so any
schema-inconsistent prose — for example a mid-edit definition.md — blocks
EVERY commit, even ones that do not stage that file. During
T-2KK8-capability-kind-grains-and-locations this forced the capability
definition rewrite ahead of the task’s intended step order:
check_entitiesgates the working tree, not the index — definition.md had to be schema-consistent before ANY commit could land, forcing the definition rewrite ahead of the task’s step order— T-2KK8-capability-kind-grains-and-locations post-mortem
Gate the staged (index) content instead — e.g. resolve entity prose via
git show :<path> for files with staged changes, or skip entity dirs with no
staged changes — so unstaged WIP prose cannot block unrelated commits.
| Location | Role today |
|---|---|
.claude/skills/project-check/check_entities.ts | Entity-prose consistency checker; main resolves --entities-dir (default apps/sdlc/lib/model/entities/) and reads every entity *.md from the working tree via readFileSync |
.claude/skills/project-check/check_entities.ts#checkOneEntity | Per-entity checks (schema load, backticked field and status-enum references in prose, body-only template) — all evaluated against working-tree file contents |
lefthook.yml | Wires the checker as the project-check-entities pre-commit command with no staged scoping, so unstaged WIP prose fails commits that never staged it |
apps/sdlc/lib/services/docs/generate.ts#checkStaged | Repo precedent for index-scoped gating: materializes the git index via git checkout-index into a temp tree so the docs-drift hook checks what the commit will actually contain |
.claude/skills/project-check/tests/check_entities.test.ts | Fixture-driven harness invoking the checker with --entities-dir; no staged-mode coverage |
Proposed
Section titled “Proposed”Teach check_entities.ts a --staged mode and switch the lefthook.yml
hook to it. In --staged mode the checker gates the git index: it exits 0
immediately when no staged path falls under the entities dir, and otherwise
runs the existing checks against the staged copies of the entity files, so
unstaged WIP prose (for example a mid-edit definition.md) can no longer
block an unrelated commit. Bare invocations — CI, direct runs, and the
fixture-driven tests — keep today’s working-tree behavior.
Approach
Section titled “Approach”- Add a
--stagedboolean flag toparseArgsin.claude/skills/project-check/check_entities.ts(default off;ParsedArgsgains astagedfield). Without the flag, behavior is unchanged. - In
main, whenstagedis set, resolve the git repo governing the entities dir viagit rev-parse --show-toplevelrun with-Cat the resolved entities dir. If the dir is not inside a git repo, print an error naming the conflict and exit 2 (the existing script-failure exit class). - Compute the entities dir’s repo-relative path and probe the index with
git diff --cached --name-onlylimited to that path. Empty output means nothing relevant is staged: print a one-line skip note and exit 0 without reading any prose. - Otherwise materialize the staged subtree into a
mkdtempSynctemp dir: feedgit ls-files -zfor the entities path intogit checkout-index -z --stdin -fwith--prefixpointing at the temp dir, then run the existing per-entity loop against the materialized dir. Rebase reported paths onto the real entities dir for readable citations and remove the temp dir in afinallyblock. This is the same no-stash, working-tree-untouched materialization pattern ascheckStagedinapps/sdlc/lib/services/docs/generate.ts. Note the registered types keep their in-memory Zod schema (code imports resolve from the working tree regardless); only the prose and any fixtureschema.jsoncome from the index. - Point the
project-check-entitiescommand inlefthook.ymlatbun run .claude/skills/project-check/check_entities.ts --staged, with a comment carrying the index-vs-working-tree rationale (mirror theproject-check-docs-driftcomment). - Extend
.claude/skills/project-check/tests/check_entities.test.tswith staged-mode tests that build a throwaway git repo in a temp dir around a copied fixture: (a) unstaged drift with nothing staged under the entities dir exits 0 via the skip path; (b) clean content staged while a drifted edit sits unstaged in the working tree exits 0; (c) drifted content staged exits 1 with the usual citation; (d)--stagedagainst a copied fixture outside any git repo exits 2.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
.claude/skills/project-check/check_entities.ts | modify | Add --staged flag: fast-skip when nothing is staged under the entities dir, else materialize the index subtree (git ls-files into git checkout-index) and run the existing checks against it |
lefthook.yml | modify | Run project-check-entities with --staged plus a rationale comment |
.claude/skills/project-check/tests/check_entities.test.ts | modify | Add staged-mode cases: skip-fast, staged-clean vs unstaged-drift, staged-drift fails, non-repo dir errors |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: With
--stagedand no staged changes under the entities dir, the checker exits 0 with a skip note even when an entity*.mdin the working tree carries drift that fails the bare working-tree run. - AC-2: With
--stagedand a drift-free staged copy of an entity file whose working-tree copy carries unstaged drift, the checker exits 0; staging the drifted content instead makes it exit 1 with the same file citation the working-tree mode prints. - AC-3: Without
--stagedthe working-tree behavior is unchanged: the existingCASEStable in.claude/skills/project-check/tests/check_entities.test.tspasses unmodified. - AC-4: The
project-check-entitiescommand inlefthook.ymlinvokescheck_entities.tswith--staged. - AC-5:
--stagedagainst an entities dir outside any git repo exits 2 with a message naming the problem. - AC-6: running
.claude/skills/project-check/tests/check_entities.test.tsunderbun testpasses, including new staged-mode tests covering AC-1, AC-2, and AC-5.
Out of scope
Section titled “Out of scope”- Converting the other working-tree project-check hooks wired in
lefthook.yml(identifier-shape, pipe-tail, apps-imports, util-redeclarations, op-modules, skill-docs) to index gating — same failure class, separate follow-ups. - Repointing
check_entities.tsat thesdlcCLI as a gate op (the T-WC1T repointing pattern) — the script stays a thin project-check adapter here. - Any change to the checks themselves (field references, status enums, body-only template, Operations-table shape) or to the semantics of the existing fixtures.
Dependencies
Section titled “Dependencies”- none —
gitandbunare already required by the pre-commit environment, and no other task blocks this.
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-07-18 UTC from T-2KK8-capability-kind-grains-and-locations in https://github.com/sksizer/dev.