@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 +1 -1
- package/registry.json +8 -0
- 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/package.json
CHANGED
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,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. |
|