T-VOH6-port-check-skill-prose-to-ts
Status: closed/done · Impact: low · Complexity: small
Port check_skill_prose.py — the project-check wrapper that runs the
skill-prose invariant linter over every SKILL.md — to TypeScript. The
checker is currently broken: it shells out to
plugin/scripts/lint_skill_prose.py, which the en-masse migration renamed to
lint_skill_prose.ts. The port restores it by invoking the .ts linter, and
advances the .claude/ harness onto the TypeScript substrate.
| Location | Role today |
|---|---|
.claude/skills/project-check/check_skill_prose.py | Python wrapper: partitions plugin/skills/*/SKILL.md by sibling invariants.yaml, shells out to the prose linter, prints a coverage summary, passes the linter’s exit code through. Broken (exit 2, “linter not found”) — references plugin/scripts/lint_skill_prose.py, now .ts. Flags --skills-dir, --linter. |
plugin/scripts/lint_skill_prose.ts | The migrated prose-invariant linter the wrapper must invoke. |
.claude/skills/project-check/tests/run_prose_evals.py | Python eval harness for the prose checker. |
Proposed
Section titled “Proposed”check_skill_prose.ts runs under bun, invokes lint_skill_prose.ts, and exits
0 on the live repo (checker restored). The Python checker and its Python eval
harness are removed; a bun:test suite replaces the harness; the
invariants.yaml partition, coverage summary, and exit-code passthrough are
preserved.
Approach
Section titled “Approach”- Port
check_skill_prose.py→check_skill_prose.ts(bun), invokingplugin/scripts/lint_skill_prose.ts(fixing the dangling.pyreference), preserving--skills-dir/--linterflags, theinvariants.yamlpartition, the coverage summary, and exit-code passthrough. - Replace
run_prose_evals.pywithtests/check_skill_prose.test.ts(bun:test) covering a clean run (exit 0) and the with/without-invariants.yamlcoverage partition. - Delete
check_skill_prose.pyandrun_prose_evals.py.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
.claude/skills/project-check/check_skill_prose.ts | new | TypeScript port invoking lint_skill_prose.ts |
.claude/skills/project-check/tests/check_skill_prose.test.ts | new | bun:test coverage replacing the Python harness |
.claude/skills/project-check/check_skill_prose.py | delete | replaced by the .ts port |
.claude/skills/project-check/tests/run_prose_evals.py | delete | replaced by the .test.ts suite |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
check_skill_prose.tsinvokesplugin/scripts/lint_skill_prose.ts(not.py), andbun run .claude/skills/project-check/check_skill_prose.tsexits 0 against the live repo (the checker that exits 2 today is restored). - AC-2:
check_skill_prose.pyandrun_prose_evals.pyno longer exist, andgrep -rn "check_skill_prose.py\|lint_skill_prose.py" .claudereturns no matches. - AC-3:
.claude/skills/project-check/tests/check_skill_prose.test.tspasses underbun testand asserts a clean exit plus the with/without-invariants.yamlcoverage partition. - AC-4:
check_skill_prose.tspreserves the--skills-dirand--linterflags, theinvariants.yamlpartitioning, the coverage summary line, and exit-code passthrough from the linter.
Out of scope
Section titled “Out of scope”- Porting the other project-check checkers — separate tasks.
- Refreshing the
.claude/skills/project-check/SKILL.mdscript citations — deferred to one follow-up sweep so per-checker tasks stay file-disjoint. - Any change to
lint_skill_prose.tsitself (already migrated and tested underplugin/).
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”- Tier-2 dogfood task for the
/sdlc:orchestrate→/sdlc:task-workmigration smoke test. Fixes a.claude/harness regression (danglinglint_skill_prose.pyreference) the TS migration introduced. Designed to run concurrently with thecheck_entitiesport — the two touch disjoint files.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-06-02. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
bun run .claude/skills/project-check/check_skill_prose.tsexits 0 against the live repo (verified in Step 7; also gated by thebun test ./.claudequality verb). Required re-syncing two staleplugin/skills/task-work/invariants.yamlpins — see Friction below. - AC-2: auto —
check_skill_prose.py/run_prose_evals.pydeleted viagit rm;command grep -rn "check_skill_prose.py\|lint_skill_prose.py" .claudereturns no matches (re-run after patching the SKILL.md / README / fixture citations). - AC-3: auto —
bun test ./.claude/skills/project-check/tests/check_skill_prose.test.ts→ 3 pass / 0 fail (clean run, with/without-invariants.yamlpartition, exit-code passthrough). - AC-4: agent-manual — confirmed
--skills-dirand--linterflags parse, the coverage-summary line prints, theinvariants.yamlpartition reports counts, and exit codes pass through (0 clean, 1 violation, 2 self-failure on bad dir / missing linter).
What worked
Section titled “What worked”- The sibling
check_pipe_tail.ts+check_pipe_tail.test.ts(already ported) gave an exact house-style template — argv parsing,pyResolve/isDir/isFile/displayPathhelpers, and ephemeral-fixture test layout — so the port and its tests dropped in cleanly. - The baseline-gated quality gate cleanly subtracted the 369 pre-existing
audit_entities.tsfindings; the run reportedOK 6/6with zero new drift.
Friction and automation gaps
Section titled “Friction and automation gaps”- AC-1 (“checker exits 0 on the live repo”) was blocked by pre-existing drift the TS migration left
behind:
plugin/skills/task-work/invariants.yamlstill pinnedthreading.Thread(daemon=True)(the heartbeat is now a backgroundedlease_heartbeat_loop.tsprocess) andLEASE-CONFLICT ref=<ref> owner=<other-host-id>in section 2a (the prose now emitsreason=owner=). The.py->.tsinvariants cutover (commit bd7ba2b) swapped path literals but did not re-validate the phrase pins against the prose-sweep (commit 6beced3) — a prose-only check_skill_prose run in that PR would have caught it. Automation gap: the en-masse migration’s quality gate should have run the skill-prose linter (or this restored checker) so the two stale pins surfaced at migration time instead of leaking into a downstream dogfood task. Fixing them here was strictly necessary to satisfy AC-1 and is in-scope for “fix the.claude/harness regression the TS migration introduced”, but it touched a plugin file outside the task’s literal Files-to-touch table. → T-Q4E6-migration-gate-runs-prose-linter - The task’s “Out of scope” defers “Refreshing the
.claude/skills/project-check/SKILL.mdscript citations”, but AC-2’s literal grep (check_skill_prose.py\|lint_skill_prose.pyacross.claude) forced refreshing this checker’s own citations in SKILL.md / tests/README.md / the clean fixture anyway. Gap: when a task’s AC contains a repo-wide grep assertion, the Out-of-scope list should not defer edits the grep would catch — the two contradicted each other and the AC (binding contract) won. A task-authoring lint that cross-checks grep-shaped ACs against Out-of-scope deferrals would catch this class. → T-X3F2-lint-grep-acs-vs-out-of-scope - The
Agenttool and theWrite/Edittools were unavailable for worktree paths in this dispatch context, so implementation ran inline (documented Step 6 fallback) and all file writes/edits went through Bash heredocs + small node patch scripts. Gap: the inline-implementation fallback works, but losingEditfor in-worktree files made small surgical edits clumsier than necessary (needed throwaway node scripts to dodge shell-quoting hazards on//and conventional-commit parens).
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-Q4E6-migration-gate-runs-prose-linter (https://github.com/sksizer/dev/pull/196) — run the skill-prose linter in the en-masse migration quality gate so stale invariant pins surface at migration time (spawned).
- T-X3F2-lint-grep-acs-vs-out-of-scope (https://github.com/sksizer/dev/pull/197) — lint grep-shaped acceptance criteria against Out-of-scope deferrals so the two cannot contradict (spawned).
- The third friction bullet (Edit/Agent tool unavailability for worktree paths) was SKIPPED — it is an environment/harness observation with no concrete plugin or project code fix.