@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.
- package/package.json +1 -1
- package/registry.json +24 -2
- package/skills/map-your-market/SKILL.md +1 -9
- package/skills/product-update-logger/.env.example +4 -0
- package/skills/product-update-logger/README.md +197 -0
- package/skills/product-update-logger/SKILL.md +462 -0
- package/skills/product-update-logger/evals/evals.json +119 -0
- package/skills/product-update-logger/references/changelog-format.md +96 -0
- package/skills/product-update-logger/references/content-rules.md +154 -0
- package/skills/product-update-logger/references/noise-filter.md +86 -0
- package/skills/product-update-logger/scripts/gather.py +364 -0
- package/skills/where-your-customer-lives/.env.example +4 -0
- package/skills/where-your-customer-lives/README.md +135 -0
- package/skills/where-your-customer-lives/SKILL.md +515 -0
- package/skills/where-your-customer-lives/evals/evals.json +108 -0
- package/skills/where-your-customer-lives/references/channel-types.md +147 -0
- package/skills/where-your-customer-lives/references/entry-tactics.md +179 -0
- package/skills/where-your-customer-lives/references/scoring-guide.md +141 -0
- package/skills/where-your-customer-lives/scripts/fetch.py +810 -0
|
@@ -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)
|