T-KDJ6-per-commit-typecheck-before-push
Status: closed/superseded · Impact: medium · Complexity: small
AUTO-DEFINED: this spec was best-effort machine-authored by /sdlc:task-auto-define on 2026-08-03, driven by /sdlc:spawn-task-pr —drive-to-ready. Review the Goal, Approach, Today, Files-to-touch, and Acceptance-criteria carefully before trusting it.
/sdlc:task-work gates the PR on the branch tip only, so an intermediate
commit can fail typecheck while the tip passes — a bisect-hostile hole that
today only human inspection catches. Close it by sweeping a declared subset of
the project’s quality verbs over each commit the branch adds, before the push.
Splitting a branch into per-acceptance-criterion commits left an intermediate commit that did not typecheck — a barrel re-export landed one commit before its definition. It was caught by human inspection, not by tooling. A per-commit typecheck sweep over the branch’s commits, run before push, would catch this class mechanically.
| Location | Role today |
|---|---|
apps/sdlc/skills/task-work/SKILL.md | Step 7 runs sdlc quality run against the worktree’s tip tree; Step 9 rebases and re-runs that same tip-only gate; Step 10 pushes. No step evaluates an intermediate commit. |
apps/sdlc/lib/services/quality/ops/run.ts | The sdlc quality run op — reads the quality_checks: verb list and runs each verb once, in one working tree. |
apps/sdlc/skills/task-work/check_ancestry.ts | The only per-commit walk task-work performs before pushing; it classifies commit subjects for contamination and never executes a check at a commit. |
apps/sdlc/lib/config/load.ts#SdlcConfigSchema | Zod schema for sdlc.yaml. Declares quality_checks, worktree_init, commit_worktree_init; no key names a per-commit subset. |
apps/sdlc/lib/config/sdlc_yaml.ts#SCHEMA_OWNED_KEYS | The set of verb-list keys loadVerbList schema-validates before reading. |
apps/sdlc/lib/services/git/arm-worktree.ts#armCommitWorktree | Existing primitive that arms an ephemeral detached worktree by running the commit_worktree_init: verbs — the reusable “check out a commit and make it runnable” step. |
apps/sdlc/conventions/sdlc-yaml.md | Reference documentation for each sdlc.yaml key. |
sdlc.yaml | This repo’s config. quality_checks: already lists bunx tsc --noEmit; nothing selects a per-commit subset. |
Proposed
Section titled “Proposed”sdlc quality sweep-commits is a new deterministic op that replays a declared
subset of quality verbs at each commit a branch adds, and /sdlc:task-work
Step 9 runs it after the ancestry rebase and before Step 10’s push, gating the
push on its exit code. The subset is declared per project as a new
per_commit_checks: key in sdlc.yaml; when that key is absent or empty the
op reports a skip and exits 0, so projects that have not opted in are
unaffected. In this repo the subset is bunx tsc --noEmit, so the
barrel-re-export-before-definition failure that motivated this task fails the
sweep at the offending SHA instead of surviving to review.
Approach
Section titled “Approach”- Add
per_commit_checkstoSdlcConfigSchemainapps/sdlc/lib/config/load.tsas an optional list of non-empty shell-verb strings, and toSCHEMA_OWNED_KEYSinapps/sdlc/lib/config/sdlc_yaml.tssoloadVerbListschema-validates it. Absent, null, and empty all mean “sweep nothing”. - Add the op module
apps/sdlc/lib/services/quality/ops/sweep-commits.ts(auto-registered by theservices/<noun>/ops/<verb>.tsdirectory convention, giving the verbsdlc quality sweep-commits). Flags:--base <ref>defaulting toorigin/main,--config <path>defaulting to the project root’ssdlc.yaml, and the standard--output text|json. - In the op, load the verb list first. When it is empty, print
SWEEP-COMMITS-SKIPPED reason=no-per-commit-checksand exit 0 without touching git. - Otherwise enumerate the sweep range with
git rev-list --reverse <base>..HEADthrough the command seam (apps/sdlc/lib/util/command.ts), then drop the last entry — the tip tree is already covered by Step 7’ssdlc quality rungate, so re-running it here would only double the cost. - For each remaining SHA, in order:
git worktree add --detachinto a temporary directory at that SHA, arm it witharmCommitWorktree, run each declared verb in that directory viarunChecked, then remove the worktree withgit worktree remove --forcein afinallyso a thrown verb still cleans up. Stop at the first failing pair and printSWEEP-COMMITS-FAIL sha=<sha> verb=<verb>plus the verb’s captured output, exit 1. When the range is exhausted clean, printSWEEP-COMMITS-OK commits=<n> verbs=<m>and exit 0. - Add a sub-step to Step 9 of
apps/sdlc/skills/task-work/SKILL.md, placed after the ancestry rebase and the quality re-run and before the “ready to push” sub-step, that invokes the op un-piped and states that a non-zero exit blocks the push until the operator amends or reorders the offending commit. - Document
per_commit_checks:inapps/sdlc/conventions/sdlc-yaml.mdalongside the other verb-list keys, naming the skip-when-empty default and the cost tradeoff that keeps the list a strict subset ofquality_checks:. - Declare
per_commit_checks:in this repo’ssdlc.yamlwith the single verbbunx tsc --noEmit. - Extend
apps/sdlc/lib/services/quality/tests/quality_ops.test.tswith coverage for the three outcomes (skip, clean sweep, failing middle commit).
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/services/quality/ops/sweep-commits.ts | new | The quality sweep-commits op: range enumeration, per-commit ephemeral worktree, verb execution, first-failure reporting. |
apps/sdlc/lib/config/load.ts | modify | Add the optional per_commit_checks verb-list field to SdlcConfigSchema. |
apps/sdlc/lib/config/sdlc_yaml.ts | modify | Add per_commit_checks to SCHEMA_OWNED_KEYS so the key is schema-validated on read. |
apps/sdlc/skills/task-work/SKILL.md | modify | Step 9 gains the sweep sub-step; a non-zero exit blocks Step 10’s push. |
apps/sdlc/conventions/sdlc-yaml.md | modify | Document the per_commit_checks: key and its skip-when-empty default. |
sdlc.yaml | modify | Declare per_commit_checks: with bunx tsc --noEmit. |
apps/sdlc/lib/services/quality/tests/quality_ops.test.ts | modify | Cover skip, clean sweep, and failing-middle-commit outcomes. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: With
per_commit_checks:declared,sdlc quality sweep-commits --base origin/mainexits non-zero and printsSWEEP-COMMITS-FAIL sha=<sha> verb=<verb>naming the first commit ingit rev-list --reverse origin/main..HEAD~1that fails a declared verb, and exits 0 printingSWEEP-COMMITS-OK commits=<n> verbs=<m>when none fail. - AC-2: With
per_commit_checks:absent or empty, the op exits 0 printingSWEEP-COMMITS-SKIPPED reason=no-per-commit-checks, andgit worktree listreports the same entries before and after the run. - AC-3:
apps/sdlc/skills/task-work/SKILL.mdStep 9 invokessdlc quality sweep-commitsafter the ancestry rebase and before the push sub-step, and states that a non-zero exit blocks the push. - AC-4: A regression test in
apps/sdlc/lib/services/quality/tests/quality_ops.test.tsbuilds a throwaway repo whose middle commit fails a declared verb, and asserts the op’s stdout names that commit’s SHA. - AC-5:
bunx tsc --noEmitandbun test apps/sdlcboth pass on the branch tip.
Out of scope
Section titled “Out of scope”- A CI-side sweep on the PR head. This task gates locally, before push; a GitHub Actions equivalent is separate work.
- Running the whole
quality_checks:list at each commit. Only the declaredper_commit_checks:subset is swept, so the sweep stays cheap enough to run on every push. - Auto-amending, auto-squashing, or auto-reordering a commit the sweep rejects — the operator decides the fix.
- The response-commit path in
/sdlc:pr-respond; wiring the sweep there is a follow-up once the op exists.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-08-03 UTC from
T-5G81-lease-payload-gates in git@github.com:sksizer/dev.git.