···11package daily
2233import (
44+ "encoding/json"
45 "fmt"
56 "regexp"
67 "strings"
···4243 Body string
4344}
44454646+type bodyMetadata struct {
4747+ Fields []bodyMetadataField `json:"fields"`
4848+}
4949+5050+type bodyMetadataField struct {
5151+ ID string `json:"id"`
5252+ Label string `json:"label"`
5353+}
5454+4555var headingPattern = regexp.MustCompile(`^#{2,6}\s*(.*?)\s*(?:<!--\s*pad:id:([A-Za-z0-9._-]+)\s*-->)?\s*$`)
5656+var bodyMetadataPattern = regexp.MustCompile(`(?m)^<!--\s*pad:fields:(.*?)\s*-->$`)
46574758var legacyFieldAliases = map[string][]string{
4859 "yesterday": {"✅ What did you do yesterday?"},
···223234}
224235225236func (e Entry) Body() string {
226226- blocks := make([]string, 0, len(e.Template.Fields))
237237+ blocks := make([]string, 0, len(e.Template.Fields)+1)
238238+ if metadata := renderBodyMetadata(e.Template); metadata != "" {
239239+ blocks = append(blocks, metadata)
240240+ }
227241 for _, field := range e.Template.Fields {
228242 switch field.Type {
229243 case issueform.FieldMarkdown:
···330344}
331345332346func renderSection(field issueform.Field, body string) string {
333333- heading := "### " + field.Label
334334- if field.ID != "" {
335335- heading += " <!-- pad:id:" + field.ID + " -->"
347347+ return "### " + field.Label + "\n" + body
348348+}
349349+350350+func renderBodyMetadata(template issueform.Template) string {
351351+ metadata := bodyMetadata{Fields: make([]bodyMetadataField, 0, len(template.Fields))}
352352+ for _, field := range template.EditableFields() {
353353+ if field.ID == "" || strings.TrimSpace(field.Label) == "" {
354354+ continue
355355+ }
356356+ metadata.Fields = append(metadata.Fields, bodyMetadataField{ID: field.ID, Label: field.Label})
336357 }
337337- return heading + "\n" + body
358358+ if len(metadata.Fields) == 0 {
359359+ return ""
360360+ }
361361+362362+ encoded, err := json.Marshal(metadata)
363363+ if err != nil {
364364+ return ""
365365+ }
366366+367367+ return "<!-- pad:fields:" + string(encoded) + " -->"
338368}
339369340370func renderCheckboxBody(field issueform.Field, checked bool) string {
···354384 current := parsedSection{}
355385 active := false
356386 allowedHeadings := collectAllowedHeadings(template)
387387+ metadataIDs := parseBodyMetadata(body)
357388358389 for _, line := range strings.Split(strings.ReplaceAll(body, "\r\n", "\n"), "\n") {
359390 heading, id, ok := parseHeading(line)
391391+ if ok && id == "" {
392392+ id = metadataIDs[normalizeHeading(heading)]
393393+ }
360394 if ok && shouldStartSection(heading, id, allowedHeadings) {
361395 if active {
362396 current.Body = strings.TrimSpace(current.Body)
···384418 }
385419386420 return sections
421421+}
422422+423423+func parseBodyMetadata(body string) map[string]string {
424424+ matches := bodyMetadataPattern.FindAllStringSubmatch(body, -1)
425425+ if len(matches) == 0 {
426426+ return nil
427427+ }
428428+429429+ metadata := bodyMetadata{}
430430+ if err := json.Unmarshal([]byte(matches[0][1]), &metadata); err != nil {
431431+ return nil
432432+ }
433433+434434+ idsByHeading := make(map[string]string, len(metadata.Fields))
435435+ for _, field := range metadata.Fields {
436436+ if field.ID == "" || strings.TrimSpace(field.Label) == "" {
437437+ continue
438438+ }
439439+ idsByHeading[normalizeHeading(field.Label)] = field.ID
440440+ }
441441+442442+ return idsByHeading
387443}
388444389445func collectAllowedHeadings(template issueform.Template) map[string]struct{} {
+37-11
internal/daily/entry_test.go
···111111 }
112112}
113113114114-func TestEntryFromIssueBodyUsesHiddenIDsWhenLabelsChange(t *testing.T) {
114114+func TestEntryFromIssueBodyUsesMetadataWhenLabelsChange(t *testing.T) {
115115+ template := mustTemplateWithRenamedYesterday(t)
116116+ body := `<!-- pad:fields:{"fields":[{"id":"yesterday","label":"Yesterday Work"},{"id":"today","label":"Current Focus"}]} -->
117117+118118+## Yesterday Work
119119+- Reviewed PR #42
120120+121121+## Current Focus
122122+- Continue feature work`
123123+124124+ got := EntryFromIssueBody("2026-04-17", template, body)
125125+126126+ if got.Text("yesterday") != "- Reviewed PR #42" {
127127+ t.Fatalf("unexpected yesterday %q", got.Text("yesterday"))
128128+ }
129129+130130+ if got.Text("today") != "- Continue feature work" {
131131+ t.Fatalf("unexpected today %q", got.Text("today"))
132132+ }
133133+}
134134+135135+func TestEntryFromIssueBodyStillParsesInlineIDsWhenLabelsChange(t *testing.T) {
115136 template := mustTemplateWithRenamedYesterday(t)
116137 body := `## Yesterday Work <!-- pad:id:yesterday -->
117138- Reviewed PR #42
···132153133154func TestEntryFromIssueBodyAddsCarryoverForRemovedFields(t *testing.T) {
134155 template := mustTemplateWithRenamedYesterday(t)
135135- body := `## Yesterday Work <!-- pad:id:yesterday -->
156156+ body := `<!-- pad:fields:{"fields":[{"id":"yesterday","label":"Yesterday Work"},{"id":"today","label":"Current Focus"},{"id":"comments","label":"💬 Additional Comments"}]} -->
157157+158158+## Yesterday Work
136159- Reviewed PR #42
137160138138-## Current Focus <!-- pad:id:today -->
161161+## Current Focus
139162- Continue feature work
140163141141-## 💬 Additional Comments <!-- pad:id:comments -->
164164+## 💬 Additional Comments
142165- Offline after 17:00`
143166144167 got := EntryFromIssueBody("2026-04-17", template, body)
···157180158181func TestEntryFromIssueBodyKeepsMarkdownHeadingsInsideResponseBody(t *testing.T) {
159182 template := mustTemplate(t)
160160- body := `## ✅ What did you do yesterday? <!-- pad:id:yesterday -->
183183+ body := `<!-- pad:fields:{"fields":[{"id":"yesterday","label":"✅ What did you do yesterday?"},{"id":"today","label":"🎯 What will you do today?"},{"id":"blockers","label":"🚧 Any blockers?"}]} -->
184184+185185+## ✅ What did you do yesterday?
161186- Reviewed PR #42
162187163163-## 🎯 What will you do today? <!-- pad:id:today -->
188188+## 🎯 What will you do today?
164189- Continue feature work
165190166191### Notes
167192- Include nested heading in response
168193169169-## 🚧 Any blockers? <!-- pad:id:blockers -->
194194+## 🚧 Any blockers?
170195_None._`
171196172197 got := EntryFromIssueBody("2026-04-17", template, body)
···176201 }
177202}
178203179179-func TestBodyRendersTemplateSectionsAndHiddenIDs(t *testing.T) {
204204+func TestBodyRendersTemplateSectionsAndMetadata(t *testing.T) {
180205 entry := New("2026-04-16", mustTemplate(t))
181206 entry.SetText("yesterday", "- Reviewed PR #42")
182207 entry.SetText("today", "- Continue feature work")
···184209 body := entry.Body()
185210186211 checks := []string{
212212+ `<!-- pad:fields:{"fields":[{"id":"yesterday","label":"✅ What did you do yesterday?"},{"id":"today","label":"🎯 What will you do today?"},{"id":"blockers","label":"🚧 Any blockers?"},{"id":"parking_lot","label":"🚨 Do you request a Parking Lot or escalation?"},{"id":"parking_details","label":"📝 Parking Lot Details"},{"id":"comments","label":"💬 Additional Comments"}]} -->`,
187213 "## Daily Standup Update",
188188- "### ✅ What did you do yesterday? <!-- pad:id:yesterday -->",
189189- "### 🎯 What will you do today? <!-- pad:id:today -->",
190190- "### 🚧 Any blockers? <!-- pad:id:blockers -->",
214214+ "### ✅ What did you do yesterday?",
215215+ "### 🎯 What will you do today?",
216216+ "### 🚧 Any blockers?",
191217 "_No response_",
192218 }
193219
+3-2
internal/tui/editor_test.go
···31313232 checks := []string{
3333 "[Daily Update] [2026/04/16]",
3434- "## ✅ What did you do yesterday? <!-- pad:id:yesterday -->",
3535- "## 🎯 What will you do today? <!-- pad:id:today -->",
3434+ `<!-- pad:fields:{"fields":[{"id":"yesterday","label":"✅ What did you do yesterday?"},{"id":"today","label":"🎯 What will you do today?"},{"id":"parking_lot","label":"🚨 Do you request a Parking Lot or escalation?"}]} -->`,
3535+ "### ✅ What did you do yesterday?",
3636+ "### 🎯 What will you do today?",
3637 }
37383839 for _, check := range checks {