Skip to content

Commit 4bd884c

Browse files
refactor(prompt): make prompt construction template-driven
1 parent 1e363a0 commit 4bd884c

22 files changed

Lines changed: 2834 additions & 622 deletions

docs/superpowers/plans/2026-04-04-go-template-prompt-builder.md

Lines changed: 681 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
# Go-template prompt builder refactor
2+
3+
## Summary
4+
5+
Refactor `internal/prompt/` so prompt construction is template-driven end to end. That includes system-prompt fallbacks in `internal/prompt/templates.go`, assembled prompt bodies in `internal/prompt/prompt.go`, shared optional sections such as project guidelines and previous reviews, and oversized-diff fallback text. By the end of this workstream, prompt rendering should use typed Go view models plus nested `text/template` partials instead of manual `strings.Builder` / `fmt.Fprintf` assembly.
6+
7+
This design builds on the branch’s current partial migration (`prompt_body_templates.go` and `assembled_{single,range,dirty}.tmpl`) but pushes it to the intended finish line: no stringbuilder-style prompt assembly remains in the prompt package for review, range, dirty, address, security, or design-review flows.
8+
9+
## Goals
10+
11+
- Make prompt structure explicit through nested Go templates.
12+
- Cover all prompt families involved in review flows: single, range, dirty, address, security, and design-review.
13+
- Remove stringbuilder-style prompt assembly from prompt construction code.
14+
- Use typed view structs per prompt family rather than map-driven rendering.
15+
- Preserve current wording, section ordering, trimming priorities, and fallback behavior unless an intentional wording change is called out.
16+
- Keep exported entry points unchanged, including `Build`, `BuildWithAdditionalContext`, `BuildDirty`, `BuildAddressPrompt`, and `GetSystemPrompt`.
17+
18+
## Non-goals
19+
20+
- Reworking caller APIs or config resolution.
21+
- Changing review policy or output expectations.
22+
- Introducing a generic metadata engine or schema language for prompts.
23+
- Reorganizing unrelated packages outside `internal/prompt/`.
24+
- Doing speculative abstraction beyond what is needed to remove ad hoc prompt assembly.
25+
26+
## Current problems
27+
28+
Today the prompt package still spreads prompt construction across:
29+
30+
- hard-coded fallback constants in `prompt.go`
31+
- agent-specific template files in `templates/`
32+
- body renderers that currently concatenate preformatted strings
33+
- helper methods like `writeProjectGuidelines`, `writePreviousReviews`, `writePreviousAttemptsForGitRef`, and `BuildAddressPrompt` that manually append markdown
34+
- oversized-diff fallback variants built from string formatting helpers
35+
36+
That leaves the package in a mixed state where some prompt shape lives in templates and some lives in procedural string assembly. The result is harder to reason about, harder to test structurally, and easy to regress as more conditions are added.
37+
38+
## Approved design
39+
40+
### 1. Template the whole prompt system, not just final bodies
41+
42+
The end state uses templates for:
43+
44+
- system prompt fallbacks
45+
- top-level prompt bodies
46+
- shared optional sections
47+
- current commit / range / dirty sections
48+
- oversized-diff fallback variants
49+
50+
`GetSystemPrompt` remains the public entry point, but its default fallback content moves out of Go string constants and into templates. Agent-specific template selection still works the same way; the difference is that the fallback path is also template-rendered.
51+
52+
### 2. Use typed nested view models per prompt family
53+
54+
Each prompt family gets a typed view struct with nested section structs. The important split is between orchestration and rendering:
55+
56+
- Go code gathers data, resolves review type aliases, chooses fallback mode, and applies size fitting.
57+
- Templates render the document shape from typed data.
58+
59+
Representative view types:
60+
61+
- `singlePromptView`
62+
- `rangePromptView`
63+
- `dirtyPromptView`
64+
- `addressPromptView`
65+
- `systemPromptView`
66+
67+
Representative nested section types:
68+
69+
- `guidelinesSectionView`
70+
- `previousReviewSectionView`
71+
- `reviewAttemptSectionView`
72+
- `currentCommitSectionView`
73+
- `commitRangeSectionView`
74+
- `dirtyChangesSectionView`
75+
- `diffSectionView`
76+
- `diffFallbackView`
77+
78+
The exact type names can vary, but the structure should remain typed and explicit. The goal is to model prompt sections directly, not to shuttle around large pre-rendered markdown strings.
79+
80+
### 3. Use nested templates for shared sections
81+
82+
Instead of one flat template per prompt body with opaque string slots, templates should call shared named partials. Examples:
83+
84+
- `project_guidelines`
85+
- `previous_reviews`
86+
- `previous_review_attempts`
87+
- `current_commit`
88+
- `commit_range`
89+
- `dirty_changes`
90+
- `diff_block`
91+
- `codex_commit_fallback`
92+
- `codex_range_fallback`
93+
- `address_findings`
94+
95+
This keeps shared prompt structure in one place and avoids reintroducing procedural formatting in Go helpers.
96+
97+
### 4. Keep fitting and prioritization in Go
98+
99+
Prompt budgeting remains explicit Go logic. Templates render the ideal shape; Go decides how much data is available to render.
100+
101+
The fitting rules stay aligned with current behavior:
102+
103+
- Preserve the system prompt prefix.
104+
- Preserve the current commit / range / dirty header section.
105+
- Trim optional context before sacrificing current metadata.
106+
- For single/range prompts, downgrade to the existing large-diff fallback variants when the inline diff cannot fit.
107+
- For dirty prompts, keep the current truncated-inline-diff behavior.
108+
- Use the hard cap only as a final guardrail.
109+
110+
The key change is that fitting operates on typed view fields and re-rendered templates instead of handwritten prompt concatenation.
111+
112+
### 5. Finish the migration already underway on this branch
113+
114+
The branch already has:
115+
116+
- `internal/prompt/prompt_body_templates.go`
117+
- `internal/prompt/prompt_body_templates_test.go`
118+
- `internal/prompt/templates/assembled_single.tmpl`
119+
- `internal/prompt/templates/assembled_range.tmpl`
120+
- `internal/prompt/templates/assembled_dirty.tmpl`
121+
122+
Those pieces should be kept only if they serve the final nested-template design. It is acceptable to restructure them if needed. Preserving the temporary shape is not a requirement; reaching the cleaner typed nested-template architecture is.
123+
124+
## Components
125+
126+
### Template definitions
127+
128+
Use embedded templates under `internal/prompt/templates/` for both system prompts and assembled bodies. Split templates by responsibility:
129+
130+
- family templates for single/range/dirty/address/system prompts
131+
- shared partials for repeated sections
132+
- fallback templates for large-diff messaging
133+
134+
Templates should contain layout and branching that is natural to read in a template. They should not take on budgeting or repository lookup logic.
135+
136+
### Renderer
137+
138+
Introduce a small renderer layer that owns template parsing and execution. It should expose explicit entry points such as:
139+
140+
- `renderSinglePrompt(...)`
141+
- `renderRangePrompt(...)`
142+
- `renderDirtyPrompt(...)`
143+
- `renderAddressPrompt(...)`
144+
- `renderSystemPrompt(...)`
145+
146+
It is fine for the implementation to share an internal `executeTemplate` helper, but call sites should still use prompt-family-specific entry points.
147+
148+
### Builder/orchestration
149+
150+
The existing builder methods stay responsible for:
151+
152+
- loading guidelines
153+
- loading previous reviews and responses
154+
- loading previous attempts
155+
- reading commit/range metadata
156+
- computing diffs and fallback modes
157+
- enforcing size limits
158+
159+
Their job is to fill typed views and call renderers, not to write markdown themselves.
160+
161+
### Fit helpers
162+
163+
Keep family-specific fit helpers where needed, but operate on typed fields. For example, a single-prompt fit helper can trim nested optional sections or degrade a diff section while preserving the current commit section.
164+
165+
## Data flow
166+
167+
### Single, range, and dirty prompts
168+
169+
1. Resolve prompt kind and review type alias.
170+
2. Gather git/config/db data.
171+
3. Build a typed view with nested sections.
172+
4. Render the prompt body through named templates and shared partials.
173+
5. If over budget, trim optional sections or degrade diff mode in Go.
174+
6. Re-render until the prompt fits, then prepend the rendered system prompt.
175+
176+
### Address prompts
177+
178+
1. Gather project guidelines, previous attempts, severity filter instruction, review findings, and original diff context.
179+
2. Build an `addressPromptView`.
180+
3. Render the full prompt through templates instead of `strings.Builder`.
181+
4. Return the rendered string without changing public behavior.
182+
183+
### System prompts
184+
185+
1. Normalize the agent name.
186+
2. Resolve the prompt family (`review`, `range`, `dirty`, `address`, `security`, `design-review`, `run`).
187+
3. Prefer an agent-specific template file when present.
188+
4. Otherwise render the default fallback template for that family.
189+
5. Apply the current date and no-skills instruction through structured template data rather than string concatenation.
190+
191+
## Error handling
192+
193+
- Template parse failures should fail fast during tests or package initialization.
194+
- Template execution failures should be returned with prompt-family context.
195+
- Missing optional data should produce omitted sections, not placeholder text.
196+
- Internal helpers may still use `bytes.Buffer` to execute templates; the ban is on ad hoc prompt assembly, not on standard library rendering internals.
197+
198+
## Testing strategy
199+
200+
Update and extend:
201+
202+
- `internal/prompt/prompt_body_templates_test.go`
203+
- `internal/prompt/prompt_test.go`
204+
- `internal/prompt/templates_test.go`
205+
206+
### Coverage to preserve
207+
208+
- Expected headings and section ordering for single/range/dirty prompts.
209+
- Previous reviews and responses rendering.
210+
- Previous review attempts rendering.
211+
- Additional context ordering.
212+
- Guidelines loading behavior.
213+
- Codex and non-Codex fallback wording.
214+
- Prompt cap behavior and UTF-8 safety.
215+
216+
### New coverage to add
217+
218+
- Nested section templates render the same shape as the prior inline output.
219+
- Address prompts render through templates and preserve ordering.
220+
- Default system prompt fallbacks render from templates for review/security/address/design-review families.
221+
- No-skills/date injection behavior remains correct after the system-prompt migration.
222+
- No prompt path depends on `strings.Builder`-assembled markdown sections.
223+
224+
## Risks and mitigations
225+
226+
### Risk: output drift
227+
228+
Moving all prompt construction into templates can change spacing or section order.
229+
230+
**Mitigation:** keep wording stable, add ordering assertions, and compare rendered output directly in focused tests.
231+
232+
### Risk: overfitting the temporary branch shape
233+
234+
The branch already has a partial template migration that still uses flat string slots.
235+
236+
**Mitigation:** prefer the approved typed nested-template structure even if it means reshaping the current in-progress files.
237+
238+
### Risk: templates becoming opaque
239+
240+
Too much logic in templates would hide behavior that belongs in Go.
241+
242+
**Mitigation:** keep budgeting, repo lookups, and fallback selection in Go; keep templates focused on structure.
243+
244+
## Rollout
245+
246+
Land the work in place in `internal/prompt/`. The migration can proceed incrementally, but the finished state should leave no prompt-construction path relying on stringbuilder-style markdown assembly.
247+
248+
## Success criteria
249+
250+
- `Build`, `BuildWithAdditionalContext`, `BuildDirty`, `BuildAddressPrompt`, and `GetSystemPrompt` all render through templates.
251+
- Shared prompt sections use nested partials rather than handwritten markdown assembly.
252+
- Existing review behavior, section ordering, and fallback semantics remain materially unchanged.
253+
- The prompt package no longer assembles prompt markdown through `strings.Builder` / `fmt.Fprintf` style code paths.
254+
- Regression tests cover system prompts, address prompts, and body prompts under both normal and over-budget conditions.

0 commit comments

Comments
 (0)