あなたの顧客がいる場所
与えられた製品ユーティリティとICPに基づいて、実際のICPのペインポストを元のコミュニティまでトレースします。競合の議論シグナルをレイヤー化します。DuckDuckGoを介してSlack/Discord/ニュースレター/ポッドキャスト/カンファレンスのチャネルを発見します。すべてのチャネルをICPシグナル数、規模、アクティビティ、競合の存在でスコアリングします。ランク付けされたプレイブックを出力します:証拠、エントリ戦術、コンテンツアングル、アンチパターン - チャネルごとに1つ。推測はしません。信号トレースされたチャネルのみです。
重要なルール: 出力内のすべてのチャネル名は、この実行におけるReddit API応答またはDuckDuckGo検索結果に存在している必要があります。すべてのメンバー数は、about.json APIまたは検索スニペットから取得する必要があります - 決して推定しないでください。すべてのICPシグナル数は、生データと一致する必要があります。チャネルタイプの結果が0の場合は、0と報告してください - チャネルを捏造しないでください。
よくある間違い
| エージェントがやってしまいがちなこと | なぜそれが間違いか |
|---|---|
| 一般的なチャネル(「LinkedIn」「Twitter」)を推奨する | すべてのチャネルは、名前、メンバー数、URLを具体的に指定する必要があります。「LinkedInグループ:エンタープライズチームのためのDevOps(45Kメンバー)」- 「LinkedIn」だけではありません。 |
| すべてのICPに同じチャネルを使用する | 信号トレースはICP固有です。DevOpsのICPとFinanceのICPでは、まったく異なるチャネルリストが生成されます。ICPごとにスクリプトを新たに実行してください。 |
| メンバー数やコミュニティ名を捏造する | すべてのチャネル名はDuckDuckGoの結果またはReddit APIから取得する必要があります。すべてのメンバー数はAPIまたは検索スニペットから取得する必要があります。入手できない場合は、「メンバー数不明」と記載してください。 |
| 競合レイヤーをスキップする | 競合が議論されている場所 = あなたのICPが代替案を評価している場所 = 最もホットなアウトリーチのコンテキストです。ユーザーが求めていなくても、常に競合検索を実行してください。 |
| 製品の売り込みになるようなエントリ戦術を書く | 「r/devops にあなたの製品について投稿する」はエントリ戦術ではありません。エントリ戦術は、具体的なスレッドタイプ、コンテンツ形式、およびコミュニティの規範を指定します。 |
| Redditを唯一のチャネルタイプとして扱う | 出力には少なくとも3つのチャネルタイプを含める必要があります。Redditしか見つからない場合は、DuckDuckGoでSlack/Discord/ニュースレター/カンファレンスを明示的に検索してから停止してください。 |
ステップ1:セットアップチェック
echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-未設定 -- 非認証時は競合レイヤーが60 req/hrで実行されます}"
echo ""
echo "この実行で使用するデータソース:"
echo " Reddit public JSON (認証不要, 信号トレース)"
echo " Reddit about.json (認証不要, サブレディットメタデータ)"
echo " HN Algolia API (認証不要, 信号トレース)"
echo " DuckDuckGo HTML (認証不要, チャネル発見)"
echo " GitHub API (${GITHUB_TOKEN:+認証済み, }競合エンリッチメントに任意)"
GITHUB_TOKEN が設定されていなくても続行してください。すべてのコアチャネル発見はそれなしで動作します。
ステップ2:ICPの解析
会話から収集します:
product-- 製品の機能(一文)icp_role-- ICPの役割(例:「テクニカル共同創業者」、「シリーズAのDevOpsエンジニア」)icp_pain-- 彼らの主要な問題(例:「顧客獲得」、「アラート疲れ」)category-- 市場カテゴリのキーワード(例:「スタートアップGTMセールス」、「devopsモニタリング」)competitors-- 任意の競合名(例:「Clay, Apollo, HubSpot」)
ICPカスケード:
- ユーザーのプロンプトにproduct + icp_role + icp_painが含まれている場合:それらを直接抽出して続行します。
- プロンプトが不十分な場合(カテゴリまたは製品名のみ):
docs/icp.mdに保存済みのICPプロファイルがないか確認します。プロンプトの詳細とマージします。 - それでも不十分な場合(icp_roleまたはicp_painがない):以下の3つの質問を1つずつ行います。
- 「あなたの製品は何をする製品ですか?(一文で)」
- 「あなたの理想的な顧客は誰ですか?(役割、会社の種類、チーム規模)」
- 「あなたの製品を見つける前の、彼らの主要な問題は何ですか?」
- 最終的なICPを
docs/icp.mdに保存して、他のスキルが再利用できるようにします。
docs/icp.md にまだこの製品が含まれていない場合、ICPファイルを保存します:
python3 << 'PYEOF'
import json, os
icp = {
"product": "PRODUCT_HERE",
"icp_role": "ICP_ROLE_HERE",
"icp_pain": "ICP_PAIN_HERE",
"competitors": ["COMP_1", "COMP_2"],
"category": "CATEGORY_HERE"
}
os.makedirs("docs", exist_ok=True)
with open("/tmp/wcl-input.json", "w") as f:
json.dump(icp, f, indent=2)
# Update docs/icp.md
icp_md_path = "docs/icp.md"
new_block = f"""## {icp['product']}
- **ICP role:** {icp['icp_role']}
- **ICP pain:** {icp['icp_pain']}
- **Competitors:** {', '.join(icp['competitors']) if icp['competitors'] else 'none'}
- **Category:** {icp['category']}
"""
existing = open(icp_md_path).read() if os.path.exists(icp_md_path) else ""
if icp['product'] not in existing:
with open(icp_md_path, "a") as f:
f.write(new_block)
print(f"ICP saved to {icp_md_path}")
else:
print(f"ICP already in {icp_md_path}")
print(f"Product: {icp['product']}")
print(f"ICP role: {icp['icp_role']}")
print(f"ICP pain: {icp['icp_pain']}")
print(f"Competitors: {', '.join(icp['competitors']) if icp['competitors'] else 'none'}")
PYEOF
ステップ3:スタンドアロンデータ収集スクリプトの実行
スクリプトが存在するか確認します:
ls scripts/fetch.py 2>/dev/null && echo "script available" || echo "not found"
チャネル発見を実行します:
GITHUB_TOKEN="${GITHUB_TOKEN:-}" python3 scripts/fetch.py \
"$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['category'])")" \
--icp-role "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['icp_role'])")" \
--icp-pain "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['icp_pain'])")" \
--product "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(d['product'])")" \
--competitors "$(python3 -c "import json; d=json.load(open('/tmp/wcl-input.json')); print(','.join(d['competitors']))")" \
--output /tmp/wcl-raw.json
完了を待ちます(最大5分 - Reddit + DuckDuckGo検索で合計約120秒かかります)。
出力を確認します:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
print(f'Reddit posts found: {d[\"reddit_posts_found\"]}')
print(f'HN signals found: {d[\"hn_signals_found\"]}')
print(f'Channels discovered: {d[\"summary\"][\"total_channels\"]}')
print(f'Top priority: {len(d[\"summary\"][\"top_priority\"])}')
print(f'By type: {d[\"summary\"][\"by_type\"]}')
print(f'Competitor layer ran: {d[\"summary\"][\"competitor_layer_ran\"]}')
"
total_channels < 3 の場合:ユーザーに次のように伝えます:「3つ未満のチャネルしか見つかりませんでした。ICPの説明がReddit/DDGのカバレッジに対して狭すぎる可能性があります。より広いカテゴリキーワードを試すか、競合レイヤーをアクティブにするために競合名を追加してください。」その後、停止する前に、より広いカテゴリキーワードを使用して1回の再試行を行います。
ステップ4:チャネルサマリーの表示
生データを読み込み、ランク付けされたサマリーテーブルを表示します:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
channels = d['channels_discovered']
print(f'Channels found: {len(channels)}')
print()
print(f'{'#':<4} {'Channel':<35} {'Type':<14} {'Members':<12} {'ICP signals':<13} {'Score':<8} Tier')
print('-' * 100)
for i, ch in enumerate(channels[:15], 1):
members = ch.get('members', 0)
m_str = f'{members//1000}K' if members >= 1000 else str(members) if members else '?'
print(f'{i:<4} {ch[\"name\"]:<35} {ch[\"type\"]:<14} {m_str:<12} {ch.get(\"icp_signal_count\",0):<13} {ch.get(\"channel_score\",0):<8} {ch.get(\"tier\",\"\")}')
"
最高スコアのチャネルから上位3つの証拠投稿を表示します:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
channels = d['channels_discovered']
if channels:
top = channels[0]
print(f'Top channel: {top[\"name\"]}')
print(f'Evidence posts:')
for ep in top.get('evidence_posts', [])[:3]:
print(f' [{ep.get(\"score\",0):.0f}] {ep.get(\"title\",\"\")}')
print(f' {ep.get(\"url\",\"\")}')
"
ステップ5:AIチャネルエンリッチメント
生のチャネルデータが準備できました。トッププライオリティおよびハイティアの各チャネルについて、プレイブックエントリを生成します。
すべてのチャネルを読み込みます:
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
d = json.load(f)
top_channels = [ch for ch in d['channels_discovered'] if ch.get('tier') in ('top-priority', 'high')]
print(json.dumps(top_channels, indent=2))
"
上記の各チャネルについて、以下を生成します:
who_is_here: このチャネルに存在する特定のタイプのICPを説明する2文。証拠投稿、サブレディットの説明、ICPプロファイルから導出します。「あなたのターゲットオーディエンス」とは書かないでください - 具体的に記述してください。例:「50〜500人の企業で、専任のSREチームなしでインフラスタックを所有するDevOpsエンジニア。彼らはオンコールの burnout、Kubernetesのスプロール、クラウドネイティブとセルフホスト型のオブザーバビリティの選択について投稿しています。」
entry_tactic: 1つの具体的で実行可能なエントリ行動。スレッドタイプ、投稿形式、コミュニティの規範を指定します。「コミュニティに参加する」ではありません。例:毎週月曜日にautomoderatorが投稿する「今週の取り組みは?」スレッドを見つけます。あなたが解決した技術的な課題を3文で返信します - 何が壊れたか、何を試したか、何がうまくいったか。製品について言及しないでください。スタンドアロンのコンテンツを投稿する前にカルマを構築してください。
content_angle: この特定のチャネルで最もエンゲージメントが高いコンテンツ形式。証拠投稿のタイトルとスコアから導出します。例:「技術的なポストモーテムは、製品発表を5:1で上回るエンゲージメントを獲得します。形式:「20万人のユーザーをXからYに移行しました - 何が壊れて、なぜそうなったか」具体的な数字 + 失敗したこと = 最も多くのupvotes。」
anti_patterns: このコミュニティで投稿が削除されたり、評判が損なわれたりする具体的な行動を2〜3つ。サブレディットのルール(説明で入手可能な場合)と証拠投稿のパターンから導出します。例:["プロモーションではないスレッドに製品リンクを投稿する - モデレーターが数時間以内に削除します", 「具体的なコンテキストなしで「どのツールを使っていますか?」と尋ねる - 市場調査ファーミングとしてフラグが立てられます」]
エンリッチされたプレイブックを /tmp/wcl-channels.json に書き込みます:
{
"playbook": [
{
"channel": "r/devops",
"evidence": "ここで34のICPシグナルがトレースされました。平均ペインスコア180",
"who_is_here": "...",
"entry_tactic": "...",
"content_angle": "...",
"anti_patterns": ["...", "..."]
}
]
}
python3 -c "
import json
with open('/tmp/wcl-channels.json') as f:
d = json.load(f)
print(f'Playbook entries: {len(d[\"playbook\"])}')
for p in d['playbook']:
print(f' {p[\"channel\"]}')
"
ステップ6:完全なランク付け出力の生成
完全なランク付けプレイブックを /tmp/wcl-output.json に書き込みます:
python3 << 'PYEOF'
import json
from datetime import datetime
with open('/tmp/wcl-input.json') as f:
inp = json.load(f)
with open('/tmp/wcl-raw.json') as f:
raw = json.load(f)
with open('/tmp/wcl-channels.json') as f:
enriched = json.load(f)
playbook_by_channel = {p['channel']: p for p in enriched['playbook']}
channels = raw['channels_discovered']
output = {
"date": raw['date'],
"product": inp['product'],
"icp_role": inp['icp_role'],
"icp_pain": inp['icp_pain'],
"competitors": inp.get('competitors', []),
"total_channels": raw['summary']['total_channels'],
"channels": []
}
for ch in channels:
name = ch['name']
playbook = playbook_by_channel.get(name, {})
output['channels'].append({
"rank": channels.index(ch) + 1,
"name": name,
"type": ch['type'],
"url": ch['url'],
"members": ch.get('members', 0),
"active_users": ch.get('active_users', 0),
"icp_signal_count": ch.get('icp_signal_count', 0),
"competitor_mentions": ch.get('competitor_mentions', 0),
"channel_score": ch.get('channel_score', 0),
"tier": ch.get('tier', ''),
"entry_type": ch.get('entry_type', 'open'),
"evidence_posts": ch.get('evidence_posts', []),
"who_is_here": playbook.get('who_is_here', ''),
"entry_tactic": playbook.get('entry_tactic', ''),
"content_angle": playbook.get('content_angle', ''),
"anti_patterns": playbook.get('anti_patterns', []),
})
with open('/tmp/wcl-output.json', 'w') as f:
json.dump(output, f, indent=2)
print(f"Output written: /tmp/wcl-output.json")
print(f"Total channels: {len(output['channels'])}")
PYEOF
ステップ7:自己QA
python3 -c "
import json
with open('/tmp/wcl-raw.json') as f:
raw = json.load(f)
with open('/tmp/wcl-output.json') as f:
output = json.load(f)
full_text = json.dumps(output)
raw_channel_names = {ch['name'].lower() for ch in raw['channels_discovered']}
passes = 0
fails = 0
# Check 1: No em dashes
if chr(8212) in full_text:
print('FAIL: em dash found in output -- replace with hyphen')
fails += 1
else:
print('PASS: no em dashes')
passes += 1
# Check 2: No banned words
banned = ['powerful', 'robust', 'seamless', 'innovative', 'game-changing',
'streamline', 'leverage', 'transform', 'revolutionize']
found = [w for w in banned if w.lower() in full_text.lower()]
if found:
print(f'FAIL: banned words: {found}')
fails += 1
else:
print('PASS: no banned words')
passes += 1
# Check 3: At least 3 channel types
types = {ch['type'] for ch in output['channels']}
if len(types) < 3:
print(f'FAIL: only {len(types)} channel type(s) in output: {types}')
fails += 1
else:
print(f'PASS: {len(types)} channel types: {types}')
passes += 1
# Check 4: All channel names exist in raw data
for ch in output['channels']:
if ch['name'].lower() not in raw_channel_names:
print(f'FAIL: channel not in raw data: {ch[\"name\"]}')
fails += 1
if fails == 0:
print('PASS: all channel names verified in raw data')
passes += 1
# Check 5: No generic entry tactics
generic_phrases = ['engage with the community', 'post about your product', 'share your content']
for ch in output['channels']:
tactic = ch.get('entry_tactic', '').lower()
for phrase in generic_phrases:
if phrase in tactic:
print(f'FAIL: generic entry tactic in {ch[\"name\"]}: \"{phrase}\"')
fails += 1
if fails == 0:
print('PASS: no generic entry tactics found')
passes += 1
print(f'\\nResults: {passes} passed, {fails} failed')
if fails > 0:
exit(1)
"
ステップ8:出力の表示
結果をきれいに表示します:
python3 -c "
import json
with open('/tmp/wcl-output.json') as f:
output = json.load(f)
print('=' * 80)
print('RANKED CHANNEL PLAYBOOK')
print(f'Product: {output[\"product\"]}')
print(f'Target ICP: {output[\"icp_role\"]}')
print(f'Primary pain: {output[\"icp_pain\"]}')
print(f'Competitors: {\", \".join(output[\"competitors\"]) if output[\"competitors\"] else \"none\"}')
print(f'Total channels found: {output[\"total_channels\"]}')
print('=' * 80)
print()
for ch in output['channels']:
print(f'### {ch[\"rank\"]}. {ch[\"name\"]} ({ch[\"type\"]})')
print(f' Tier: {ch[\"tier\"]} | Score: {ch[\"channel_score\"]} | Members: {ch[\"members\"]} | ICP signals: {ch[\"icp_signal_count\"]}')
if ch.get('who_is_here'):
print(f' Who is here: {ch[\"who_is_here\"]}')
if ch.get('entry_tactic'):
print(f' Entry tactic: {ch[\"entry_tactic\"]}')
if ch.get('content_angle'):
print(f' Content angle: {ch[\"content_angle\"]}')
if ch.get('anti_patterns'):
print(f' Anti-patterns:')
for ap in ch['anti_patterns']:
print(f' - {ap}')
if ch.get('evidence_posts'):
print(f' Evidence:')
for ep in ch['evidence_posts'][:3]:
print(f' [{ep.get(\"score\",0):.0f}] {ep.get(\"title\",\"\")}')
print(f' {ep.get(\"url\",\"\")}')
print()
"