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。ユーザーが「先週」や「前回のリリース以降」と言った場合に設定。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": "", # 設定:パイプ区切りの自由記述テキスト、なければ ""
"since": 7, # 設定:整数の日数
"repo": "", # 設定:"owner/repo" か ""
"version_label": "" # 設定:"" は自動(日付ベース)、カスタム文字列も可
}
with open("/tmp/pul-input.json", "w") as f:
json.dump(inp, f, indent=2)
print(f"対象日数: {inp['since']} 日")
print(f"自由記述アイテム: {inp['items'] or 'なし(Git/GitHubを使用)'}")
print(f"GitHubリポジトリ: {inp['repo'] or 'なし'}")
print(f"バージョンラベル: {inp['version_label'] or '自動(日付ベース)'}")
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'見つかったアイテム数: {d[\"total_items\"]}')
print(f'フィルタリングされたノイズ: {d[\"noise_filtered\"]}')
print(f'Git利用可能: {d[\"git_available\"]}')
print(f'GitHub利用可能: {d[\"github_available\"]}')
print(f'ソース: 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('アイテム:')
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'既存の変更ログフォーマット: {d[\"existing_changelog\"][\"format\"]}')
print(f'最後のラベル: {d[\"existing_changelog\"][\"last_label\"]}')
print(f'今日の日付: {d[\"date\"]}')
"
AIへの指示: 各生アイテムを技術言語からユーザー向けの利点言語に変換してください。変換ルールと例については references/changelog-format.md に従ってください。
ルール:
- 結果や指標を捏造しないでください。 「40%高速化」はソースデータに由来する必要があります。コミットやPRに数値がなければ、追加しないでください。
- 過去形を使用: 「追加」「修正」「改善」 -- 「追加します」「修正します」ではない
- 各アイテムにカテゴリを1つ割り当て: New(新機能)、Improved(改善)、Fixed(修正)、Under the hood(内部変更)
- Under the hood: 開発者に関連する場合のみ含めてください(API変更、破壊的変更)。空のセクションは省略。
- 以下に該当するものは除外: テスト変更、CI変更、ドキュメントのみのコミット
バージョンラベルの決定:
- ユーザーが指定した場合:そのまま使用
existing_changelog.format == "semver"の場合:変更に基づいてインクリメント(修正のみならパッチ、新機能があればマイナー)- デフォルト:今日の日付を使用して「[月 日, 年] の週」
エントリを /tmp/pul-entry.json に書き込みます:
{
"label": "2026年4月23日の週",
"date": "2026-04-23",
"new": [
{"title": "ダークモード", "description": "設定 > 表示で切り替え。全ビューで動作します。"}
],
"improved": [
{"title": "API応答時間", "description": "平均40%高速化。ダッシュボードが1秒未満で読み込まれます。"}
],
"fixed": [
{"title": "CSVエクスポート", "description": "エクスポートで最終行が欠落しなくなりました。"}
],
"under_the_hood": []
}
エントリを確認:
python3 -c "
import json
with open('/tmp/pul-entry.json') as f:
e = json.load(f)
print(f'ラベル: {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} アイテム')
"
ステップ5:コンテンツパッケージの生成
ステップ4の変更ログエントリを使用して、4つのコンテンツすべてを生成します。references/content-rules.md に厳密に従ってください。
一言キャッチコピー(最大20語):最大の1~2アイテムをカバーする一文。平易な言葉で、専門用語なし。
ツイートスレッド(3~5ツイート):
- ツイート1:フック -- 「今週[N]個のものを出荷しました。」または最大の機能でリード
- ツイート2~N:ツイートごとに1アイテム、最大1~2文
- 最後のツイート:「変更ログ:[リンク]」または「また来週。」(任意)
- 各ツイートは厳密に280文字未満
- ハッシュタグなし。ダッシュなし。能動態。
LinkedIn投稿:
- マークダウンなし(アスタリスクはLinkedIn上でそのまま表示されるため)
- ハッシュタグなし
- 創業者の声:「We are excited to announce」ではなく「We shipped」
- 短い段落(各1~2文)、段落間は空行
- CTAではなく、質問や観察で締めくくる
- 合計150~400語
メールスニペット:
- 件名:「今週の出荷:[最大のアイテム] + [あと1つ]」
- 本文:50~100語。「今週出荷したものはこちらです:」の後に箇条書き。
/tmp/pul-content.json に書き込みます:
{
"one_liner": "ダークモード、API高速化、エクスポートバグ修正。",
"tweet_thread": [
"今週3つのものを出荷しました。",
"ダークモードが利用可能に。設定 > 表示で切り替えられます。全画面で動作します。",
"API応答時間が平均40%高速化。ダッシュボードが1秒未満で読み込まれます。",
"修正:CSVエクスポートで最終行が欠落していました。現在は修正済みです。",
"変更ログ:[リンク]"
],
"linkedin_post": "今週3つのアップデートを出荷しました。\n\nダークモードが利用可能になりました。設定の表示から切り替えられます。すべてのビューで動作します。\n\nAPI応答時間が平均40%高速化しました。ほとんどのユーザーでダッシュボードが1秒未満で読み込まれるようになりました。\n\nまた、CSVエクスポートが最終行を静かに欠落させるバグを修正しました。これに遭遇してエクスポートを中止していた方は、再試行する価値があります。\n\nどのような機能を待っていましたか?",
"email_snippet": {
"subject": "今週の出荷:ダークモード + API高速化",
"body": "今週出荷したものはこちらです:\n\n- ダークモード:設定 > 表示で切り替え\n- API応答時間:平均40%高速化、ダッシュボードが1秒未満で読み込み\n- 修正:CSVエクスポートで最終行が欠落しなくなりました\n\n以下に完全な変更ログがあります。"
}
}
ステップ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
# チェック1:ダッシュなし
if chr(8212) in full_text:
print('FAIL: ダッシュが見つかりました -- ハイフンに置き換えてください')
fails += 1
else:
print('PASS: ダッシュなし')
# チェック2:禁止語句
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: 禁止語句が見つかりました: {found}')
fails += 1
else:
print('PASS: 禁止語句なし')
# チェック3:ツイートの長さ
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: 280文字を超えるツイート: {long_tweets}')
fails += 1
else:
print(f'PASS: 全{len(thread)}ツイートが280文字未満')
# チェック4:LinkedInにハッシュタグなし
li = content.get('linkedin_post', '')
if re.search(r'#[A-Za-z]', li):
print('FAIL: LinkedIn投稿にハッシュタグがあります')
fails += 1
else:
print('PASS: LinkedInにハッシュタグなし')
# チェック5:LinkedInにマークダウンなし
if '**' in li or '__' in li:
print('FAIL: LinkedInにマークダウン書式があります(アスタリスクがそのまま表示されます)')
fails += 1
else:
print('PASS: LinkedInにマークダウンなし')
# チェック6:一言キャッチコピーの語数
one_liner = content.get('one_liner', '')
word_count = len(one_liner.split())
if word_count > 20:
print(f'FAIL: 一言キャッチコピーが{word_count}語(最大20語)')
fails += 1
else:
print(f'PASS: 一言キャッチコピーは{word_count}語')
# チェック7:アイテム数
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: {raw_total}個の生アイテムから{entry_items}個の変更ログアイテム')
print()
print(f'結果: {"PASS" if fails == 0 else f"FAIL ({fails} 件の問題)"}')
"
チェックに失敗した場合: ステップ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)
# 新しい変更ログセクションを構築
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)
# 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()
# トップレベル見出しの後(ある場合)または最上部に挿入
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_path}")
# コンテンツパッケージを保存
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"# コンテンツパッケージ: {entry['label']}",
"",
"## 一言キャッチコピー",
content.get('one_liner', ''),
"",
"## ツイートスレッド",
"",
]
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投稿",
"",
content.get('linkedin_post', ''),
"",
"## メールスニペット",
"",
f"件名: {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_path}")
PYEOF
ステップ8:クリーンアップと提示
rm -f /tmp/pul-input.json /tmp/pul-raw.json /tmp/pul-entry.json /tmp/pul-content.json
echo "完了。"
以下の順序でユーザーに提示:
1. 変更ログエントリ(整形済みマークダウン、生JSONではない):
## 2026年4月23日の週
### New
- **ダークモード** -- 設定 > 表示で切り替え。全ビューで動作します。
### Improved
- **API応答時間** -- 平均40%高速化。ダッシュボードが1秒未満で読み込まれます。
...