T-WLNF-scope-task-state-origin-to-branch
Status: closed/superseded · 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 repo-wide lefthook pre-commit check project-check-task-state-origin
(wired in the root lefthook.yml, running sdlc task lint-state-origin) fails
globally whenever any other live task/* branch carries a stray state-only
“record PR” commit, so it blocks commits on every branch at once — not just the
offending one. This follow-up, spawned from T-NLKF-converge-obsidian-bases
in sksizer/dev, scopes the check to the branch actually
being committed (or to HEAD’s ancestry) so one session’s stray commit no longer
blocks unrelated work; alternatively it makes task-work’s append_pr_url land
the prs: commit reliably on main so the drift never appears.
The repo-wide lefthook check
project-check-task-state-origin(wired in the rootlefthook.yml, runningsdlc task lint-state-origin) fails globally when any other livetask/*branch carries a stray state-only ‘record PR’ commit, blocking commits on EVERY branch — worked around during T-NLKF byLEFTHOOK_EXCLUDE=project-check-task-state-origin. Scope the check to the branch being committed (or to HEAD’s ancestry) rather than failing on another live session’s branch; alternatively make task-work Step 10.4’sappend_pr_urlreliably land theprs:commit on main, never on a task branch. Sibling context: T-8NFT (start_task.ts stranding on a pre-existing-drift pre-commit block) and closed/done T-SIHV (task-state commits land on main, not the worktree branch).
| Location | Role today |
|---|---|
apps/sdlc/lib/model/entities/task/ops/_lint_state_origin_core.ts#listTaskBranches | Returns every local task/* branch via git for-each-ref, with no filter for which branch is actually being committed. |
apps/sdlc/lib/model/entities/task/ops/_lint_state_origin_core.ts#lint | Walks the commits of every branch listTaskBranches returns and flags each state-only commit, so a stray commit on any sibling task/* branch fails the whole run. |
apps/sdlc/lib/model/entities/task/ops/lint-state-origin.ts#input | Op input schema (projectRoot, baseRef); carries no branch-scope field, so the CLI/lefthook invocation can only run the check repo-wide. |
lefthook.yml | Pre-commit gate project-check-task-state-origin runs sdlc task lint-state-origin with no branch argument, so every commit is gated on all live task/* branches at once. |
Proposed
Section titled “Proposed”Scope the lint to the branch being committed: resolve the current HEAD
branch and check only its commits, so a stray state-only commit on a sibling
task/* branch no longer blocks unrelated work. The alternative named in the
Goal — hardening append_pr_url so the prs: commit always lands on main —
is deferred (see Out of scope); this task takes the lint-scoping path only.
Approach
Section titled “Approach”- In
_lint_state_origin_core.ts, add acurrentBranch(g)helper — taking theGitclient, likelistTaskBranches(g)/resolveBaseRefs(g, …)— that returnsgit rev-parse --abbrev-ref HEAD(trimmed), yieldingHEADwhen the checkout is detached. - Change the signature
lint(runner, repo, baseRef)tolint(runner, repo, baseRef, scopeBranch?)(scopeBranchis the new FOURTH argument).lintalready buildsconst g = new Git(repo, { runner }); whenscopeBranchis omitted, resolve it there viacurrentBranch(g). AfterlistTaskBranches(g), filter the result tobranches.filter((b) => b === scopeBranch). WhenscopeBranchis not atask/*branch, or is not among the local task branches, the filtered list is empty and the lint is a no-op returning[0, [], []]. - In
lint-state-origin.ts, add an optionalbranchfield to the input schema and pass it through as the fourthlintargument (lint(ctx.git, repo, args.baseRef, args.branch)). When it is omitted,lintresolves the currentHEADbranch itself (step 2); passing--branchexplicitly stays supported for tests. - Leave
resolveBaseRefsand the trunk-exclusion logic (commitsOnBranch/isStateOnlyCommit) unchanged — this task only narrows WHICH branches are walked, not how commits are excluded or classified. - Keep the render hook’s messages accurate for the scoped case: when the filtered branch list is empty, the existing “no task/* branches present; nothing to check.” line already reads correctly.
- Add regression tests to
apps/sdlc/lib/model/entities/task/ops/tests/lint-state-origin.test.tsfor sibling-branch isolation (a stray state-only commit ontask/otherdoes not fail whileHEADis a cleantask/fixture; the offending branch is still flagged when it is the branch checked; committing on a non-task branch with a dirty siblingtask/*branch is a no-op). - Confirm the
lefthook.ymlproject-check-task-state-originline needs no change (the op auto-detects the current branch); update only the inline comment there if it documents the retired repo-wide behavior.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/model/entities/task/ops/_lint_state_origin_core.ts#lint | modify | Add a currentBranch(g) helper and a fourth scopeBranch? parameter to lint(runner, repo, baseRef, scopeBranch?); filter listTaskBranches(g)’ output down to the branch being committed, resolving HEAD via currentBranch(g) when scopeBranch is omitted. |
apps/sdlc/lib/model/entities/task/ops/lint-state-origin.ts#input | modify | Add an optional branch input and pass it through as the fourth lint argument; lint resolves the current HEAD branch when it is omitted. |
apps/sdlc/lib/model/entities/task/ops/tests/lint-state-origin.test.ts | modify | Add sibling-branch-isolation cases proving a stray state-only commit on another task/* branch no longer fails the current branch’s commit. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
lint()in_lint_state_origin_core.tswalks only the branch being committed (the currentHEADbranch), not every localtask/*branch. - AC-2: With a state-only commit on branch
task/otherandHEADon a cleantask/fixture,sdlc task lint-state-originexits 0 — asserted by a new test. - AC-3: With
HEADon the offendingtask/*branch that carries the state-only commit,sdlc task lint-state-originstill exits 1 and names the violating field (existing regression coverage preserved). - AC-4: With
HEADon a non-task/*branch (e.g.mainor ameta-task/*branch), the lint exits 0 even when a siblingtask/*branch carries a stray state-only commit. - AC-5: The pre-existing cases in
apps/sdlc/lib/model/entities/task/ops/tests/lint-state-origin.test.ts(clean_branch,state_only_commit_flagged,state_plus_body_change_ignored,non_state_field_change_ignored,main_state_commit_ignored,origin_lagging_local_main) still pass unchanged. - AC-6:
bun testonapps/sdlc/lib/model/entities/task/ops/tests/lint-state-origin.test.tspasses.
Out of scope
Section titled “Out of scope”- Hardening task-work Step 10.4’s
append_pr_urlto land theprs:commit reliably on main — the alternative fix named in the Goal; deferred, not pursued here. - Changing the set of frontmatter fields treated as task-state
(
TASK_STATE_FIELDS) or the trunk-exclusion logic (resolveBaseRefs). - Sibling tasks T-8NFT (start_task stranding on pre-existing drift) and closed/done T-SIHV (task-state commits landing on the worktree branch).
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 sksizer/dev.