@opendirectory.dev/skills 0.1.39 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendirectory.dev/skills",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "bin": {
package/registry.json CHANGED
@@ -252,6 +252,14 @@
252
252
  "version": "1.0.0",
253
253
  "path": "skills/pricing-page-psychology-audit"
254
254
  },
255
+ {
256
+ "name": "product-update-logger",
257
+ "description": "Tell the skill what your product shipped.",
258
+ "tags": [],
259
+ "author": "opendirectory",
260
+ "version": "0.0.1",
261
+ "path": "skills/product-update-logger"
262
+ },
255
263
  {
256
264
  "name": "producthunt-launch-kit",
257
265
  "description": "Generate every asset you need for a Product Hunt launch: listing copy, maker comment, and day-one social posts.",
@@ -0,0 +1,4 @@
1
+ GITHUB_TOKEN= # optional -- github.com/settings/tokens (no scopes needed for public repos)
2
+ # Without it: free text + git commits work fully
3
+ # With it: GitHub PR fetching enabled (richer context from PR bodies)
4
+ # Core changelog generation works without any token
@@ -0,0 +1,197 @@
1
+ # product-update-logger
2
+
3
+ Tell the skill what your product shipped. It writes a polished, living `docs/changelog.md` entry and hands you a ready-to-use content package: tweet thread, LinkedIn post, email snippet, and one-liner.
4
+
5
+ Run it after every deploy. Over quarters, `docs/changelog.md` becomes a complete, shareable product history.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npx "@opendirectory.dev/skills" install product-update-logger --target claude
11
+ ```
12
+
13
+ ### Video Tutorial
14
+ Watch this quick video to see how it's done:
15
+
16
+ https://github.com/user-attachments/assets/ee98a1b5-ebc4-452f-bbfb-c434f2935067
17
+
18
+ ### Step 1: Download the skill from GitHub
19
+ 1. Click the **Code** button on this repo's GitHub page.
20
+ 2. Select **Download ZIP** to download the repository.
21
+ 3. Extract the ZIP file on your computer.
22
+
23
+ ### Step 2: Install the Skill in Claude
24
+ 1. Open your **Claude desktop app**.
25
+ 2. Go to the sidebar on the left side and click on the **Customize** section.
26
+ 3. Click on the **Skills** tab, then click on the **+** (plus) icon button to create a new skill.
27
+ 4. Choose the option to **Upload a skill**, and drag and drop the `.zip` file (or you can extract it and drop the folder, both work).
28
+
29
+ > **Note:** For some skills (like `position-me`), the `SKILL.md` file might be located inside a subfolder. Always make sure you are uploading the specific folder that contains the `SKILL.md` file!
30
+
31
+ ## What It Does
32
+
33
+ - Accepts: free text items, git commits (auto-read), GitHub PRs -- any combination
34
+ - Filters noise: merge commits, version bumps, CI/CD, typos excluded automatically
35
+ - Transforms: technical commit language -> user-facing benefit language
36
+ - Categorizes: New / Improved / Fixed / Under the hood
37
+ - Generates a content package: one-liner, tweet thread, LinkedIn post, email snippet
38
+ - Appends a new dated entry to `docs/changelog.md` (living log, newest entry first)
39
+ - Saves the content package to `docs/product-updates/[date]-content.md`
40
+
41
+ ## Requirements
42
+
43
+ | Requirement | Purpose | How to Set Up |
44
+ |---|---|---|
45
+ | GITHUB_TOKEN | Optional -- enables GitHub PR fetching for richer context | github.com/settings/tokens (no scopes needed) |
46
+
47
+ No other API keys required. Free text + git auto-read work with no configuration.
48
+
49
+ ## Setup
50
+
51
+ ```bash
52
+ cp .env.example .env
53
+ # Add GITHUB_TOKEN if you want to pull from GitHub PRs
54
+ ```
55
+
56
+ ## How to Use
57
+
58
+ ```
59
+ "We shipped dark mode and fixed the export bug this week."
60
+ "Log my product updates. I'm in a git repo."
61
+ "We launched 3 things: dark mode, faster search, fixed mobile login."
62
+ "Log updates from last 14 days. Repo: acme/dashboard"
63
+ "We shipped the onboarding redesign. Version: v2.1.0"
64
+ ```
65
+
66
+ Include competitor names for richer channel discovery. Include ICP role + pain for accurate signal-tracing.
67
+
68
+ ## What Gets Generated
69
+
70
+ ### docs/changelog.md (living log)
71
+
72
+ ```markdown
73
+ # Changelog
74
+
75
+ ## Week of April 23, 2026
76
+
77
+ ### New
78
+ - **Dark mode** -- Toggle in Settings > Appearance. Works across all views.
79
+
80
+ ### Improved
81
+ - **Search** -- Results now load in under half a second.
82
+
83
+ ### Fixed
84
+ - **CSV export** -- Exports no longer drop the last row.
85
+
86
+ ---
87
+
88
+ ## Week of April 14, 2026
89
+ ...
90
+ ```
91
+
92
+ ### docs/product-updates/[date]-content.md (content package)
93
+
94
+ ```
95
+ ## One-liner
96
+ Dark mode, faster search, and a fix for the export bug.
97
+
98
+ ## Tweet Thread
99
+ [1/4] We shipped 3 things this week.
100
+ [2/4] Dark mode is live. Toggle it in Settings > Appearance.
101
+ ...
102
+
103
+ ## LinkedIn Post
104
+ We shipped 3 updates this week.
105
+ ...
106
+
107
+ ## Email Snippet
108
+ Subject: What shipped this week: dark mode + faster search
109
+ ...
110
+ ```
111
+
112
+ ## Input Sources
113
+
114
+ The skill pulls from all three sources automatically:
115
+
116
+ | Source | When used | How to trigger |
117
+ |---|---|---|
118
+ | Free text | Always first | Just describe what shipped in your message |
119
+ | Git commits | Auto-read when in a git repo | Run the skill from your project directory |
120
+ | GitHub PRs | When GITHUB_TOKEN + repo provided | Mention "repo: owner/repo" in your message |
121
+
122
+ All three can be combined. Overlapping items are deduplicated.
123
+
124
+ ## Changelog Entry Format
125
+
126
+ Each entry follows a consistent structure:
127
+
128
+ ```
129
+ ## [Version label]
130
+
131
+ ### New
132
+ - **Feature** -- Benefit sentence.
133
+
134
+ ### Improved
135
+ - **Feature** -- Benefit sentence.
136
+
137
+ ### Fixed
138
+ - **Feature** -- What was broken, now fixed.
139
+
140
+ ### Under the hood
141
+ - **Component** -- Developer-relevant change only.
142
+ ```
143
+
144
+ Version labels default to `Week of [Month Day, Year]`. Detected semver format is preserved and incremented automatically.
145
+
146
+ ## Content Rules
147
+
148
+ - **Tweets**: Under 280 chars each, no hashtags, no em dashes, active voice
149
+ - **LinkedIn**: No markdown formatting, no hashtags, founder voice ("We shipped" not "We are excited to announce")
150
+ - **Email**: 50-100 word body, subject line formula, casual and direct
151
+ - **One-liner**: Max 20 words, covers top 1-2 items
152
+
153
+ ## Standalone Script
154
+
155
+ Run data collection without Claude. Useful for raw item discovery before analysis.
156
+
157
+ ```bash
158
+ # Git auto-read
159
+ python3 scripts/gather.py --since 7 --output /tmp/pul-raw.json
160
+
161
+ # Free text items
162
+ python3 scripts/gather.py --items "Add dark mode|Fix CSV bug" --output /tmp/pul-raw.json
163
+
164
+ # With GitHub PRs
165
+ GITHUB_TOKEN=your_token python3 scripts/gather.py --repo owner/repo --since 14
166
+
167
+ # Print to stdout
168
+ python3 scripts/gather.py --since 7 --stdout | jq '.items'
169
+ ```
170
+
171
+ ## Project Structure
172
+
173
+ ```
174
+ product-update-logger/
175
+ ├── SKILL.md
176
+ ├── README.md
177
+ ├── .env.example
178
+ ├── scripts/
179
+ │ └── gather.py git + GitHub + free text collector
180
+ ├── evals/
181
+ │ └── evals.json 5 test cases
182
+ └── references/
183
+ ├── changelog-format.md entry structure, category rules, transformation examples
184
+ ├── content-rules.md tweet/LinkedIn/email writing rules + banned words
185
+ └── noise-filter.md git commit patterns to skip
186
+ ```
187
+
188
+ ## Cost Per Run
189
+
190
+ - Git, free text: free, no auth
191
+ - GitHub PRs: free with optional token
192
+ - AI analysis: uses the model running the skill
193
+ - Total: free
194
+
195
+ ## License
196
+
197
+ MIT
@@ -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. |