T-Q4E6-migration-gate-runs-prose-linter
Status: closed/obsoleted · Impact: medium · Complexity: small
Restore-and-prevent follow-up from the check_skill_prose.py → TypeScript
port. The en-masse .py→.ts migration’s invariants cutover swapped path
literals in skill invariants.yaml files but did not re-validate the phrase
pins against the parallel SKILL.md prose-sweep. Two stale pins in
plugin/skills/task-work/invariants.yaml (a Python threading.Thread heartbeat
reference and a pre-reason= LEASE-CONFLICT stderr shape) survived and only
surfaced when a downstream dogfood task ran the restored prose checker. The
migration quality gate should run the skill-prose linter (or the restored
check_skill_prose.ts) so stale invariant pins surface at migration time
rather than leaking into downstream tasks.
From T-VOH6-port-check-skill-prose-to-ts: The .py->.ts invariants cutover swapped path literals but did not re-validate the phrase pins against the prose-sweep, leaving two stale task-work/invariants.yaml pins that only surfaced in a downstream dogfood task. The migration quality gate should run the skill-prose linter (or the restored check_skill_prose.ts) so stale invariant pins surface at migration time.
The skill-prose linter runs in two places — the lefthook pre-commit
gate and the /project-check skill — but NOT in the project’s
quality_checks: list in sdlc.yaml. That list is what the en-masse
migration’s cutover gate (and /sdlc:task-work Step 7) actually ran,
so the migration never re-validated the phrase pins. Two stale pins
in task-work’s invariants.yaml survived and the prose linter still
flags them today.
| Location | Role today |
|---|---|
sdlc.yaml | The migration cutover gate / task-work Step 7 verb list (quality_checks:): bunx tsc --noEmit, bun test, bun test ./.claude, audit_entities.ts, two .claude/ Python harnesses. The skill-prose linter is absent — the gap this task closes. |
plugin/scripts/lint_skill_prose.ts | The prose-invariant linter itself. Run bare against plugin/skills/task-work/SKILL.md it currently exits 1 with 2 violations. The migration gate should run it (directly, or via the check_skill_prose.ts wrapper). |
.claude/skills/project-check/check_skill_prose.py | The project-check wrapper on main today: sweeps every plugin/skills/*/SKILL.md and shells the linter. Broken — references the deleted lint_skill_prose.py. Its .ts replacement is restored by T-VOH6-port-check-skill-prose-to-ts (PR #200, not yet merged to main); the gate verb should target the .ts wrapper once that lands. |
plugin/skills/task-work/invariants.yaml | Carries the two stale pins under required_phrases:: threading.Thread(daemon=True) (a Python heartbeat idiom the SKILL.md prose no longer uses) and LEASE-CONFLICT ref=<ref> owner=<other-host-id> (a pre-reason= stderr shape) — both flagged by the linter today. Its foot comment still cites the deleted lint_skill_prose.py. |
plugin/scripts/run_quality_checks.ts | The executor that reads quality_checks: and runs each verb; gates the migration cutover and Step 7. Adding the linter to the list is what makes it run here. |
Proposed
Section titled “Proposed”sdlc.yaml’s quality_checks: list includes the skill-prose linter, so
any future en-masse cutover (and every task-work Step 7) runs it and
fails on stale invariant pins at gate time rather than leaking them to a
downstream dogfood task. The two currently-stale task-work pins are
realigned with the SKILL.md prose so the newly-added gate verb passes on
a clean checkout.
Approach
Section titled “Approach”- Add the skill-prose linter to
sdlc.yaml’squality_checks:list as a new verb. Prefer the wrapperbun run .claude/skills/project-check/check_skill_prose.tsonce T-VOH6-port-check-skill-prose-to-ts (PR #200) merges (it sweeps everyplugin/skills/*/SKILL.md, so one verb covers the whole skill corpus). If this task lands before #200, gate against the linter directly with an explicit glob (bun run plugin/scripts/lint_skill_prose.ts plugin/skills/*/SKILL.md). Add a YAML comment explaining it pins skill-prose invariants the migration cutover previously skipped. - Realign the two stale
plugin/skills/task-work/invariants.yamlpins so the newly-added gate verb passes on a clean checkout. For each, either update the pinnedphrase:to the current SKILL.md wording, or update the SKILL.md prose to re-assert the pin — whichever reflects the intended invariant:threading.Thread(daemon=True)— the heartbeat is now TypeScript; update the pin to the current async heartbeat idiom the Step-6 prose actually uses (or restore the phrase to the prose if the daemon-thread semantics are still the intended contract).LEASE-CONFLICT ref=<ref> owner=<other-host-id>— update the pin to the currentreason=-bearing stderr shape emitted byacquire_lease(or restore that exact line to Step 2a’s prose).
- Confirm
bun run .claude/skills/project-check/check_skill_prose.tsexits 0 across all skills, and thatrun_quality_checks.tsruns the new verb. - Fix the stale
# Linter at plugin/scripts/lint_skill_prose.pycomment at the foot ofplugin/skills/task-work/invariants.yaml(the migration left it pointing at the deleted.py).
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
sdlc.yaml | modify | Add bun run .claude/skills/project-check/check_skill_prose.ts to quality_checks:, with an explanatory comment |
plugin/skills/task-work/invariants.yaml | modify | Realign the two stale pins (heartbeat idiom, LEASE-CONFLICT shape) with current SKILL.md prose; fix the .py linter-path comment |
plugin/skills/task-work/SKILL.md | modify | If the chosen realignment is “restore the phrase to the prose,” re-assert the pinned line in Step 2a / Step 6 |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
sdlc.yaml’squality_checks:list contains a verb that runs the skill-prose linter. Verifiable:grep -E "check_skill_prose\.ts|lint_skill_prose\.ts" sdlc.yamlreturns ≥1 match. - AC-2: The new gate verb exits 0 against the live repo (every
plugin/skills/*/SKILL.mdpasses itsinvariants.yaml) when run viabun run plugin/scripts/run_quality_checks.ts --config sdlc.yaml --line. - AC-3:
bun run plugin/scripts/lint_skill_prose.ts plugin/skills/task-work/SKILL.mdexits 0 (the two stale pins —threading.Thread(daemon=True)andLEASE-CONFLICT ref=<ref> owner=<other-host-id>— no longer violate; they currently produce 2 violations). - AC-4:
bun run plugin/scripts/run_quality_checks.ts --config sdlc.yaml --lineruns the prose-linter verb in its run set and the full gate exits 0 on a clean checkout. - AC-5: No
lint_skill_prose.pyreference remains inplugin/skills/task-work/invariants.yaml. Verifiable:grep -c "lint_skill_prose.py" plugin/skills/task-work/invariants.yamlreturns 0.
Out of scope
Section titled “Out of scope”- Re-validating invariant pins across ALL skills’
invariants.yamlbeyond task-work — AC-2 already gates the whole corpus via the wrapper; any other stale pin surfaced by AC-2 is fixed as part of making AC-2 pass, but no proactive audit of pin intent is in scope. - Adding the prose linter to downstream consumer projects’
sdlc.yaml— this task wires only this repo’s gate. - Changing the lefthook pre-commit wiring (it already runs the checker; this task adds the parallel quality-gate path).
- Migrating the remaining
.claude/Python harnesses still listed inquality_checks:— separateclaude-ts-migrationtasks.
Dependencies
Section titled “Dependencies”-
Soft: T-VOH6-port-check-skill-prose-to-ts (PR #200) restores
check_skill_prose.ts, the preferred gate verb. Not a hard blocker — if this task lands first, gate againstplugin/scripts/lint_skill_prose.tsdirectly (Approach step 1) and switch to the wrapper when#200 merges.
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-06-02 UTC from T-VOH6-port-check-skill-prose-to-ts in git@github.com:sksizer/dev.git.