feature-tweet · git:20260608.962e3bf · 2026-06-08 · sha256 7c91e85906548977

feature-tweet git:20260608.962e3bfA

Immutable. This exact content is served forever at /api/v1/blob/7c91e85906548977.

---
name: feature-tweet
description: "This skill should be used when converting a merged, verified-live PR into a draft short-form X post (single tweet or up-to-3-tweet thread) for operator approval."
---

# feature-tweet

Convert a feature **just shipped to production** (a merged PR, verified live)
into a **draft** short-form X post — written to the existing
`knowledge-base/marketing/distribution-content/*.md` format and drained by the
existing `content-publisher.sh` cron. No new publishing path; nothing
reaches X until the operator flips `status: draft` → `scheduled`.

Invoked by `/soleur:postmerge` after its production-health check passes (only
tweet what actually deployed), and runnable standalone as a catch-up path:

```
/soleur:feature-tweet #<pr>
```

The brand-critical floor is the deterministic, fail-closed eligibility filter
`tweet-eligibility.sh` (under the repo's scripts/lib directory). A forbidden PR (security/infra/non-product/
unlabeled) is excluded **before** any draft is generated, so it never reaches
the operator's approval queue.

## Arguments

- `#<pr>` (required) — the merged PR number. Accept `123` or `#123`.
- `--headless` — non-interactive; never posts (drafts only, same as default).

## Step 1 — Eligibility (fail-closed, run FIRST)

```bash
bash scripts/lib/tweet-eligibility.sh <pr>
```

- Exit 0 + `eligible` → continue.
- Exit non-zero + `excluded: <reason>` → **stop. Write no file.** Print the
  reason and exit. This is a silent no-op from the operator's perspective (no
  draft, no error escalation) — exclusion is the designed outcome, not a fault.

Never second-guess or override the filter. Any uncertainty (missing labels,
`gh` error, deny label, deny path) is `excluded` by design.

## Step 2 — Idempotency

Search existing drafts for this PR before generating:

```bash
grep -rl 'pr_reference: "#<pr>"' knowledge-base/marketing/distribution-content/ 2>/dev/null
```

If a file matches, **no-op** with a message naming the existing path — do not
overwrite an operator-edited draft.

## Step 3 — Fetch PR context

```bash
gh pr view <pr> --json title,body,url
```

Read the author identity for any copy that references a person from
`plugins/soleur/docs/_data/site.json` → `author.name` ("Jean Deruelle"). **Never
infer a name** from the PR; contributor names are PII and are excluded.

## Step 4 — Generate the thread (sanitized, voice-aligned)

Follow `knowledge-base/marketing/brand-guide.md` → `### X/Twitter` and its
`#### Ship Tweets (feature-launch)` sub-section. Hard rules:

- **Shape:** single tweet (hook only) OR a numbered thread of **at most 3**
  tweets. Tweet 1 = the hook (no prefix, no "thread" announcement). Tweets 2–3
  are prefixed `2/`, `3/` on a fresh line (the canonical numbered format the
  publisher's `extract_tweets` parses).
- **280-character limit per tweet**, enforced during generation, not trimmed
  after.
- **Sanitization (mandatory — benefit only):**
  - No implementation/diff detail. State the user-facing benefit, not how it was
    built.
  - **No contributor names or author attribution** (PII; no marketing consent).
  - **No customer names** — run an explicit customer-name/NDA scan over the PR
    title and body; if a proper noun could be a customer, omit it.
  - **No naked numbers** — any statistic must be verifiable from the PR or
    omitted.
- Present-tense "just shipped X" framing; lead the build-in-public peer voice,
  land one concrete buyer benefit. General register (plain language).
- Links (if any) go in the **final** tweet only. A ship tweet has no blog, so
  `blog_url` is omitted entirely.

## Step 5 — Write the draft file

Path: `knowledge-base/marketing/distribution-content/<YYYY-MM-DD>-<slug>.md`
where `<slug>` is the PR title with its `feat(...)` prefix stripped and
slugified.

Frontmatter + body:

```markdown
---
title: "<concise benefit-framed title>"
type: feature-launch
publish_date: ""
channels: x
status: draft
pr_reference: "#<pr>"
issue_reference: "#<issue>"   # only if the PR closes one; else omit
---

<!-- To publish: set BOTH publish_date AND status: scheduled -->

## X/Twitter Thread

<hook tweet>

2/ <body tweet>

3/ <final tweet, link only if applicable>
```

`publish_date: ""` + `status: draft` is the intentionally-parked state
(`content-publisher.sh` skips it). `channels: x` only — Bluesky is deferred
(the publisher needs one body section per channel).

## Step 6 — Structural assertion (skill-owned gate — NOT lint)

```bash
bash scripts/lib/validate-tweet-draft.sh <file>
```

Asserts a non-empty `title`, `status: draft`, a `channels` value including `x`,
and a non-empty `## X/Twitter Thread` section. On non-zero exit, **delete the
file** (`rm -f <file>`) and abort — leave no partial draft. This is the field/
heading gate; the Liquid linter validates neither.

## Step 7 — Lint (Liquid markers)

```bash
bash scripts/lint-distribution-content.sh <file>
```

On non-zero exit, delete the file and abort.

## Output

On success, print the draft path and the explicit operator instruction:

> Draft written to `<path>`. To publish: set BOTH `publish_date` and
> `status: scheduled`. The existing content-publisher cron posts it on date.

Headless mode never posts — it only writes the draft and prints the path.

## Multi-PR contract (v1)

One tweet per eligible PR. `/soleur:postmerge` passes its single bound PR
number; batching multiple PRs from one deploy is deferred. A `/soleur:merge-pr`-
only flow bypasses the postmerge hook by design — run standalone
`/soleur:feature-tweet #<pr>` as the recovery path.