T-W6NY-apps-imports-check-scopes-to-staged
Status: planning/proposed · Impact: medium · Complexity: small
AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-07-19 because the task is autonomy: autonomous/pr. Review the Goal, Approach, Today, Files-to-touch, and Acceptance-criteria carefully before trusting it.
The project-check-apps-imports pre-commit gate re-scans every app in the
monorepo on every commit, making commits slow enough to time out a foreground
commit at the 2-minute tool limit. This task scopes that check to the staged
paths so it only re-checks changed files, closing a friction gap surfaced by
T-NLKF-converge-obsidian-bases in https://github.com/sksizer/dev.
The pre-commit hook is slow (~2-3 min) because
.claude/skills/project-check/check_apps_imports.ts(wired asproject-check-apps-importsin the rootlefthook.yml) scans all monorepo apps on every commit, timing out a foreground commit at the 2-min tool limit during T-NLKF. Scope the apps-imports check to staged paths so it only re-checks changed files instead of the whole monorepo. Sibling: open/ready T-SGIN does the same staged-scoping for the check_entities pre-commit gate; follow the same pattern here.
| Location | Role today |
|---|---|
.claude/skills/project-check/check_apps_imports.ts | S-0008 apps-import-discipline checker; wired as the project-check-apps-imports pre-commit gate |
.claude/skills/project-check/check_apps_imports.ts#main | Collects EVERY source file under apps/** + packages/** from the working tree (via walkSourceFiles) and scans each with scanOneFile — re-walks the whole monorepo on every invocation, with no staged scoping |
.claude/skills/project-check/check_apps_imports.ts#parseArgs | Parses only --root; ParsedArgs carries a single root field |
lefthook.yml | Wires project-check-apps-imports to run .claude/skills/project-check/check_apps_imports.ts (via bun run) with no staged scoping, so every commit re-scans the whole tree |
apps/sdlc/lib/services/docs/generate.ts#checkStaged | Repo precedent for index-scoped gating: materializes the git index into a temp tree via git checkout-index and runs the unchanged check against it, leaving the working tree untouched |
.claude/skills/project-check/tests/check_apps_imports.test.ts | Fixture-driven harness shelling the checker with --root <fixture>; no staged-mode coverage |
Proposed
Section titled “Proposed”Teach check_apps_imports.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 under apps/** or packages/** is a source
file, and otherwise scans only the staged copies of those files (materialized
from the index) instead of re-walking the whole monorepo — so a commit no
longer pays to re-scan every app, and unstaged WIP edits cannot fail an
unrelated commit. Bare invocations — CI, direct runs, and the fixture-driven
tests — keep today’s --root working-tree behavior.
Approach
Section titled “Approach”- Add a
--stagedboolean flag toparseArgsin.claude/skills/project-check/check_apps_imports.ts(default off;ParsedArgsgains astagedfield). Without the flag, behavior is unchanged — the existing--rootworking-tree walk still runs. - In
main, whenstagedis set, resolve the git repo governingrootviagit rev-parse --show-toplevelrun with-Cat the resolved root. If root is not inside a git repo, print an error naming the conflict and exit 2 (the existing script-failure exit class). - Probe the index with
git diff --cached --name-only -z -- apps packagesand keep only paths whose extension is inSOURCE_EXTS, applying the sameEXCLUDE_PREFIXES/REINCLUDE_PREFIXESscoping the working-tree walk uses. Empty result means nothing relevant is staged: print a one-line skip note and exit 0 without reading any file. - Otherwise materialize the staged copies of those files into a
mkdtempSynctemp dir viagit checkout-index -z --stdin -fwith--prefixpointing at the temp dir (feeding the staged path list), then run the existingscanOneFileover each materialized file withrootset to the temp dir. Rebase reported paths onto the real root 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. - Point the
project-check-apps-importscommand inlefthook.ymlatbun run .claude/skills/project-check/check_apps_imports.ts --staged, with a comment carrying the index-vs-working-tree rationale (mirror theproject-check-docs-driftcomment). - Extend
.claude/skills/project-check/tests/check_apps_imports.test.tswith staged-mode tests that build a throwaway git repo in a temp dir around a copied fixture: (a) a forbidden import unstaged with nothing staged underapps/**/packages/**exits 0 via the skip path; (b) a compliant file staged while a forbidden-import edit sits unstaged exits 0; (c) the forbidden import staged exits 1 with the usual S-0008 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_apps_imports.ts | modify | Add --staged flag: resolve the repo, fast-skip when no staged source file falls under apps/** / packages/**, else materialize the staged files (git checkout-index) and run the existing scanOneFile over them |
lefthook.yml | modify | Run project-check-apps-imports with --staged plus an index-vs-working-tree rationale comment |
.claude/skills/project-check/tests/check_apps_imports.test.ts | modify | Add staged-mode cases: skip-fast, staged-clean vs unstaged-leak, staged-leak fails, non-repo root errors |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: With
--stagedand no staged source file underapps/**orpackages/**, the checker exits 0 with a skip note even when a working-tree file carries a forbidden import that fails the bare--rootrun. - AC-2: With
--stagedand a forbidden-import-free staged copy of anapps/**file whose working-tree copy carries an unstaged forbidden import, the checker exits 0; staging the forbidden import instead makes it exit 1 with the same S-0008 file citation the working-tree mode prints. - AC-3: Without
--stagedthe working-tree behavior is unchanged: the existing cases in.claude/skills/project-check/tests/check_apps_imports.test.tspass unmodified. - AC-4: The
project-check-apps-importscommand inlefthook.ymlinvokescheck_apps_imports.tswith--staged. - AC-5:
--stagedagainst a--rootoutside any git repo exits 2 with a message naming the problem. - AC-6: running
.claude/skills/project-check/tests/check_apps_imports.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, entities, pipe-tail, no-legacy-script-paths, util-redeclarations, op-modules, skill-docs) to index gating — same failure class, separate follow-ups (check_entitiesis the sibling T-SGIN). - Repointing
check_apps_imports.tsat thesdlcCLI as a gate op (the T-WC1T repointing pattern) — the script stays a thin project-check adapter here. - Any change to the S-0008 detection itself (forbidden trees,
@lib/alias handling, specifier extraction) or to the existing fixtures’ semantics.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-07-19 UTC from T-NLKF-converge-obsidian-bases in https://github.com/sksizer/dev.