T-6OS7-rewire-skill-prose-gate-onto-contract
Status: closed/done · Impact: high · Complexity: medium
Point sdlc gate skill-prose at the new contract: compile and run the
markdown-contract contract instead of importing lintSkill from the
bespoke matcher. The op keeps its name, its --paths / --diff-only
inputs, and the exact finding-output format that the lefthook recipe and
check_skill_prose.ts wrapper parse — so the rewire is invisible to
every consumer while the engine underneath changes.
| Location | Role today |
|---|---|
plugin/lib/services/gate/ops/skill-prose.ts | The gate op; imports lintSkill from _skill_prose_core.ts, resolves --paths / --diff-only, emits finding lines |
.claude/skills/project-check/check_skill_prose.ts | The project-check wrapper that shells the gate op and parses its finding lines |
.claude/skills/project-check/skill_md_harness.ts | The shared SKILL.md harness that already runs sibling checks against markdown-contract; the model for a thin contract caller |
lefthook.yml | Pre-commit recipe that invokes the gate and parses its output / exit code |
Approach
Section titled “Approach”Swap the op’s engine while preserving its entire external surface.
- In
skill-prose.ts, replaceimport { lintSkill } from "./_skill_prose_core.ts"with the runner from_skill_prose_contract.ts. In the handler, for each SKILL.md select the per-skill contract by slug, runcontract.validate(source, { path }), and map findings → the existingViolation[]({ path, line, message }) thatrenderSkillProsealready prints. - Preserve: op path
["gate","skill-prose"], thepaths/diffOnly/displayRootinputs, the staged-diff seam, the skills walk, the<path>:<line>: <message>render, exit 1 on violations, and the summary line. withInvariantscount. It currently counts siblinginvariants.yaml; during this phase the sidecars still exist (deleted in T-UDY1) so it keeps working, but it now means “covered by the contract.” Leave the wrapper’s partition for T-UDY1; only the op changes here.- Honest byte-stability. Exit codes and the
<path>:<line>: <message>shape stay stable, but the message text changes (markdown-contract messages ≠ the oldpyRepr-quoted strings). Because the op stops reading sidecars, the sidecar-drivencheck_skill_prose.test.ts(it writes an ephemeralinvariants.yamland expects enforcement) no longer applies — rewrite it here, in lockstep, onto the contract path (a real skill with a real contract violation), and confirm the lefthookproject-check-skill-proserecipe still gates red on a true violation.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/lib/services/gate/ops/skill-prose.ts | modify | Import the contract runner instead of lintSkill; per-skill select + validate + map findings to Violation[]; keep op path / inputs / render / exit |
.claude/skills/project-check/tests/check_skill_prose.test.ts | modify | Rewrite the sidecar-driven cases onto the contract path (the op no longer reads sibling invariants.yaml); assert exit-code passthrough, a <path>:<line> citation, and a real violation |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
sdlc gate skill-prosecompiles and runs the per-skill contract from T-FQFJ; the op name,--paths, and--diff-onlyinputs are unchanged. - AC-2: Exit codes and the
<path>:<line>: <message>output shape are stable; where finding-message text changes,check_skill_prose.test.tsand any lefthook output assertions are updated in the same PR. Theproject-check-skill-proselefthook recipe gates red on a real violation. - AC-3: No project-local phrase matcher remains in the gate path; the op
resolves phrases through
markdown-contractonly.
Out of scope
Section titled “Out of scope”- Deleting
_skill_prose_core.ts— the next task removes it once the gate no longer imports it. - Retiring the
invariants.yamlsidecars or migrating fixtures — later phase-C tasks.
Dependencies
Section titled “Dependencies”- T-FQFJ-author-skill-prose-contract — the gate runs the contract this task wires up.
Depends on
Section titled “Depends on”T-FQFJ-author-skill-prose-contract