T-D7PW-templatized-commit-kinds
Status: closed/done · Impact: medium · Complexity: medium
The commit corpus is already machine-shaped — chore(tasks): start T-…,
docs(tasks): verify T-… implementation-ready, docs(tasks): record PR for T-… account for the bulk of recent history — but every one of those
subjects is composed by an LLM following SKILL.md prose, and several are
pinned verbatim in invariants files. Give commit messages the same
treatment reports (sdlc report render) and PR bodies
(T-UZ7T-pr-bodies-as-report-style-eta-kinds) get: a Zod contract +
Eta template per commit kind, rendered deterministically, so skills pass
structured params instead of recalling prose shapes, and a subject-shape
change is a one-file template edit instead of a cross-skill sweep.
| Location | Role today |
|---|---|
plugin/conventions/commit-messages.md | Canonical safe routing (tempfile + git commit -F; PR #281 names sdlc_commit.ts as preferred). Says nothing about message content — every subject shape lives in skill prose. |
plugin/skills/task-work/SKILL.md | Composes chore(tasks): start <basename>, docs(tasks): record PR for <basename>, post-mortem commit subjects in prose |
plugin/skills/task-ensure-ready/SKILL.md | Composes docs(tasks): verify <basename> implementation-ready / flag <basename> as needs-definition in prose |
plugin/skills/task-close-out/SKILL.md | Composes docs(tasks): mark <basename> closed/done in prose |
plugin/skills/task-work/invariants.yaml | Pins the literal phrase docs(tasks): verify <basename> implementation-ready |
plugin/skills/task-close-out/invariants.yaml | Pins docs(tasks): record PR ... phrasing |
plugin/lib/services/report/render.ts | The schema-contracted render engine (validate → Eta) this task reuses for plain-text output |
plugin/lib/model/entities/task/reports/review/schema.ts | Precedent: entity-specific kind package consumed by a lib service |
Proposed
Section titled “Proposed”sdlc_commit.ts (lands via T-SPBO-commit-helper-routes-through-tempfile / PR #281) grows a
--kind <slug> mode: stdin carries a JSON payload instead of a raw message, the helper validates it
against the kind’s zod/v4 contract, renders the kind’s Eta template (plain text, autoEscape off) to
the message, and proceeds through its existing tempfile + git commit -F tail. Kind packages follow
the PR #278 placement rule: the task-lifecycle kind lives at
plugin/lib/model/entities/task/commits/lifecycle/{schema.ts,template.eta} with a payload like
{action: start|verify-ready|flag-needs-definition| record-pr|close-done|close-obsoleted|post-mortem, basename, detail?};
the registry and any shared machinery live beside the report service.
Raw-message mode (--message -) survives unchanged as the freeform
escape hatch. The skills that compose those subjects switch to one-line
--kind calls; the invariants that pin subjects keep passing because the
template emits the identical shapes — they now pin the template’s
output, which has a single source.
Approach
Section titled “Approach”- Extract or reuse the validate→render seam from
plugin/lib/services/report/render.tsfor plain-text output (same decision point as T-UZ7T-pr-bodies-as-report-style-eta-kinds Approach step 1 — whichever task lands second consumes the extraction the first one made). - Create
plugin/lib/services/commit/(kind registry + render entry; no new CLI noun yet —sdlc_commit.tsis the door) and theentities/task/commits/lifecycle/kind package.commits/is a sibling ofops/, never inside it (the walk imports everything inops/as op modules). - Extend
sdlc_commit.ts:--kind <slug>+ stdin-JSON mode; contract violations exit 2 with path-precise Zod issues; unknown kind exits 2 listing registered kinds. Raw mode untouched. - Wire the three pilot skills (task-work, task-ensure-ready,
task-close-out): replace composed-subject prose with the
--kindcall. Verify the pinned phrases in both invariants.yaml files still match the rendered output byte-for-byte; update the invariant notes to name the template as the source of the shape. - Tests: each lifecycle action renders its exact historical subject shape; payload round-trips metacharacters; invalid payload / unknown kind exit 2; raw mode regression-covered by PR #281’s existing suite.
- Update
plugin/conventions/commit-messages.md: kinds are the preferred shape for stereotyped messages, raw stdin for one-offs, heredoc fallback only where bun is unreachable.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/scripts/sdlc_commit.ts | modify | add --kind + stdin-JSON mode (file lands via PR #281 — see Dependencies) |
plugin/lib/services/commit/ | new | kind registry + plain-text render entry |
plugin/lib/model/entities/task/commits/lifecycle/schema.ts | new | lifecycle payload contract (action enum, basename, detail) |
plugin/lib/model/entities/task/commits/lifecycle/template.eta | new | subject/body shapes for each action |
plugin/lib/services/report/render.ts | modify | extract shared validate→render seam (skip if T-UZ7T extracted it first) |
plugin/skills/task-work/SKILL.md | modify | composed subjects → --kind calls |
plugin/skills/task-ensure-ready/SKILL.md | modify | composed subjects → --kind calls |
plugin/skills/task-close-out/SKILL.md | modify | composed subjects → --kind calls |
plugin/skills/task-work/invariants.yaml | modify | note the template as the pinned phrase’s source |
plugin/skills/task-close-out/invariants.yaml | modify | same |
plugin/conventions/commit-messages.md | modify | kinds preferred for stereotyped messages |
plugin/scripts/tests/sdlc_commit.test.ts | modify | kind-mode coverage |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
echo '{"action":"start","basename":"T-0001"}' | sdlc_commit --kind task-lifecycleproduces a commit whose subject is exactlychore(tasks): start T-0001; each enum action reproduces its historical subject shape byte-for-byte (snapshot test). - AC-2: invalid payload or unknown kind exits 2 with the Zod issues / the registered-kind list; no commit is created.
- AC-3: raw
--message -mode behaves exactly as shipped by PR #281 (existing tests untouched and green). - AC-4: the three pilot SKILL.mds contain no composed commit-subject prose for lifecycle actions; both invariants.yaml pinned phrases still match rendered output.
- AC-5:
bun testandbunx tsc --noEmitclean.
Out of scope
Section titled “Out of scope”- Kinds for non-task commit families (backlog capture, entities-migrate stamps, index regen) — same recipe once lifecycle proves out.
- A
sdlc commitregistry noun — the helper script stays the single door until a second non-CLI consumer appears. - PR-body templating — that’s T-UZ7T-pr-bodies-as-report-style-eta-kinds.
Dependencies
Section titled “Dependencies”- T-SPBO-commit-helper-routes-through-tempfile (PR #281) must merge first — this task extends
plugin/scripts/sdlc_commit.ts, which does not exist on main until then. Recorded in frontmatterdepends_on:.
Discovery context
Section titled “Discovery context”User direction (2026-06-04 session): “I actually wanted to parameterize
and templatize all commit messages anyway,” upon rediscovering in-flight
T-SPBO-commit-helper-routes-through-tempfile (PR #281) while
designing a commit helper from scratch. Third leg of the
schema-contracted rendering family: reports (PR #277/#278), PR bodies
(T-UZ7T-pr-bodies-as-report-style-eta-kinds), commit messages (this).
Corpus shape data: 10× verify … implementation-ready, 10× start …,
7× record PR for … in the last 40 commits.
Depends on
Section titled “Depends on”T-SPBO-commit-helper-routes-through-tempfile