@opendirectory.dev/skills 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,462 @@
1
+ ---
2
+ name: product-update-logger
3
+ description: "Tell the skill what your product shipped. It writes a polished dated entry to a living docs/changelog.md and produces a ready-to-use content package: tweet thread, LinkedIn post, email snippet, and one-liner."
4
+ ---
5
+
6
+ # product-update-logger
7
+
8
+ Tell this skill what your product shipped. It writes a polished changelog entry to `docs/changelog.md` (a living log, newest entry first) and simultaneously produces a content package: tweet thread, LinkedIn post, email snippet, and one-liner.
9
+
10
+ Input sources: free text from your message, git commits auto-read from the local repo, or GitHub PRs if you provide a repo. Any combination works.
11
+
12
+ ## Reference Files
13
+
14
+ Read these files before each run:
15
+
16
+ ```bash
17
+ cat references/changelog-format.md
18
+ cat references/content-rules.md
19
+ cat references/noise-filter.md
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Step 1: Setup Check
25
+
26
+ ```bash
27
+ echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set -- GitHub PR fetching disabled}"
28
+ echo "Git: $(git rev-parse --is-inside-work-tree 2>/dev/null && echo 'repo detected' || echo 'not a git repo')"
29
+ echo "Changelog: $(ls docs/changelog.md 2>/dev/null && echo 'exists' || echo 'will be created')"
30
+ ```
31
+
32
+ Note whether git is available and whether a changelog already exists. This determines the version label format.
33
+
34
+ ---
35
+
36
+ ## Step 2: Parse Input
37
+
38
+ Collect from the conversation:
39
+
40
+ - `items` -- free text description of what shipped (pipe-separated if multiple). Optional if git is available.
41
+ - `since` -- how many days back to look. Default: 7. User may say "last 2 weeks" (14) or "since last release."
42
+ - `repo` -- GitHub "owner/repo" for PR fetching. Optional.
43
+ - `version_label` -- custom label like "v2.1.0" or "The Speed Update." Optional; default is date-based.
44
+
45
+ **If the user said nothing about items AND there is no git repo:** Ask "What did you ship? List the features, fixes, or improvements -- one per line."
46
+
47
+ **If git is available and user said nothing specific:** Proceed with git auto-read in Step 3. Show the user what was found and confirm before transforming.
48
+
49
+ Write parsed input:
50
+
51
+ ```bash
52
+ python3 << 'PYEOF'
53
+ import json, os, re
54
+
55
+ inp = {
56
+ "items": "", # FILL: pipe-separated free text, or "" if none
57
+ "since": 7, # FILL: integer days
58
+ "repo": "", # FILL: "owner/repo" or ""
59
+ "version_label": "" # FILL: "" means auto (date-based), or custom string
60
+ }
61
+
62
+ with open("/tmp/pul-input.json", "w") as f:
63
+ json.dump(inp, f, indent=2)
64
+ print(f"Since: {inp['since']} days")
65
+ print(f"Free text items: {inp['items'] or 'none (will use git/GitHub)'}")
66
+ print(f"GitHub repo: {inp['repo'] or 'none'}")
67
+ print(f"Version label: {inp['version_label'] or 'auto (date-based)'}")
68
+ PYEOF
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Step 3: Run the Gather Script
74
+
75
+ ```bash
76
+ ls scripts/gather.py 2>/dev/null && echo "script found" || echo "ERROR: scripts/gather.py not found"
77
+ ```
78
+
79
+ ```bash
80
+ GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/gather.py \
81
+ --since "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['since'])")" \
82
+ --repo "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['repo'])")" \
83
+ --items "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['items'])")" \
84
+ --output /tmp/pul-raw.json
85
+ ```
86
+
87
+ Verify output:
88
+
89
+ ```bash
90
+ python3 -c "
91
+ import json
92
+ with open('/tmp/pul-raw.json') as f:
93
+ d = json.load(f)
94
+ print(f'Items found: {d[\"total_items\"]}')
95
+ print(f'Noise filtered: {d[\"noise_filtered\"]}')
96
+ print(f'Git available: {d[\"git_available\"]}')
97
+ print(f'GitHub available: {d[\"github_available\"]}')
98
+ print(f'Sources: git={sum(1 for i in d[\"items\"] if i[\"source\"]==\"git_commit\")}, '
99
+ f'prs={sum(1 for i in d[\"items\"] if i[\"source\"]==\"github_pr\")}, '
100
+ f'text={sum(1 for i in d[\"items\"] if i[\"source\"]==\"free_text\")}')
101
+ print()
102
+ print('Items:')
103
+ for item in d['items']:
104
+ print(f' [{item[\"source\"]}] {item[\"subject\"]}')
105
+ "
106
+ ```
107
+
108
+ **If total_items == 0:** Stop. Tell the user: "No shipped items found. Either describe what you shipped, point me to a git repo with recent commits, or add a GitHub repo with `repo: owner/repo` and a GITHUB_TOKEN."
109
+
110
+ **Show the item list to the user and ask: "These are the items I found. Anything to add or remove before I write the changelog?"**
111
+
112
+ Wait for confirmation or edits. If the user says "looks good", "proceed", or makes no changes, continue. If the user adds or removes items, update `/tmp/pul-raw.json` accordingly before Step 4.
113
+
114
+ ---
115
+
116
+ ## Step 4: Generate Changelog Entry
117
+
118
+ Print items for context:
119
+
120
+ ```bash
121
+ python3 -c "
122
+ import json
123
+ with open('/tmp/pul-raw.json') as f:
124
+ d = json.load(f)
125
+ print(json.dumps(d['items'], indent=2))
126
+ print()
127
+ print(f'Existing changelog format: {d[\"existing_changelog\"][\"format\"]}')
128
+ print(f'Last label: {d[\"existing_changelog\"][\"last_label\"]}')
129
+ print(f'Today: {d[\"date\"]}')
130
+ "
131
+ ```
132
+
133
+ **AI instructions:** Transform each raw item from technical language to user-facing benefit language. Follow `references/changelog-format.md` for transformation rules and examples.
134
+
135
+ Rules:
136
+ - **Do NOT invent outcomes or metrics.** "40% faster" must come from the source data. If no number is in the commit or PR, do not add one.
137
+ - **Use past tense:** "Added", "Fixed", "Improved" -- not "Adds", "Fixes"
138
+ - **Assign exactly one category** to each item: New, Improved, Fixed, or Under the hood
139
+ - **Under the hood:** Only include if developer-relevant (API changes, breaking changes). Omit empty sections.
140
+ - **Omit** anything that maps to: test changes, CI changes, documentation-only commits
141
+
142
+ Determine version label:
143
+ - If user specified one: use it exactly
144
+ - If `existing_changelog.format == "semver"`: increment based on changes (patch for fixes only, minor for any new feature)
145
+ - Default: `Week of [Month Day, Year]` using today's date
146
+
147
+ Write the entry to `/tmp/pul-entry.json`:
148
+
149
+ ```json
150
+ {
151
+ "label": "Week of April 23, 2026",
152
+ "date": "2026-04-23",
153
+ "new": [
154
+ {"title": "Dark mode", "description": "Toggle in Settings > Appearance. Works across all views."}
155
+ ],
156
+ "improved": [
157
+ {"title": "API response time", "description": "40% faster on average. Dashboard now loads in under 1 second."}
158
+ ],
159
+ "fixed": [
160
+ {"title": "CSV export", "description": "Exports no longer drop the last row."}
161
+ ],
162
+ "under_the_hood": []
163
+ }
164
+ ```
165
+
166
+ Verify the entry:
167
+
168
+ ```bash
169
+ python3 -c "
170
+ import json
171
+ with open('/tmp/pul-entry.json') as f:
172
+ e = json.load(f)
173
+ print(f'Label: {e[\"label\"]}')
174
+ total = 0
175
+ for cat in ['new', 'improved', 'fixed', 'under_the_hood']:
176
+ items = e.get(cat, [])
177
+ if items:
178
+ print(f'{cat.replace(\"_\", \" \").title()} ({len(items)}):')
179
+ for item in items:
180
+ print(f' - {item[\"title\"]}: {item[\"description\"]}')
181
+ total += len(items)
182
+ print(f'Total: {total} items')
183
+ "
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Step 5: Generate Content Package
189
+
190
+ Using the changelog entry from Step 4, generate all four content pieces. Follow `references/content-rules.md` strictly.
191
+
192
+ **One-liner** (max 20 words): One sentence covering the biggest 1-2 items. Plain language, no jargon.
193
+
194
+ **Tweet thread** (3-5 tweets):
195
+ - Tweet 1: Hook -- "We shipped [N] things this week." or lead with the biggest feature
196
+ - Tweets 2-N: One item per tweet, 1-2 sentences max
197
+ - Last tweet: "Changelog: [link]" or "More next week." (optional)
198
+ - Each tweet strictly under 280 characters
199
+ - No hashtags. No em dashes. Active voice.
200
+
201
+ **LinkedIn post**:
202
+ - No markdown (asterisks render as literal on LinkedIn)
203
+ - No hashtags
204
+ - Founder voice: "We shipped", not "We are excited to announce"
205
+ - Short paragraphs (1-2 sentences each), blank lines between them
206
+ - Close with a question or observation, not a CTA
207
+ - 150-400 words total
208
+
209
+ **Email snippet**:
210
+ - Subject: "What shipped this week: [biggest item] + [1 more]"
211
+ - Body: 50-100 words. "Here's what we shipped this week:" then bullets.
212
+
213
+ Write to `/tmp/pul-content.json`:
214
+
215
+ ```json
216
+ {
217
+ "one_liner": "Dark mode, faster API, and a fixed export bug.",
218
+ "tweet_thread": [
219
+ "We shipped 3 things this week.",
220
+ "Dark mode is live. Toggle it in Settings > Appearance. Works everywhere.",
221
+ "API response time is now 40% faster. Dashboard loads in under a second.",
222
+ "Fixed: CSV exports were dropping the last row. That's gone now.",
223
+ "Changelog: [link]"
224
+ ],
225
+ "linkedin_post": "We shipped 3 updates this week.\n\nDark mode is live. Toggle it in Settings under Appearance. It works across every view.\n\nAPI response time is 40% faster on average. The dashboard now loads in under a second for most users.\n\nWe also fixed a bug where CSV exports were silently dropping the last row. If you hit this and stopped exporting, it's worth trying again.\n\nWhat feature have you been waiting for?",
226
+ "email_snippet": {
227
+ "subject": "What shipped this week: dark mode + faster API",
228
+ "body": "Here's what we shipped this week:\n\n- Dark mode: toggle in Settings > Appearance\n- API response time: 40% faster, dashboard loads under 1 second\n- Fixed: CSV exports no longer drop the last row\n\nFull changelog below."
229
+ }
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Step 6: Self-QA
236
+
237
+ ```bash
238
+ python3 -c "
239
+ import json, re
240
+
241
+ with open('/tmp/pul-raw.json') as f:
242
+ raw = json.load(f)
243
+ with open('/tmp/pul-entry.json') as f:
244
+ entry = json.load(f)
245
+ with open('/tmp/pul-content.json') as f:
246
+ content = json.load(f)
247
+
248
+ full_text = json.dumps(entry) + json.dumps(content)
249
+ fails = 0
250
+
251
+ # Check 1: No em dashes
252
+ if chr(8212) in full_text:
253
+ print('FAIL: em dash found -- replace with hyphen')
254
+ fails += 1
255
+ else:
256
+ print('PASS: no em dashes')
257
+
258
+ # Check 2: Banned words
259
+ banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
260
+ 'streamline', 'leverage', 'transform', 'revolutionize', 'excited to announce',
261
+ 'pleased to announce', 'we are thrilled', 'cutting-edge', 'best-in-class',
262
+ 'world-class', 'unlock', 'delightful']
263
+ found = [w for w in banned if w.lower() in full_text.lower()]
264
+ if found:
265
+ print(f'FAIL: banned words found: {found}')
266
+ fails += 1
267
+ else:
268
+ print('PASS: no banned words')
269
+
270
+ # Check 3: Tweet length
271
+ thread = content.get('tweet_thread', [])
272
+ long_tweets = [(i+1, len(t)) for i, t in enumerate(thread) if len(t) > 280]
273
+ if long_tweets:
274
+ print(f'FAIL: tweets over 280 chars: {long_tweets}')
275
+ fails += 1
276
+ else:
277
+ print(f'PASS: all {len(thread)} tweets under 280 chars')
278
+
279
+ # Check 4: LinkedIn no hashtags
280
+ li = content.get('linkedin_post', '')
281
+ if re.search(r'#[A-Za-z]', li):
282
+ print('FAIL: hashtags found in LinkedIn post')
283
+ fails += 1
284
+ else:
285
+ print('PASS: no hashtags in LinkedIn')
286
+
287
+ # Check 5: No markdown in LinkedIn
288
+ if '**' in li or '__' in li:
289
+ print('FAIL: markdown formatting in LinkedIn (renders as literal asterisks)')
290
+ fails += 1
291
+ else:
292
+ print('PASS: no markdown in LinkedIn')
293
+
294
+ # Check 6: One-liner word count
295
+ one_liner = content.get('one_liner', '')
296
+ word_count = len(one_liner.split())
297
+ if word_count > 20:
298
+ print(f'FAIL: one-liner is {word_count} words (max 20)')
299
+ fails += 1
300
+ else:
301
+ print(f'PASS: one-liner is {word_count} words')
302
+
303
+ # Check 7: Item count
304
+ entry_items = (len(entry.get('new', [])) + len(entry.get('improved', [])) +
305
+ len(entry.get('fixed', [])) + len(entry.get('under_the_hood', [])))
306
+ raw_total = raw['total_items']
307
+ print(f'INFO: {entry_items} changelog items from {raw_total} raw items')
308
+
309
+ print()
310
+ print(f'Result: {\"PASS\" if fails == 0 else f\"FAIL ({fails} issues)\"}')
311
+ "
312
+ ```
313
+
314
+ **If any check fails:** Fix the issue in the relevant temp file before proceeding to Step 7. Re-run the check after fixing.
315
+
316
+ ---
317
+
318
+ ## Step 7: Append to Changelog + Save Content
319
+
320
+ ```bash
321
+ python3 << 'PYEOF'
322
+ import json, os, re
323
+
324
+ with open('/tmp/pul-entry.json') as f:
325
+ entry = json.load(f)
326
+ with open('/tmp/pul-content.json') as f:
327
+ content = json.load(f)
328
+
329
+ # Build the new changelog section
330
+ lines = [f"## {entry['label']}", ""]
331
+
332
+ CAT_HEADERS = {
333
+ "new": "### New",
334
+ "improved": "### Improved",
335
+ "fixed": "### Fixed",
336
+ "under_the_hood": "### Under the hood",
337
+ }
338
+
339
+ for cat, header in CAT_HEADERS.items():
340
+ items = entry.get(cat, [])
341
+ if items:
342
+ lines.append(header)
343
+ for item in items:
344
+ lines.append(f"- **{item['title']}** -- {item['description']}")
345
+ lines.append("")
346
+
347
+ lines.append("---")
348
+ lines.append("")
349
+ new_section = "\n".join(lines)
350
+
351
+ # Prepend to docs/changelog.md
352
+ os.makedirs("docs", exist_ok=True)
353
+ changelog_path = "docs/changelog.md"
354
+
355
+ if os.path.exists(changelog_path):
356
+ existing = open(changelog_path).read()
357
+ # Insert after the top-level heading (if any) or at the very top
358
+ if existing.startswith("# "):
359
+ end_of_heading = existing.index("\n") + 1
360
+ updated = existing[:end_of_heading] + "\n" + new_section + existing[end_of_heading:]
361
+ else:
362
+ updated = new_section + existing
363
+ else:
364
+ updated = "# Changelog\n\n" + new_section
365
+
366
+ with open(changelog_path, "w") as f:
367
+ f.write(updated)
368
+
369
+ print(f"Changelog updated: {changelog_path}")
370
+
371
+ # Save content package
372
+ date = entry['date']
373
+ content_dir = "docs/product-updates"
374
+ os.makedirs(content_dir, exist_ok=True)
375
+ content_path = f"{content_dir}/{date}-content.md"
376
+
377
+ content_lines = [
378
+ f"# Content Package: {entry['label']}",
379
+ "",
380
+ "## One-liner",
381
+ content.get('one_liner', ''),
382
+ "",
383
+ "## Tweet Thread",
384
+ "",
385
+ ]
386
+ thread = content.get('tweet_thread', [])
387
+ for i, tweet in enumerate(thread, 1):
388
+ content_lines.append(f"[{i}/{len(thread)}] {tweet}")
389
+ content_lines.append("")
390
+
391
+ content_lines += [
392
+ "## LinkedIn Post",
393
+ "",
394
+ content.get('linkedin_post', ''),
395
+ "",
396
+ "## Email Snippet",
397
+ "",
398
+ f"Subject: {content.get('email_snippet', {}).get('subject', '')}",
399
+ "",
400
+ content.get('email_snippet', {}).get('body', ''),
401
+ "",
402
+ ]
403
+
404
+ with open(content_path, "w") as f:
405
+ f.write("\n".join(content_lines))
406
+
407
+ print(f"Content package: {content_path}")
408
+ PYEOF
409
+ ```
410
+
411
+ ---
412
+
413
+ ## Step 8: Clean Up and Present
414
+
415
+ ```bash
416
+ rm -f /tmp/pul-input.json /tmp/pul-raw.json /tmp/pul-entry.json /tmp/pul-content.json
417
+ echo "Done."
418
+ ```
419
+
420
+ Present to the user in this order:
421
+
422
+ **1. Changelog entry** (formatted markdown, not raw JSON):
423
+
424
+ ```
425
+ ## Week of April 23, 2026
426
+
427
+ ### New
428
+ - **Dark mode** -- Toggle in Settings > Appearance. Works across all views.
429
+
430
+ ### Improved
431
+ - **API response time** -- 40% faster on average. Dashboard now loads in under 1 second.
432
+
433
+ ### Fixed
434
+ - **CSV export** -- Exports no longer drop the last row.
435
+ ```
436
+
437
+ **2. Content package:**
438
+
439
+ - One-liner: `[text]`
440
+ - Tweet thread: numbered list of tweets
441
+ - LinkedIn post: full text
442
+ - Email snippet: subject line + body
443
+
444
+ **3. Saved files:**
445
+ - `docs/changelog.md` -- updated (new entry prepended)
446
+ - `docs/product-updates/[date]-content.md` -- full content package saved
447
+
448
+ ---
449
+
450
+ ## Common Mistakes
451
+
452
+ | The agent will want to... | Why that's wrong |
453
+ |---|---|
454
+ | Invent outcomes or metrics | Every claim must come from the raw items. "40% faster" needs to come from the commit message or PR body. If no number is present, don't add one. |
455
+ | Write "We are excited to announce" | Banned. Use "We shipped", "[Feature] is now live", or just state the fact. |
456
+ | Use markdown bold (**) in LinkedIn | LinkedIn renders ** as literal asterisks. Plain text only. |
457
+ | Add hashtags to LinkedIn or tweets | This skill never uses hashtags. |
458
+ | Put all items in "New" | Bugs are Fixed, speed improvements are Improved. Miscategorizing weakens the changelog. |
459
+ | Skip the confirmation step in Step 3 | Always show the item list and ask the user to confirm before transforming. This prevents wrong-branch commits or stale items. |
460
+ | Include empty "Under the hood" section | Omit if empty. Silence is better than noise. |
461
+ | Combine multiple items into one tweet | One item per tweet. Specificity > breadth. |
462
+ | Pad with filler tweets | If there's one real item, write 2 tweets. Don't pad to 5. |
@@ -0,0 +1,119 @@
1
+ [
2
+ {
3
+ "id": "eval_001",
4
+ "name": "Free text only -- happy path",
5
+ "description": "User provides 3 items as free text. Expects all categories populated, clean content output.",
6
+ "input": {
7
+ "items": "Added dark mode|Fixed CSV export bug|Improved API speed by 40%",
8
+ "since": 7,
9
+ "repo": ""
10
+ },
11
+ "expected": {
12
+ "changelog_entry_has_categories": ["new", "improved", "fixed"],
13
+ "tweet_thread_count_min": 3,
14
+ "tweet_thread_count_max": 5,
15
+ "all_tweets_under_280_chars": true,
16
+ "linkedin_no_hashtags": true,
17
+ "linkedin_no_markdown": true,
18
+ "no_em_dashes": true,
19
+ "no_banned_words": true,
20
+ "no_invented_metrics": true,
21
+ "one_liner_under_20_words": true
22
+ }
23
+ },
24
+ {
25
+ "id": "eval_002",
26
+ "name": "Git commits only -- noise filtered",
27
+ "description": "No free text. Git log has 4 commits: 2 real, 2 noise (merge + version bump). Expects noise filtered.",
28
+ "input": {
29
+ "items": "",
30
+ "since": 7,
31
+ "repo": ""
32
+ },
33
+ "git_log_mock": [
34
+ "abc1234|Add user avatar upload|Users can now upload a profile picture|2026-04-21",
35
+ "def4567|Merge pull request #45 from user/feature||2026-04-21",
36
+ "ghi7890|bump version to 1.3.2||2026-04-20",
37
+ "jkl0123|Fix timezone bug in scheduler|Events were firing 1 hour early in non-UTC timezones|2026-04-19"
38
+ ],
39
+ "expected": {
40
+ "noise_filtered_count": 2,
41
+ "items_remaining": 2,
42
+ "changelog_has_new": true,
43
+ "changelog_has_fixed": true,
44
+ "merge_commit_not_in_output": true,
45
+ "version_bump_not_in_output": true
46
+ }
47
+ },
48
+ {
49
+ "id": "eval_003",
50
+ "name": "GitHub PRs as source",
51
+ "description": "GITHUB_TOKEN set, repo provided. Expects PRs to be fetched and used as source. PR body used for context.",
52
+ "input": {
53
+ "items": "",
54
+ "since": 7,
55
+ "repo": "acme/dashboard"
56
+ },
57
+ "mock_prs": [
58
+ {
59
+ "number": 47,
60
+ "title": "Fix CSV export dropping last row",
61
+ "body": "Regression from v1.3.1. Off-by-one error in row iterator. Fixes #102.",
62
+ "merged_at": "2026-04-22T10:30:00Z",
63
+ "labels": ["bug"]
64
+ },
65
+ {
66
+ "number": 48,
67
+ "title": "Add bulk delete to contacts list",
68
+ "body": "Users can now select multiple contacts and delete them at once.",
69
+ "merged_at": "2026-04-21T09:00:00Z",
70
+ "labels": []
71
+ }
72
+ ],
73
+ "expected": {
74
+ "github_available": true,
75
+ "pr_titles_used_as_source": true,
76
+ "pr_body_used_for_context": true,
77
+ "changelog_has_new": true,
78
+ "changelog_has_fixed": true,
79
+ "regression_context_preserved": true
80
+ }
81
+ },
82
+ {
83
+ "id": "eval_004",
84
+ "name": "Single item -- minimal input",
85
+ "description": "User provides one item. Expects no invented features, concise thread, one-liner under 20 words.",
86
+ "input": {
87
+ "items": "Fixed the login page loading slowly on mobile",
88
+ "since": 7,
89
+ "repo": ""
90
+ },
91
+ "expected": {
92
+ "changelog_entry_has_fixed": true,
93
+ "changelog_entry_has_new": false,
94
+ "tweet_thread_count_min": 2,
95
+ "tweet_thread_count_max": 3,
96
+ "one_liner_under_20_words": true,
97
+ "no_invented_additional_features": true,
98
+ "linkedin_mentions_mobile": true
99
+ }
100
+ },
101
+ {
102
+ "id": "eval_005",
103
+ "name": "Existing changelog -- correct prepend",
104
+ "description": "docs/changelog.md already exists with a prior entry. New run should prepend, not append. Old entry preserved.",
105
+ "input": {
106
+ "items": "Launched API v2|Deprecated old auth endpoint",
107
+ "since": 7,
108
+ "repo": ""
109
+ },
110
+ "existing_changelog": "# Changelog\n\n## Week of April 14, 2026\n\n### New\n- **Dashboard** -- New analytics dashboard with weekly summaries.\n\n---\n",
111
+ "expected": {
112
+ "new_entry_is_prepended_not_appended": true,
113
+ "existing_entry_still_present": true,
114
+ "new_label_not_equal_to_april_14": true,
115
+ "changelog_heading_preserved": true,
116
+ "api_v2_in_new_section": true
117
+ }
118
+ }
119
+ ]
@@ -0,0 +1,96 @@
1
+ # Changelog Format Reference
2
+
3
+ ## Entry Structure
4
+
5
+ ```markdown
6
+ ## Week of April 23, 2026
7
+
8
+ ### New
9
+ - **Feature name** -- One sentence describing what users can now do.
10
+
11
+ ### Improved
12
+ - **Feature name** -- One sentence describing how the experience changed.
13
+
14
+ ### Fixed
15
+ - **Feature name** -- One sentence describing what was broken and now works.
16
+
17
+ ### Under the hood
18
+ - **Component** -- One sentence for developer-relevant changes only.
19
+
20
+ ---
21
+ ```
22
+
23
+ The `---` separator goes at the bottom of each entry. Entries are prepended, so the most recent is always at the top of the file.
24
+
25
+ ## Category Decision Rules
26
+
27
+ **New**: Use when the change adds net-new capability a user could not do before.
28
+ - New page, new API endpoint, new integration, new toggle, new export format
29
+ - If in doubt: can users do something they couldn't do yesterday? -> New
30
+
31
+ **Improved**: Use when an existing feature is made faster, easier, more reliable, or more capable.
32
+ - "Now 40% faster", "Works on mobile", "Added bulk actions to existing list view"
33
+ - Performance improvements that users will notice -> Improved
34
+ - Security patches the user doesn't see -> Under the hood
35
+
36
+ **Fixed**: Something was broken and now works. Bug reports, regression fixes, crash fixes.
37
+ - Prefer specificity: "Fixed: CSV exports no longer drop the last row"
38
+ - If the user never knew it was broken, it can be Improved or omitted
39
+
40
+ **Under the hood**: Infrastructure, dependency updates, refactoring, build changes.
41
+ - Include only if developer-relevant: API changes, breaking changes, new environment requirements
42
+ - Omit if the user wouldn't care
43
+ - Never pad this section -- empty is fine
44
+
45
+ ## Version Label Rules
46
+
47
+ **Default (date-based):**
48
+ - `Week of [Month Day, Year]` -- e.g., "Week of April 23, 2026"
49
+ - Use when no semver is detected in existing changelog and user didn't specify
50
+
51
+ **Semver (auto-detected):**
52
+ - If existing changelog headings match `v1.2.3` or `1.2.3` format -> use semver
53
+ - Patch bump (x.x.+1): only if all changes are Fixed
54
+ - Minor bump (x.+1.0): if any New feature is included
55
+ - Show the bumped version: `v1.4.0`
56
+
57
+ **Custom override:**
58
+ - If user says "call it 'The Speed Update'" or "v2.1.0" -> use exactly that
59
+ - Do not auto-format custom labels
60
+
61
+ ## Transformation Examples
62
+
63
+ | Raw (commit/PR) | User-facing (changelog) |
64
+ |---|---|
65
+ | `Fix memory leak in worker process` | **Worker stability** -- The app no longer slows down after extended use |
66
+ | `Add CSV export to reports page` | **Export reports** -- Download any report as a CSV from the Reports page |
67
+ | `Improve query performance with index` | **Faster search** -- Search results now load in under half a second |
68
+ | `Add dark mode toggle in Settings` | **Dark mode** -- Toggle it in Settings > Appearance. Works across all views |
69
+ | `Fix timezone bug in scheduler` | **Scheduler** -- Events no longer fire an hour early in non-UTC timezones |
70
+ | `Update dependencies to latest` | (omit -- not user-relevant) |
71
+ | `Refactor auth middleware` | (omit unless breaking change) |
72
+ | `Add rate limiting to API` | **API rate limits** -- Documented in the API reference. 1000 req/min per key |
73
+ | `Fix login page slow on mobile` | **Mobile login** -- The login page now loads instantly on mobile devices |
74
+ | `Migrate to Postgres 16` | **Database upgrade** -- Postgres 16 with improved connection pooling |
75
+
76
+ ## What to Omit
77
+
78
+ - Test file changes
79
+ - CI/CD pipeline changes
80
+ - Dependency bumps (unless security-relevant)
81
+ - Documentation-only commits
82
+ - Version bump commits
83
+ - Merge commits
84
+ - Commit messages under 8 characters
85
+
86
+ ## Item Format
87
+
88
+ ```
89
+ **[Feature name]** -- [one sentence benefit, no jargon]
90
+ ```
91
+
92
+ - Feature name: 1-3 words, title case, no verbs ("Dark mode" not "Added dark mode")
93
+ - Benefit: past tense ("Added", "Fixed", "Improved")
94
+ - No period at end
95
+ - No nested bullets
96
+ - No links (links belong in the full changelog page, not the entry)