with one click
announce
Post a customer-voice product update to
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Post a customer-voice product update to
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Pick up a Linear task and execute it on a fresh branch through local validation. Invoked via `/checkout` (no args → pick the highest-priority unblocked issue in the ProductFlash team) or `/checkout <LINEAR-ID>` (e.g. `/checkout PF-12` → work that specific issue). Loads the Linear issue into context, branches from main, executes, validates locally. Stops before PR — use `/open-pr` next to ship. Pairs with CLAUDE.md (Karpathy rules + stack + hard rules — don't restate them here).
Ship the current branch — open a PR, wait for CI, squash-merge into main, and move the Linear issue to Done. Invoked via `/open-pr` (no args; always operates on the current branch). Picks up where `/checkout` left off. Assumes local validation is already done; pairs with CLAUDE.md (Karpathy + hard rules apply).
Create a new Linear task in the ProductFlash team. Invoked via `/task <one-line description>` or `/task` (then ask). Captures title, description, labels, blockers/dependencies, and optionally fast-tracks it (state=Todo + priority=Urgent) so `/checkout` picks it up next. Optimized for low-friction capture — one round of clarifying questions max.
Perform a read-only architecture and code-quality audit of a TypeScript web/SaaS codebase. Covers SOLID (as applicable), DRY/KISS/YAGNI/POLA, file-structure compliance, module coupling & cohesion, colocation, client/server/isomorphism boundaries, abstraction discipline, naming consistency, type-safety hygiene, and error-handling patterns. Produces one ranked report with severity, finding kind (Violation vs Smell), evidence (file:line), and a concrete remediation. Use whenever the user asks for an "arch review", "architecture audit", "code-quality review", "SOLID check", "code smells", "structure review", "DRY/KISS audit", or similar. Never modifies code — read-only by contract.
Perform a comprehensive read-only security audit of a web/SaaS codebase. Covers authentication & tenant isolation, route/endpoint exposure, secret leaks, SQL/ORM injection, XSS/CSRF/SSRF/open redirect, LLM cost-abuse and prompt injection, dependency supply-chain risks, and OWASP basics. Produces a single ranked report with severity, likelihood, impact, evidence (file:line), and remediation. Use whenever the user asks for a "security audit", "security review", "pentest of the code", "look for vulns", "OWASP review", "LLM abuse surface", or similar. Never modifies files — read-only by contract.
Perform a read-only audit of the codebase's test pyramid. Two halves of equal weight: (1) find critical flows lacking coverage; (2) audit *existing* tests for false confidence — shape-only assertions, tautological mocks, skipped/.only blocks, brittle selectors, tests that pass for the wrong reason. A green suite that doesn't exercise behavior is at least as dangerous as a missing one. Assumes Vitest for unit/integration and Playwright for e2e/smoke. Pragmatic by contract: does NOT chase 100% line coverage — focuses on logic, branching, money math, auth/tenant filters, and user-visible flows. Produces one ranked report with severity, finding kind (Gap vs Defect), evidence (file:line), and a concrete suggested fix. Use whenever the user asks for a "test audit", "test coverage review", "test pyramid health check", "audit our tests", "what should we test", or similar. Never modifies test code — read-only by contract.
| name | announce |
| description | Post a customer-voice product update to |
You've just shipped something via /open-pr. This skill turns it into a short product-voice update in #pf-product-news. Customer value, not engineering detail. Bullets read like "Your first brief now asks how we did" — never like "Added an fte_feedback table with NPS column."
Operates with no arguments. Assume the most recent merged PR on the user's history is the one to announce; confirm before posting.
SLACK_BOT_TOKEN must be set in the environment. The token starts with xoxb-. If missing, stop and tell the user to set it — see the Setup gotcha below before suggesting a fix.#pf-product-news. Channel ID is C0B78CER88H — hardcoded into the API calls below. The bot must be a member of that channel.chat:write, files:write. We do not need channels:read because the channel ID is baked in.argv. Always read it from the environment inside the curl call (-H "Authorization: Bearer $SLACK_BOT_TOKEN") so it never appears in shell history. Per the project secret-handling rule.Claude Code's Bash tool spawns /bin/bash, not zsh — so an export SLACK_BOT_TOKEN=… in ~/.zshrc alone is invisible to this skill. If ${#SLACK_BOT_TOKEN} is 0 and the user says they "set it in zshrc", tell them to either:
~/.bashrc, or~/.profile and source that from both rc files.Then fully restart Claude Code — env vars are inherited at process startup; source ~/.bashrc inside a single Bash tool call doesn't propagate to subsequent calls (each spawns a fresh subshell).
Verify without leaking the value: echo "${#SLACK_BOT_TOKEN}" should print ~56.
git log --merges --oneline -5 origin/main
The most recent merge commit's title typically contains the PR number (Merge pull request #117 ... or a squash-merge subject ending in (#117)). Extract the number. If ambiguous or none found, ask the user for the PR number.
Then pull the canonical inputs:
gh pr view <n> --json title,body,url,mergedAt,number,headRefName — title/body/url.headRefName (e.g. feat/pf-197-... → PF-197).mcp__linear-server__get_issue with that ID — title, description, project, milestone.If the PR is not merged yet, stop and ask the user to confirm they want to announce an unmerged change. Default behavior is post-merge only.
Look in /tmp/ for files named pf-<n>-*.png or <linear-id>-*.png matching this task. Include all of them.
If there are no screenshots, that's fine. Skip image upload, post text-only.
Voice rules — these are not optional:
pnpm tsx scripts/x.ts), no env-var names, no library names, no commit hashes, no branch names. If the change is purely internal and nothing about it can be described without referencing code, that's a signal the post shouldn't exist — re-confirm with the user whether to announce at all.@here so active channel members get a notification. Use the literal mrkdwn token <!here> (Slack renders it as @here and triggers the ping). Place it on the headline line, before the emoji: <!here> 🚀 *<headline>*. Plain-text text fallback should mirror the same prefix so the notification preview reads naturally.Show the draft to the user with AskUserQuestion, offering: Post as-is / Edit / Cancel. If Edit, ask for the revised text; loop until approved.
Example draft for PF-197 (use this as a tonal reference, not a template):
✨ *Your first brief now asks how we did*
• The "Next Brief" countdown on the briefs list is gone — it was promising a cadence we don't have yet, and even when daily sends ship we won't promise a brief every day.
• A new NPS-style feedback panel sits at the bottom of your first brief: score 0–10 + a quick "what landed, what missed" box. Your honest "meh" is more useful than a polite 7.
• Replaced with a small "private beta" banner that sets honest expectations.
_Why this matters: the moment right after the first brief is where the mind-blown reaction either lands or doesn't — we want to capture it, not redirect to a stale countdown._
PR: <url>
Linear: <url>
Screenshots, when present, are embedded inline in the main message via image blocks referencing the uploaded files — not as a thread reply. Use curl directly — no SDK. Write the final chat.postMessage payload to /tmp/announce-payload.json (--data @file keeps the body out of argv and shell history).
Slack's modern external-upload flow (files.upload is deprecated). For each file:
GET https://slack.com/api/files.getUploadURLExternal?filename=<name>&length=<bytes> → returns upload_url + file_id.POST the raw bytes to upload_url:
curl -sS -X POST "$upload_url" --data-binary @"$file"
CRITICAL: do NOT use curl -F "filename=@$file" (multipart form). Slack's external upload URL accepts the multipart POST without complaint but stores a 0-byte file, and files.completeUploadExternal happily returns ok: true with a broken mimetype: "" size mismatch. Use --data-binary only.POST https://slack.com/api/files.completeUploadExternal with body:
{ "files": [ {"id":"<file_id>","title":"<filename>"}, ... ] }
Omit channel_id and thread_ts. The file is uploaded but not yet shared anywhere — it'll be attached to the channel as part of the chat.postMessage call below via slack_file references.Verify the upload didn't silently fail. ok: true is not sufficient. For each returned file, check size > 0 and size matches the local file's byte count. If any file shows size: 0 or mimetype: "", the raw-bytes POST didn't land — re-run step 2 with --data-binary and re-complete. Don't post the message until the size check passes.
Use Block Kit. Append one image block per uploaded file, each referencing the file by id:
{
"channel": "C0B78CER88H",
"unfurl_links": false,
"unfurl_media": false,
"text": "<plain-text fallback for notifications>",
"blocks": [
{ "type": "section", "text": { "type": "mrkdwn", "text": "<!here> 🚀 *<headline>*" } },
{ "type": "section", "text": { "type": "mrkdwn", "text": "• <bullet 1>\n\n• <bullet 2>\n\n• <bullet 3>" } },
{ "type": "section", "text": { "type": "mrkdwn", "text": "_<why this matters line>_" } },
{ "type": "image", "slack_file": { "id": "<file_id>" }, "alt_text": "<short description>" },
{ "type": "context", "elements": [
{ "type": "mrkdwn", "text": "<https://github.com/.../pull/<n>|PR #<n>> · <https://linear.app/.../PF-<n>|PF-<n>>" }
]}
]
}
curl -sS https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
--data @/tmp/announce-payload.json
Parse the response; if ok: false, surface the error field and stop. Common failures: not_in_channel (add the bot), invalid_auth (token expired/scope missing), channel_not_found (wrong channel ID), invalid_blocks (file_id wrong or file not in shareable state — re-check step 3's response). Capture ts from the response for the permalink lookup.
After a successful post:
chat.getPermalink with channel=C0B78CER88H and message_ts=<ts>, print the permalink.The channel ID C0B78CER88H is hard-coded above. If the channel is renamed, the ID stays the same — nothing to do. If the channel is recreated, ask the user for the new ID and update the two references in this file (the Channel ID is … line and the chat.postMessage body).
If the bot isn't in the channel (not_in_channel on chat.postMessage), tell the user to /invite @<bot-name> in #pf-product-news — don't try to auto-join.
SLACK_BOT_TOKEN into a heredoc, an echo, or a gh command (it'd land in shell history). Always read from $SLACK_BOT_TOKEN inside the curl invocation only./open-pr. The skill is manual on purpose so the user can read the draft before it goes out.image blocks with slack_file references — the channel reads as one clean update per ship.curl -F to push bytes to Slack's external-upload URL. It silently uploads 0 bytes and files.completeUploadExternal returns ok: true anyway. Always --data-binary @<file>, and always size-check the response.ok: true on files.completeUploadExternal without verifying size > 0 per file. The API is generous about accepting broken uploads.conversations.list). It needs channels:read we didn't grant; the ID is hardcoded for a reason.