Skip to content

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.

LocationRole today
plugin/skills/task-work/SKILL.mdStep 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.mdMeta-task PR body shape defined in prose
plugin/skills/entities-migrate/SKILL.mdMigration 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.mdImport PR body shape defined in prose
plugin/skills/milestones-from-file/SKILL.mdImport PR body as an inline heredoc
plugin/cli/backlog_cli/create.tsRolling backlog-capture PR body built in code
plugin/cli/lease_cli/parse_footer.tsParses 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.tsThe schema-contracted Eta render engine from PR #277/#278 — HTML-specific today (autoEscape on, writes to .sdlc/reports/)
plugin/conventions/skill-reports.mdDocuments the report pattern; no PR analog exists

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.

  1. Factor the shared core: extract the validate→renderString seam from plugin/lib/services/report/render.ts so 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 under lib/services/ or a parameterized render.ts — pick whichever keeps both services’ tests green without widening either’s surface.
  2. Build plugin/lib/services/pr/: shared prMeta contract (title, task/basename where applicable), kinds.ts registry, ops/render.ts (sdlc pr render <type> <payload.json> [--out <path>]) and ops/schema.ts — auto-discovered by the registry walk like the report ops.
  3. 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 of ops/, never inside it (the walk imports everything in ops/ as op modules).
  4. 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.
  5. Wire the three pilot SKILL.mds: replace each inline body block with payload-contract reference + render call + --body-file usage. Factor the shared recipe into plugin/conventions/ (extend skill-reports.md into a general schema-contracted-rendering doc, or a sibling pr-templates.md — decide by diff size).
  6. Tests mirroring plugin/lib/services/report/tests/render.test.ts: both pilot kinds render, contract violations carry Zod issues, footer round-trips through sdlc lease parse-footer, dry-run writes nothing.
LocationKindChange
plugin/lib/services/pr/newservice: shared contract, kinds registry, render engine wiring
plugin/lib/services/pr/ops/render.tsnewsdlc pr render op
plugin/lib/services/pr/ops/schema.tsnewsdlc pr schema op
plugin/lib/services/report/render.tsmodifyextract/reuse the validate→render core (step 1 decision)
plugin/lib/model/entities/task/prs/implementation/schema.tsnewimplementation-PR contract incl. footer fields
plugin/lib/model/entities/task/prs/implementation/template.etanewmarkdown body template
plugin/lib/model/entities/task/prs/meta-task/schema.tsnewmeta-task PR contract
plugin/lib/model/entities/task/prs/meta-task/template.etanewmarkdown body template
plugin/cli/lease_cli/parse_footer.tsmodifyconsume the shared footer constants
plugin/skills/task-work/SKILL.mdmodifyStep 10 body block → payload + render call
plugin/skills/spawn-task-pr/SKILL.mdmodifybody prose → payload + render call
plugin/skills/entities-migrate/SKILL.mdmodifybody prose → payload + render call
plugin/conventions/skill-reports.mdmodifygeneralize recipe or cross-link the PR sibling doc
plugin/lib/services/pr/tests/render.test.tsnewacceptance suite
plugin/cli/tests/sdlc.test.tsmodifynoun choice list grows by pr
  • AC-1: sdlc pr render task-implementation <payload.json> emits a markdown body containing every Step-10 section and the sdlc-lease footer; sdlc lease parse-footer extracts 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 test passes (new suite + existing report-service suite unchanged); bunx tsc --noEmit clean; CLI noun choice-list test updated for the pr noun.
  • 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-respond comment bodies) and gh pr edit flows.
  • PR title policy beyond what the kind schema carries.
  • none — the report service (PR #277/#278) is merged; this builds beside it.

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.


← Back to Tasks