product-update-logger について
product-update-loggerは、リリース内容を更新ログとマーケティング向け文章にまとめるスキルです。自由文、ローカルのGitコミット、GitHub PRを入力にできます。
主な機能
- 01docs/changelog.mdへの記録
- 02自由文、Git、GitHub PRの読み込み
- 03ツイートスレッド、LinkedIn投稿、メール文、短文の作成
- 04ルールに沿った更新ログの分類と作成
活用シーン
- 01製品リリースを継続的なログに記録
- 02リリース内容からSNSやメールの文章を準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ varnan-tech/ opendirectory --skill product-update-loggerインストール後、Claude Code で/product-update-loggerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.env.exampleREADME.mdevals/evals.jsonreferences/changelog-format.mdcontent-rules.mdnoise-filter.mdscripts/gather.py詳細
product-update-logger
このスキルに、製品でリリースした内容を伝えてください。整った変更履歴のエントリーを docs/changelog.md(最新のエントリーが先頭に追加される継続的なログ)に書き込み、同時にコンテンツパッケージ(ツイートのスレッド、LinkedIn の投稿、メール用の文章、ひと言の紹介文)を作成します。
入力ソース:メッセージの自由記述、ローカルリポジトリから自動で読み取った Git コミット、またはリポジトリを指定した場合の GitHub PR。これらは自由に組み合わせられます。
参照ファイル
実行するたびに、以下のファイルを読んでください。
cat references/changelog-format.md
cat references/content-rules.md
cat references/noise-filter.md
ステップ 1:セットアップの確認
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')"
git が利用可能かどうか、変更履歴がすでに存在するかどうかを確認します。これにより、バージョンラベルの形式が決まります。
ステップ 2:入力の解析
会話から以下の情報を収集します。
items-- リリースした内容の自由記述(複数の場合はパイプ区切り)。git が利用可能な場合は省略できます。since-- 何日前まで遡って確認するか。デフォルトは 7 です。ユーザーは「過去 2 週間」(14)や「前回のリリース以降」と指定できます。repo-- PR を取得するための GitHub の「owner/repo」。省略可能です。version_label-- 「v2.1.0」や「The Speed Update」などのカスタムラベル。省略可能です。デフォルトは日付ベースです。
ユーザーが items について何も述べておらず、git リポジトリもない場合: 次のように尋ねます。「何をリリースしましたか?機能、修正、改善を 1 行に 1 つずつ挙げてください。」
git が利用可能で、ユーザーから具体的な情報がない場合: ステップ 3 で git の自動読み取りに進みます。見つかった内容をユーザーに示し、変換する前に確認を取ります。
解析した入力を書き込みます。
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
ステップ 3:収集スクリプトの実行
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
出力を確認します。
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\"]}')
"
total_items == 0 の場合: 停止します。ユーザーに次のように伝えます。「リリースされた項目が見つかりませんでした。リリースした内容を説明するか、最近のコミットがある git リポジトリを指定するか、repo: owner/repo と GITHUB_TOKEN を指定して GitHub リポジトリを追加してください。」
見つかった項目の一覧をユーザーに示し、次のように尋ねます。「見つかった項目は以上です。変更履歴を書く前に、追加または削除するものはありますか?」
確認または編集を待ちます。ユーザーが「問題ありません」「進めてください」と言うか、変更を加えなかった場合は続行します。ユーザーが項目を追加または削除した場合は、ステップ 4 の前に /tmp/pul-raw.json を更新します。
ステップ 4:変更履歴のエントリーを生成
コンテキストとして項目を出力します。
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 への指示: 各項目の元の記述を、技術的な表現からユーザーにとってのメリットが伝わる表現に変換します。変換ルールと例については、references/changelog-format.md に従ってください。
ルール:
- 結果や指標を作り上げないでください。 「40% 高速化」は、元データに記載されている場合に限ります。コミットまたは PR に数値がない場合は、追加しないでください。
- 過去形を使います: 「Added」「Fixed」「Improved」など。「Adds」「Fixes」は使いません。
- 各項目にカテゴリを 1 つだけ割り当てます:New、Improved、Fixed、または Under the hood。
- Under the hood: 開発者に関係する場合(API の変更、破壊的変更)のみ含めます。空のセクションは省略します。
- テストの変更、CI の変更、ドキュメントのみのコミットに該当するものは省略します。
バージョンラベルを決定します。
- ユーザーが指定している場合:指定されたラベルをそのまま使用します。
existing_changelog.format == "semver"の場合:変更内容に基づいて増分します(修正のみならパッチ、新機能があればマイナー)。- デフォルト:「Week of [Month Day, Year]」を使用します(今日の日付)。
エントリーを /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": []
}
エントリーを確認します。
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')
"
ステップ 5:コンテンツパッケージを生成する
ステップ 4 の変更履歴エントリーを使って、4 種類すべてのコンテンツを生成します。references/content-rules.md に厳密に従ってください。
一文紹介(最大 20 語):最も重要な 1~2 項目を取り上げた 1 文。平易な言葉を使い、専門用語は避けます。
ツイートスレッド(3~5 件):
- ツイート 1:導入。「今週は [N] 件をリリースしました。」または、最も大きな機能から始めます
- ツイート 2~N:1 件につき 1 項目、最大 1~2 文
- 最後のツイート:「変更履歴:[link]」または「また来週。」(省略可)
- 各ツイートは必ず 280 文字未満にします
- ハッシュタグは使いません。長いダッシュは使いません。能動態にします。
LinkedIn の投稿:
- Markdown は使いません(アスタリスクがそのまま表示されます)
- ハッシュタグは使いません
- 創業者の語り口にします。「発表できることを嬉しく思います」ではなく「リリースしました」
- 短い段落(各 1~2 文)にし、段落間に空行を入れます
- CTA ではなく、質問または所感で締めくくります
- 合計 150~400 語にします
メール用スニペット:
- 件名:「今週のリリース:[最大の項目] + [もう 1 件]」
- 本文:50~100 語。「今週リリースした内容はこちらです:」に続けて箇条書きにします。
/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."
}
}
ステップ 6:セルフ 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)\"}')
"
チェックに失敗した場合: ステップ 7 に進む前に、該当する一時ファイルの問題を修正します。修正後、チェックを再実行します。
ステップ 7:変更履歴に追記し、コンテンツを保存する
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
ステップ 8:クリーンアップして提示する
rm -f /tmp/pul-input.json /tmp/pul-raw.json /tmp/pul-entry.json /tmp/pul-content.json
echo "Done."
次の順序でユーザーに提示します。
1. 変更履歴エントリー(生の JSON ではなく、書式設定済みの Markdown):
## 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. コンテンツパッケージ:
- 一文紹介:[テキスト]
- ツイートスレッド:ツイートを番号付きリストで表示
- LinkedIn の投稿:全文
- メール用スニペット:件名と本文
3. 保存したファイル:
docs/changelog.md-- 更新済み(新しいエントリーを先頭に追加)docs/product-updates/[date]-content.md-- コンテンツパッケージ全体を保存
よくある間違い
| エージェントがやりがちなこと | それが間違っている理由 |
|---|---|
| 成果や指標を作り上げる | すべての主張は元の項目に基づいている必要があります。「40% 高速化」はコミットメッセージまたは PR 本文に記載されている必要があります。数値がない場合は追加しません。 |
| 「発表できることを嬉しく思います」と書く | 禁止です。「リリースしました」、「[機能] が利用可能になりました」と書くか、事実をそのまま述べます。 |
| LinkedIn で Markdown の太字(**)を使う | LinkedIn では ** がそのままアスタリスクとして表示されます。プレーンテキストのみを使います。 |
| LinkedIn やツイートにハッシュタグを追加する | このスキルではハッシュタグを使いません。 |
| すべての項目を「新機能」に入れる | バグは「修正」、速度の改善は「改善」です。誤った分類は変更履歴の質を損ないます。 |
| ステップ 3 の確認を省略する | 変換の前に必ず項目リストを提示し、ユーザーに確認を求めます。これにより、ブランチの取り違えや古い項目の使用を防げます。 |
| 空の「内部の仕組み」セクションを含める | 空の場合は省略します。余計な情報を加えるより、何も書かないほうがよいです。 |
| 複数の項目を 1 件のツイートにまとめる | ツイート 1 件につき項目は 1 つです。幅広さより具体性を重視します。 |
| 内容のないツイートで水増しする | 実際の項目が 1 件なら、ツイートは 2 件作成します。5 件にするための水増しはしません。 |