T-UZ7T-pr-bodies-as-report-style-eta-kinds
Status: closed/superseded · Impact: medium · Complexity: medium
Every PR-opening skill today defines its PR body as prose inside its
SKILL.md — heredoc shapes, section lists, footer comments. That prose
drifts, can’t be validated, and re-states structure the report service
(PR #277/#278) already knows how to own deterministically. Give PR bodies
the same treatment: one Zod contract + one Eta template per PR type,
rendered by an sdlc pr render <type> verb, so skills assemble a payload
instead of hand-writing markdown and new PR types are a kind package, not
a new prose block.
| Location | Role today |
|---|---|
plugin/skills/task-work/SKILL.md | Step 10 defines the implementation-PR body inline (Summary / Task / Acceptance criteria / Test plan / Follow-up tasks spawned + sdlc-lease HTML-comment footer) as a quoted-heredoc shape the LLM fills |
plugin/skills/spawn-task-pr/SKILL.md | Meta-task PR body shape defined in prose |
plugin/skills/entities-migrate/SKILL.md | Migration PR body shape (incl. “Manual review needed” section) defined in prose — T-3A6G-implement-entities-migrate AC-7 says “templated in SKILL.md” |
plugin/skills/task-auto-define/SKILL.md | --make-pr chore-PR body defined in prose |
plugin/skills/import-planning/SKILL.md | Import PR body shape defined in prose |
plugin/skills/milestones-from-file/SKILL.md | Import PR body as an inline heredoc |
plugin/cli/backlog_cli/create.ts | Rolling backlog-capture PR body built in code |
plugin/cli/lease_cli/parse_footer.ts | Parses the lease-binding footer out of PR bodies — parse is code, but the footer’s render lives only in task-work prose (asymmetry; field names duplicated) |
plugin/lib/services/report/render.ts | The schema-contracted Eta render engine from PR #277/#278 — HTML-specific today (autoEscape on, writes to .sdlc/reports/) |
plugin/conventions/skill-reports.md | Documents the report pattern; no PR analog exists |
Proposed
Section titled “Proposed”A pr service at plugin/lib/services/pr/ mirroring the report service:
sdlc pr render <type> <payload.json> validates the payload against the
type’s zod/v4 contract, renders the type’s Eta template to markdown,
writes it to a file gh pr create --body-file can consume, and returns
{ bodyPath, title }; sdlc pr schema <type> emits the contract as JSON
Schema. Entity-bound PR types live under their entity package
(per the PR #278 placement rule), e.g.
plugin/lib/model/entities/task/prs/implementation/{schema.ts,template.eta};
non-entity-bound types (corpus-wide migration PR) live in the service’s
own kinds dir. The lease-binding footer is emitted by the implementation
template from schema fields that share their constants with
parse_footer.ts, closing the render/parse asymmetry. Pilot consumers:
task-work Step 10, spawn-task-pr, entities-migrate — their SKILL.mds lose
the body prose and gain a payload-contract reference + render call,
following the same recipe shape as conventions/skill-reports.md.
Approach
Section titled “Approach”- Factor the shared core: extract the validate→
renderStringseam fromplugin/lib/services/report/render.tsso report (HTML, autoEscape on,.sdlc/reports/sink) and pr (markdown, autoEscape off, temp-file sink) reuse one engine. Decide here whether that’s a shared module underlib/services/or a parameterized render.ts — pick whichever keeps both services’ tests green without widening either’s surface. - Build
plugin/lib/services/pr/: sharedprMetacontract (title, task/basename where applicable),kinds.tsregistry,ops/render.ts(sdlc pr render <type> <payload.json> [--out <path>]) andops/schema.ts— auto-discovered by the registry walk like the report ops. - Create the pilot kind packages:
entities/task/prs/implementation/(sections from task-work Step 10; footer line rendered from schema fields),entities/task/prs/meta-task/(spawn-task-pr shape), and the migration PR kind (service-local — not entity-bound).prs/is a sibling ofops/, never inside it (the walk imports everything inops/as op modules). - Single-source the footer: a shared constant module consumed by both
the implementation kind’s schema/template and
plugin/cli/lease_cli/parse_footer.ts, so a renamed field breaks at compile time, not at orchestrator runtime. - Wire the three pilot SKILL.mds: replace each inline body block with
payload-contract reference + render call +
--body-fileusage. Factor the shared recipe intoplugin/conventions/(extend skill-reports.md into a general schema-contracted-rendering doc, or a sibling pr-templates.md — decide by diff size). - Tests mirroring
plugin/lib/services/report/tests/render.test.ts: both pilot kinds render, contract violations carry Zod issues, footer round-trips throughsdlc lease parse-footer, dry-run writes nothing.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/lib/services/pr/ | new | service: shared contract, kinds registry, render engine wiring |
plugin/lib/services/pr/ops/render.ts | new | sdlc pr render op |
plugin/lib/services/pr/ops/schema.ts | new | sdlc pr schema op |
plugin/lib/services/report/render.ts | modify | extract/reuse the validate→render core (step 1 decision) |
plugin/lib/model/entities/task/prs/implementation/schema.ts | new | implementation-PR contract incl. footer fields |
plugin/lib/model/entities/task/prs/implementation/template.eta | new | markdown body template |
plugin/lib/model/entities/task/prs/meta-task/schema.ts | new | meta-task PR contract |
plugin/lib/model/entities/task/prs/meta-task/template.eta | new | markdown body template |
plugin/cli/lease_cli/parse_footer.ts | modify | consume the shared footer constants |
plugin/skills/task-work/SKILL.md | modify | Step 10 body block → payload + render call |
plugin/skills/spawn-task-pr/SKILL.md | modify | body prose → payload + render call |
plugin/skills/entities-migrate/SKILL.md | modify | body prose → payload + render call |
plugin/conventions/skill-reports.md | modify | generalize recipe or cross-link the PR sibling doc |
plugin/lib/services/pr/tests/render.test.ts | new | acceptance suite |
plugin/cli/tests/sdlc.test.ts | modify | noun choice list grows by pr |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
sdlc pr render task-implementation <payload.json>emits a markdown body containing every Step-10 section and thesdlc-leasefooter;sdlc lease parse-footerextracts the same task basename and lease_id the payload carried (round-trip test). - AC-2:
sdlc pr schema <type>emits JSON Schema for each registered type; unknown type exits non-zero listing registered types. - AC-3: a contract-violating payload exits non-zero with path-precise Zod issues and writes nothing.
- AC-4: task-work, spawn-task-pr, and entities-migrate SKILL.mds contain no inline PR-body section lists or heredoc body shapes — each references its kind’s schema and the render verb.
- AC-5:
bun testpasses (new suite + existing report-service suite unchanged);bunx tsc --noEmitclean; CLI noun choice-list test updated for theprnoun.
Out of scope
Section titled “Out of scope”- Retrofitting
plugin/cli/backlog_cli/create.ts(rolling backlog-capture PR builds its body in code; can adopt the kind later). - Wiring task-auto-define, import-planning, milestones-from-file — same recipe, follow-up wiring once the pilot proves out.
- PR responses (
/sdlc:pr-respondcomment bodies) andgh pr editflows. - PR title policy beyond what the kind schema carries.
Dependencies
Section titled “Dependencies”- none — the report service (PR #277/#278) is merged; this builds beside it.
Discovery context
Section titled “Discovery context”Direct user request (2026-06-04 session) immediately after shipping the schema-contracted report pipeline (PR #277) and its placement refactors (PR #278): “use eta templates for PRs so we can support different templates in a clear way for different pr types and minimize prose definition necessary.” A corpus search found no existing task or backlog item; nearest neighbors are the spec-PR-vs-impl-PR branch-naming task and T-3A6G-implement-entities-migrate’s prose-templated PR body.