mcpField
P
product-update-logger

product-update-logger

github.com/varnan-techマーケティング・グロース68744 回インストール2 週間前に更新
リポジトリ

product-update-logger について

product-update-loggerは、リリース内容を更新ログとマーケティング向け文章にまとめるスキルです。自由文、ローカルのGitコミット、GitHub PRを入力にできます。

主な機能

  1. 01docs/changelog.mdへの記録
  2. 02自由文、Git、GitHub PRの読み込み
  3. 03ツイートスレッド、LinkedIn投稿、メール文、短文の作成
  4. 04ルールに沿った更新ログの分類と作成

活用シーン

  1. 01製品リリースを継続的なログに記録
  2. 02リリース内容からSNSやメールの文章を準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/varnan-tech/opendirectory --skill product-update-logger

インストール後、Claude Code で/product-update-loggerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.env.example
README.md
evals/
evals.json
references/
changelog-format.md
content-rules.md
noise-filter.md
scripts/
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 件にするための水増しはしません。

コメント