Skillquality 0.50

product-update-logger

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.

Price
free
Protocol
skill
Verified
no

What it does

product-update-logger

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.

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.

Reference Files

Read these files before each run:

cat references/changelog-format.md
cat references/content-rules.md
cat references/noise-filter.md

Step 1: Setup Check

echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set -- GitHub PR fetching disabled}"
echo "Git:          $(git rev-parse --is-inside-work-tree 2>/dev/null && echo 'repo detected' || echo 'not a git repo')"
echo "Changelog:    $(ls docs/changelog.md 2>/dev/null && echo 'exists' || echo 'will be created')"

Note whether git is available and whether a changelog already exists. This determines the version label format.


Step 2: Parse Input

Collect from the conversation:

  • items -- free text description of what shipped (pipe-separated if multiple). Optional if git is available.
  • since -- how many days back to look. Default: 7. User may say "last 2 weeks" (14) or "since last release."
  • repo -- GitHub "owner/repo" for PR fetching. Optional.
  • version_label -- custom label like "v2.1.0" or "The Speed Update." Optional; default is date-based.

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."

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.

Write parsed input:

python3 << 'PYEOF'
import json, os, re

inp = {
    "items": "",          # FILL: pipe-separated free text, or "" if none
    "since": 7,           # FILL: integer days
    "repo": "",           # FILL: "owner/repo" or ""
    "version_label": ""   # FILL: "" means auto (date-based), or custom string
}

with open("/tmp/pul-input.json", "w") as f:
    json.dump(inp, f, indent=2)
print(f"Since: {inp['since']} days")
print(f"Free text items: {inp['items'] or 'none (will use git/GitHub)'}")
print(f"GitHub repo: {inp['repo'] or 'none'}")
print(f"Version label: {inp['version_label'] or 'auto (date-based)'}")
PYEOF

Step 3: Run the Gather Script

ls scripts/gather.py 2>/dev/null && echo "script found" || echo "ERROR: scripts/gather.py not found"
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/gather.py \
    --since "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['since'])")" \
    --repo "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['repo'])")" \
    --items "$(python3 -c "import json; print(json.load(open('/tmp/pul-input.json'))['items'])")" \
    --output /tmp/pul-raw.json

Verify output:

python3 -c "
import json
with open('/tmp/pul-raw.json') as f:
    d = json.load(f)
print(f'Items found:      {d[\"total_items\"]}')
print(f'Noise filtered:   {d[\"noise_filtered\"]}')
print(f'Git available:    {d[\"git_available\"]}')
print(f'GitHub available: {d[\"github_available\"]}')
print(f'Sources: git={sum(1 for i in d[\"items\"] if i[\"source\"]==\"git_commit\")}, '
      f'prs={sum(1 for i in d[\"items\"] if i[\"source\"]==\"github_pr\")}, '
      f'text={sum(1 for i in d[\"items\"] if i[\"source\"]==\"free_text\")}')
print()
print('Items:')
for item in d['items']:
    print(f'  [{item[\"source\"]}] {item[\"subject\"]}')
"

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."

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?"

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.


Step 4: Generate Changelog Entry

Print items for context:

python3 -c "
import json
with open('/tmp/pul-raw.json') as f:
    d = json.load(f)
print(json.dumps(d['items'], indent=2))
print()
print(f'Existing changelog format: {d[\"existing_changelog\"][\"format\"]}')
print(f'Last label: {d[\"existing_changelog\"][\"last_label\"]}')
print(f'Today: {d[\"date\"]}')
"

AI instructions: Transform each raw item from technical language to user-facing benefit language. Follow references/changelog-format.md for transformation rules and examples.

Rules:

  • 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.
  • Use past tense: "Added", "Fixed", "Improved" -- not "Adds", "Fixes"
  • Assign exactly one category to each item: New, Improved, Fixed, or Under the hood
  • Under the hood: Only include if developer-relevant (API changes, breaking changes). Omit empty sections.
  • Omit anything that maps to: test changes, CI changes, documentation-only commits

Determine version label:

  • If user specified one: use it exactly
  • If existing_changelog.format == "semver": increment based on changes (patch for fixes only, minor for any new feature)
  • Default: Week of [Month Day, Year] using today's date

Write the entry to /tmp/pul-entry.json:

{
  "label": "Week of April 23, 2026",
  "date": "2026-04-23",
  "new": [
    {"title": "Dark mode", "description": "Toggle in Settings > Appearance. Works across all views."}
  ],
  "improved": [
    {"title": "API response time", "description": "40% faster on average. Dashboard now loads in under 1 second."}
  ],
  "fixed": [
    {"title": "CSV export", "description": "Exports no longer drop the last row."}
  ],
  "under_the_hood": []
}

Verify the entry:

python3 -c "
import json
with open('/tmp/pul-entry.json') as f:
    e = json.load(f)
print(f'Label: {e[\"label\"]}')
total = 0
for cat in ['new', 'improved', 'fixed', 'under_the_hood']:
    items = e.get(cat, [])
    if items:
        print(f'{cat.replace(\"_\", \" \").title()} ({len(items)}):')
        for item in items:
            print(f'  - {item[\"title\"]}: {item[\"description\"]}')
        total += len(items)
print(f'Total: {total} items')
"

Step 5: Generate Content Package

Using the changelog entry from Step 4, generate all four content pieces. Follow references/content-rules.md strictly.

One-liner (max 20 words): One sentence covering the biggest 1-2 items. Plain language, no jargon.

Tweet thread (3-5 tweets):

  • Tweet 1: Hook -- "We shipped [N] things this week." or lead with the biggest feature
  • Tweets 2-N: One item per tweet, 1-2 sentences max
  • Last tweet: "Changelog: [link]" or "More next week." (optional)
  • Each tweet strictly under 280 characters
  • No hashtags. No em dashes. Active voice.

LinkedIn post:

  • No markdown (asterisks render as literal on LinkedIn)
  • No hashtags
  • Founder voice: "We shipped", not "We are excited to announce"
  • Short paragraphs (1-2 sentences each), blank lines between them
  • Close with a question or observation, not a CTA
  • 150-400 words total

Email snippet:

  • Subject: "What shipped this week: [biggest item] + [1 more]"
  • Body: 50-100 words. "Here's what we shipped this week:" then bullets.

Write to /tmp/pul-content.json:

{
  "one_liner": "Dark mode, faster API, and a fixed export bug.",
  "tweet_thread": [
    "We shipped 3 things this week.",
    "Dark mode is live. Toggle it in Settings > Appearance. Works everywhere.",
    "API response time is now 40% faster. Dashboard loads in under a second.",
    "Fixed: CSV exports were dropping the last row. That's gone now.",
    "Changelog: [link]"
  ],
  "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?",
  "email_snippet": {
    "subject": "What shipped this week: dark mode + faster API",
    "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."
  }
}

Step 6: Self-QA

python3 -c "
import json, re

with open('/tmp/pul-raw.json') as f:
    raw = json.load(f)
with open('/tmp/pul-entry.json') as f:
    entry = json.load(f)
with open('/tmp/pul-content.json') as f:
    content = json.load(f)

full_text = json.dumps(entry) + json.dumps(content)
fails = 0

# Check 1: No em dashes
if chr(8212) in full_text:
    print('FAIL: em dash found -- replace with hyphen')
    fails += 1
else:
    print('PASS: no em dashes')

# Check 2: Banned words
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
          'streamline', 'leverage', 'transform', 'revolutionize', 'excited to announce',
          'pleased to announce', 'we are thrilled', 'cutting-edge', 'best-in-class',
          'world-class', 'unlock', 'delightful']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
    print(f'FAIL: banned words found: {found}')
    fails += 1
else:
    print('PASS: no banned words')

# Check 3: Tweet length
thread = content.get('tweet_thread', [])
long_tweets = [(i+1, len(t)) for i, t in enumerate(thread) if len(t) > 280]
if long_tweets:
    print(f'FAIL: tweets over 280 chars: {long_tweets}')
    fails += 1
else:
    print(f'PASS: all {len(thread)} tweets under 280 chars')

# Check 4: LinkedIn no hashtags
li = content.get('linkedin_post', '')
if re.search(r'#[A-Za-z]', li):
    print('FAIL: hashtags found in LinkedIn post')
    fails += 1
else:
    print('PASS: no hashtags in LinkedIn')

# Check 5: No markdown in LinkedIn
if '**' in li or '__' in li:
    print('FAIL: markdown formatting in LinkedIn (renders as literal asterisks)')
    fails += 1
else:
    print('PASS: no markdown in LinkedIn')

# Check 6: One-liner word count
one_liner = content.get('one_liner', '')
word_count = len(one_liner.split())
if word_count > 20:
    print(f'FAIL: one-liner is {word_count} words (max 20)')
    fails += 1
else:
    print(f'PASS: one-liner is {word_count} words')

# Check 7: Item count
entry_items = (len(entry.get('new', [])) + len(entry.get('improved', [])) +
               len(entry.get('fixed', [])) + len(entry.get('under_the_hood', [])))
raw_total = raw['total_items']
print(f'INFO: {entry_items} changelog items from {raw_total} raw items')

print()
print(f'Result: {\"PASS\" if fails == 0 else f\"FAIL ({fails} issues)\"}')
"

If any check fails: Fix the issue in the relevant temp file before proceeding to Step 7. Re-run the check after fixing.


Step 7: Append to Changelog + Save Content

python3 << 'PYEOF'
import json, os, re

with open('/tmp/pul-entry.json') as f:
    entry = json.load(f)
with open('/tmp/pul-content.json') as f:
    content = json.load(f)

# Build the new changelog section
lines = [f"## {entry['label']}", ""]

CAT_HEADERS = {
    "new": "### New",
    "improved": "### Improved",
    "fixed": "### Fixed",
    "under_the_hood": "### Under the hood",
}

for cat, header in CAT_HEADERS.items():
    items = entry.get(cat, [])
    if items:
        lines.append(header)
        for item in items:
            lines.append(f"- **{item['title']}** -- {item['description']}")
        lines.append("")

lines.append("---")
lines.append("")
new_section = "\n".join(lines)

# Prepend to docs/changelog.md
os.makedirs("docs", exist_ok=True)
changelog_path = "docs/changelog.md"

if os.path.exists(changelog_path):
    existing = open(changelog_path).read()
    # Insert after the top-level heading (if any) or at the very top
    if existing.startswith("# "):
        end_of_heading = existing.index("\n") + 1
        updated = existing[:end_of_heading] + "\n" + new_section + existing[end_of_heading:]
    else:
        updated = new_section + existing
else:
    updated = "# Changelog\n\n" + new_section

with open(changelog_path, "w") as f:
    f.write(updated)

print(f"Changelog updated: {changelog_path}")

# Save content package
date = entry['date']
content_dir = "docs/product-updates"
os.makedirs(content_dir, exist_ok=True)
content_path = f"{content_dir}/{date}-content.md"

content_lines = [
    f"# Content Package: {entry['label']}",
    "",
    "## One-liner",
    content.get('one_liner', ''),
    "",
    "## Tweet Thread",
    "",
]
thread = content.get('tweet_thread', [])
for i, tweet in enumerate(thread, 1):
    content_lines.append(f"[{i}/{len(thread)}] {tweet}")
    content_lines.append("")

content_lines += [
    "## LinkedIn Post",
    "",
    content.get('linkedin_post', ''),
    "",
    "## Email Snippet",
    "",
    f"Subject: {content.get('email_snippet', {}).get('subject', '')}",
    "",
    content.get('email_snippet', {}).get('body', ''),
    "",
]

with open(content_path, "w") as f:
    f.write("\n".join(content_lines))

print(f"Content package: {content_path}")
PYEOF

Step 8: Clean Up and Present

rm -f /tmp/pul-input.json /tmp/pul-raw.json /tmp/pul-entry.json /tmp/pul-content.json
echo "Done."

Present to the user in this order:

1. Changelog entry (formatted markdown, not raw JSON):

## Week of April 23, 2026

### New
- **Dark mode** -- Toggle in Settings > Appearance. Works across all views.

### Improved
- **API response time** -- 40% faster on average. Dashboard now loads in under 1 second.

### Fixed
- **CSV export** -- Exports no longer drop the last row.

2. Content package:

  • One-liner: [text]
  • Tweet thread: numbered list of tweets
  • LinkedIn post: full text
  • Email snippet: subject line + body

3. Saved files:

  • docs/changelog.md -- updated (new entry prepended)
  • docs/product-updates/[date]-content.md -- full content package saved

Common Mistakes

The agent will want to...Why that's wrong
Invent outcomes or metricsEvery 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.
Write "We are excited to announce"Banned. Use "We shipped", "[Feature] is now live", or just state the fact.
Use markdown bold (**) in LinkedInLinkedIn renders ** as literal asterisks. Plain text only.
Add hashtags to LinkedIn or tweetsThis skill never uses hashtags.
Put all items in "New"Bugs are Fixed, speed improvements are Improved. Miscategorizing weakens the changelog.
Skip the confirmation step in Step 3Always show the item list and ask the user to confirm before transforming. This prevents wrong-branch commits or stale items.
Include empty "Under the hood" sectionOmit if empty. Silence is better than noise.
Combine multiple items into one tweetOne item per tweet. Specificity > breadth.
Pad with filler tweetsIf there's one real item, write 2 tweets. Don't pad to 5.

Capabilities

skillsource-varnan-techskill-product-update-loggertopic-agent-skillstopic-gtmtopic-hermes-agenttopic-openclaw-skillstopic-skillstopic-technical-seo

Install

Quality

0.50/ 1.00

deterministic score 0.50 from registry signals: · indexed on github topic:agent-skills · 104 github stars · SKILL.md body (15,458 chars)

Provenance

Indexed fromgithub
Enriched2026-04-24 12:55:44Z · deterministic:skill-github:v1 · v1
First seen2026-04-23
Last seen2026-04-24

Agent access