Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .claude/skills/announce/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
name: announce
description: >
Writes the announcement for a TablePro Mac release: the release post in resources/blog/ with its
figures, then the newsletter and the X posts written from it, and the checks that keep all three
true. Use when the user says "announce 0.82", "blog post for the release", "release post for
0.82", "newsletter for 0.82", "write the X thread", "X posts for 0.82", hands over an X draft to
polish, or asks whether a release is worth announcing. Not for cutting the release itself: that is
the release skill in TableProApp/TablePro.
---

# Announce

The post is the announcement; the newsletter and the X posts are written from the finished post, so
the three cannot disagree. Order: post, newsletter, X.

Commands run from this repository's root. `../TablePro` and `../tablepro-docs` are the app and docs
checkouts beside it: confirm the paths with the user and `git pull --ff-only` both first.

## Is it worth announcing

Worth a post when the shipped `### Added` has something a reader would change their behavior over (a
new pane or mode, a new way to connect, a new database, a rebuilt surface they will not recognize),
when something people rely on was removed or changed under them, or when a long-standing class of
bugs users have asked about is gone. Otherwise the changelog and the docs changelog are the
announcement. The entry count per section is a sanity check, not the rule.

Say which way you went and why before any work, and ask when it is borderline: an unwanted
newsletter costs an hour, a skipped one costs a launch.

## Blockers

Nothing goes out until each holds. Report them apart from the drafts.

- The release is live. In `php artisan release:check`, the live value of the GitHub
`releases/latest` and Sparkle appcast `version` rows is the new version. A DRIFT on those rows
against an old `resources/data/platforms.json` is the site's own follow-up, not a blocker.
- The docs changelog is deployed with the version:
`curl -s https://docs.tablepro.app/changelog | grep -c 'v<version>'` prints more than 0.
- `references/fact-checks.md` holds for every claim, plugin and screenshot.
- The fix count is recounted from the shipped section of `../TablePro/CHANGELOG.md`.

`main` deploys itself on green tests, so merging the post publishes it. A post that says "update
now" before the build exists sends readers to the previous version.

## 1. The post

`references/blog-post.md`: one file in `resources/blog/`, its figures and its test edits, through a
pull request.

Once it is live, link it from the version's `<Update>` block in `../tablepro-docs/changelog.mdx`:
`[Read the announcement](https://tablepro.app/blog/<slug>)` on its own line before the first `###`,
committed as `docs(changelog): link the <version> announcement`.

## 2. The newsletter, 3. the X posts

`references/newsletter-and-x.md`. Drafts go in the session scratchpad, named by version
(`newsletter-v0.82.0.md`, `x-posts-v0.82.0.md`), never in this repository, which is public.

Sending is out of scope. If the user asks to send the newsletter, warn them first: in the license
backend the `all` audience means verified subscribers only, a small fraction of the list, and
`everyone` is the real list.

## Checks on every draft

```bash
python3 .claude/skills/announce/scripts/lint-draft.py --blog resources/blog/<slug>.md
python3 .claude/skills/announce/scripts/lint-draft.py <scratchpad>/newsletter-v<version>.md
python3 .claude/skills/announce/scripts/lint-draft.py --x <scratchpad>/x-posts-v<version>.md
../TablePro/scripts/check-banned-words.sh resources/blog/<slug>.md <scratchpad>/*-v<version>.md
```

`lint-draft.py` checks style, budgets, figures and docs links, and exits 1 on anything under
"Fix". `check-banned-words.sh` reads the one banned-word list, `../TablePro/scripts/banned-words.txt`.
Then work `references/fact-checks.md` top to bottom.
116 changes: 116 additions & 0 deletions .claude/skills/announce/references/blog-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Release post

A release post is one file, `resources/blog/<slug>.md`, English only. `BlogService` globs the
directory, so there is no index or route to edit. It carries the detail, the figures and the limits;
the newsletter and X posts are written from it.

Three things get a post sent back: no figures, copy that runs long or reads like marketing, and a
layout invented instead of matched to the shipped posts.

## 1. Read first

```bash
ls resources/blog/tablepro-0-*.md
grep -n "^## \|^<asset-slot" resources/blog/tablepro-0-*.md
```

Read the newest release post in full. Match its shape, its frontmatter and its figure wiring.

## 2. What to feature

Pick from the shipped `CHANGELOG.md` section in `../TablePro`, not from memory.

- **Prose plus a figure** for something a reader will change their behavior over: three or four
features at most. Lead with what the most users touch, not with what took the most work.
- **One bullet** for everything else, in "Also new".
- **Fixes worth knowing about**: the few a reader hit, written as what used to go wrong.
- **A bold one-liner** for a breaking change or removal, so it cannot be skimmed past.

At least a 3x spread in words between the biggest and the smallest section; under 2x reads as
machine-written. State the behavior and stop: rationale once per post, not per section.

## 3. Shape

```
intro "TablePro 0.81 is out: 126 changes, 67 of them fixes." then one paragraph
<asset-slot> the hero, right after the intro
## <feature> one per featured item, figures between them
## Also new bullets
## Fixes worth knowing about
## What it will not do real gaps, not disguised strengths
## Getting it the update path, and plugins with updates in Settings > Plugins
```

Headings are plain statements or names. No questions, slogans or gerund stacks.

## 4. Frontmatter

```yaml
---
slug: tablepro-0-81 # equals the filename
title: "TablePro 0.81: Tabs per Database, Firebase and Bigtable" # quote a title with a colon
description: One or two sentences, at most 155 characters (SeoLengthsTest).
date: 2026-10-16 # the publish date, never changed later
release: "TablePro 0.81" # marks a release post; a guide has none
author: TablePro Team
tags: [release, connections, oracle]
ogPunchline: Two or three short sentences under about 90 characters. The OG card subtitle.
---
```

- Every tag after `release` is a key of `PostTopics::PAGES` or an engine id in
`resources/data/engines.json`; `BlogTest` fails on any other.
- `ogPunchline` is what someone reads on a shared link, not the description restated. It counts no
engines (`NoTypedCountsTest`), and neither field makes a privacy claim (`BannedClaimsTest`).

## 5. Figures

Required: never open the post's pull request without them. The screenshots usually exist on the
feature pull requests in TableProApp/TablePro: `gh pr view <n> --json body`, then download the
`user-attachments` URLs with `Authorization: token $(gh auth token)`.

- Look at each shot before using it. Crop out a home folder path, a version label from the build
that took it, and any backdrop behind a sheet. Sample data only.
- A figure is `<asset-slot id="blog-<slug>-N"></asset-slot>` in the post plus an entry in
`resources/data/assets.json`. Copy the newest post's `blog-<slug>-N` entries and change the key,
`usedOn`, `aspect` (the source's `W:H`), `replacement.base` (the new key), `src`, `legacySource`,
`description`, `alt` and `caption`.
- Files: the PNG at `public/images/blog/<name>.png`, and `blog-<slug>-N-light-{704,1408}.{avif,webp}`
beside it, each under 200 KB (`cwebp`, `avifenc`). The `figure` kind in `docs/visual-assets.md`
is the spec.
- Alt text describes the picture in one long, specific sentence: what is on screen. The caption says
what the picture proves, in one sentence that stands alone.
- Then `php artisan assets:generate`. A slot left as a placeholder renders nothing in production.

## 6. Voice

Dev to dev, plain and short: about 900 words at most, three or four figures.

- Second person, present tense, contractions. No marketing, no hype, no first person.
- Numbers instead of adjectives: "14 MB instead of a 32 MB zip", never "much faster".
- Backticks on shortcuts, SQL keywords, identifiers, paths and config keys. Bold for menu paths
(`**Settings > Appearance**`) and UI labels.
- Bullets for limits and fixes. Tables only for tabular things.
- State a limit flatly. Never call a competitor bad; say what it costs.
- No em dashes, no banned words, US spelling.

## 7. Wire it up

1. `tests/Feature/Landing/BlogTest.php`:
- the slug and date at the top of `BLOG_PUBLISHED`;
- in the `/blog` test, the newest post's `dateFormatted`;
- in the `/vi/blog` test, `posts.0` (slug, url, title, Vietnamese date) and the `slice(N, $guides)`
offset, which counts the release posts newer than the newest guide.
2. Render the OG card and look at it: a long title wraps to three lines and can push the punchline
off. `WEB_DOMAIN=tablepro.app php artisan og:generate --type=blog --slug=<slug>`.
3. Run:

```bash
python3 .claude/skills/announce/scripts/lint-draft.py --blog resources/blog/<slug>.md
../TablePro/scripts/check-banned-words.sh resources/blog/<slug>.md
php artisan test --compact --filter='BlogTest|SeoLengthsTest|OgCardsTest|AssetManifestTest'
php artisan test --compact
```

Then `references/fact-checks.md`. The pull request body lists what the post covers, the figures and
the test edits, a few bullets.
87 changes: 87 additions & 0 deletions .claude/skills/announce/references/fact-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Fact checks

`lint-draft.py` checks how a sentence reads; this file checks whether it is true. Work it top to
bottom on the post first, then on the newsletter and the X posts written from it. `../TablePro` and
`../tablepro-docs` are the app and docs checkouts, pulled to their `main`.

## 1. A fix is tied to a version only when git agrees

```bash
git -C ../TablePro log --oneline -S "<symbol the fix touched>" -- <path>
git -C ../TablePro show <previous-tag>:<path> | grep -n "<the old code>"
```

If the broken code is also at the tag before that, it was never a regression. Say what was wrong
without naming a version.

## 2. A behavior change matches what the old code did

Before grouping several things under one verb ("joins, unions and CTEs are read-only now"), read the
diff and confirm each one changed. A summary easily drops the changelog's scope.

## 3. A plugin feature needs a published binary

It is not installable until its tag exists, the tag contains the commit, and the registry lists it:

```bash
git -C ../TablePro tag -l "plugin-<slug>-*" | tail -3
git -C ../TablePro merge-base --is-ancestor <feature-commit> plugin-<slug>-v<latest> && echo included || echo NOT included
curl -s https://raw.githubusercontent.com/TableProApp/plugins/main/plugins.json | python3 -c \
'import json,sys; [print(p["id"], p["version"]) for p in json.load(sys.stdin)["plugins"] if "<Name>" in p["id"]]'
```

The registry `id` is the `bundleId` in `../TablePro/.github/plugin-registry.json`. If a check fails,
the plugin ships first or the section goes.

## 4. Images are real and current

A figure is taken on a build that contains the change; compare the shot with the pull request that
changed the UI it shows. An `images/*.png` in `../tablepro-docs` at exactly 1560x960 is a
"Screenshot coming soon" card, and `lint-draft.py` flags one a draft links. A real docs image can
still be stale:

```bash
git -C ../tablepro-docs log -1 --format='%ad %s' --date=short -- images/<file>.png
```

Never ship a placeholder or a stale shot. Mark the gap and report it as a blocker.

## 5. Menu labels come from the app

The docs paraphrase, so take the string from the menu builder. Watch for labels that change by
engine: "Close Tabs for Other Databases" reads "Other Schemas" on Oracle, Dameng and BigQuery.

```bash
grep -rn "String(localized:" ../TablePro/TablePro/Core/Menu/ | grep -i "<the item>"
```

## 6. Tiers, defaults and thresholds

```bash
grep -n -A3 "case <feature>" ../TablePro/TablePro/Models/Settings/ProFeature.swift
```

- `requiredTier` is the lowest tier that includes a feature, and Team includes Starter: a Starter
feature "needs a license, and both tiers include it", a Team feature needs Team.
- Describe a gate as it behaves: a locked overlay is not a disabled menu item.
- Quote the number that decides a behavior, or none, after reading every constant in the model
file, including the default range a comparison runs over.

## 7. Every URL resolves

`lint-draft.py` resolves each `docs.tablepro.app` page and image against `../tablepro-docs`. For an
anchor, read the headings:

```bash
grep -n "^## " ../tablepro-docs/<section>/<page>.mdx
```

The docs `changelog.mdx` carries the version only after the release-day docs deploy, so that link is
a blocker until then, not a broken link.

## 8. Counts and references

- Count the bullets under `### Fixed` in the shipped section of `../TablePro/CHANGELOG.md` and write
"N fixes", never "N bugs fixed": one bullet can cover several issues.
- Never inflate the count with issue or commit counts.
- Leave issue numbers out of every announcement; changelog references are sometimes wrong.
101 changes: 101 additions & 0 deletions .claude/skills/announce/references/newsletter-and-x.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Newsletter and X posts

Both are short and both point at the post. Write them from the finished post, not from the changelog,
so the three cannot disagree.

## The newsletter

**150 to 250 words**, one phone screen. It says a version exists, names the headline items one line
each, and links the post.

```markdown
**Subject:** TablePro <version>: <two or three plain items>

**Preview text:** <what the subject did not say>

---

# TablePro <version>

<One sentence naming what the release is about, in different words from the subject.>

- **<Feature>**: <one line, what it does for you.>
- **<Feature>**: <one line.>
- **<Feature>**: <one line.>

<One line on the rest: the fix count, or the one breaking change.>

[Read the full post](<post url>)

Update from **TablePro > Check for Updates**, or [download it](https://tablepro.app/download).
```

- **The bullets are the whole body**: three to five, in the post's order. No headings, images or
tables. A bullet that needs a second sentence belongs in the post.
- **Put these in the mail itself**, because the readers who do not click are the ones they hurt: a
breaking change or removal, and a one-time reset or migration with what it does not affect.
- **Subject**: `TablePro <version>: ` plus two or three plain items, lowercase after the colon, comma
separated, no adjectives. Gmail on iOS cuts near 40 characters, so the first item finishes inside
that. Every item in the subject is a bullet.
- **Preview text**: 40 to 90 characters that add what the subject left out. Nothing load-bearing
lives only here: Apple Intelligence can replace it with a generated summary.

### Newsletter voice

Each rule is a yes or no against the draft, and a no is a rewrite.

1. The opener names what the release is about in different words from the subject.
2. No first person: the subject of a sentence is the product, the feature, or "you".
3. Nothing tells the reader what is worth their time or how they will feel ("fixes you will
notice").
4. No em dashes, semicolons, exclamation marks or emoji.
5. Figures instead of vague quantifiers: "more than 130 other fixes", not "many".
6. No prose sentence over 35 words. An enumeration after a colon may run longer.
7. Name the old behavior beside the new one where it fits in one line.
8. Second person, present tense, active voice. No "will now", no "has been improved".
9. Backticks on shortcuts, SQL keywords and literals (`Cmd+F`, `EXPLAIN ANALYZE`). Bold for menu
paths and the first mention of a new proper noun, not for run-in headlines.
10. US spelling (color, behavior), as in the docs and the app.

## The X posts

A thread of **four to six posts** plus one standalone, one feature per post.

**Voice: an indie developer telling other developers what shipped.** Casual and plain, the way a dev
types on X: "you can now", "handy when", contractions, short lines. First person is fine here, and so
is a question opener or a little humor. Humble, not promotional: no PR or AI phrasing, no thread
hooks ("a thread", "here's why this matters"), no engagement bait, no "and much more", no hashtags
or emoji unless one really fits. Not every post has to read as an announcement.

- **Order by reach**: the features most users touch first (totals, tabs, import), niche ones (one
engine's sign-in, a socket option) in the round-up.
- **280 characters at most** per post (a link counts 23), and one needing 279 is too dense.
- **No markdown**: X renders none of it.
- **Links only in the last post** and the standalone. An earlier link costs reach.
- **One image per post at most**, named in the post header, and only images that exist and are
current.
- **The first post works alone**: the version and the biggest change in its first two lines.
- **The standalone post** is the version, three or four changes in plain nouns, then the link. It is
the one that gets reposted, so it needs no context.

```markdown
## 1/5 (attach: <image file, or omit>)

TablePro 0.82 is out.

<The headline change, two sentences.>

## 5/5

Also in 0.82: <three or four items, comma separated>

Full post: <post url>
Download: https://tablepro.app/download

## Standalone

TablePro 0.82: <three or four changes>. <post url>
```

When the user hands over their own draft to polish, keep its meaning, fix the wording, and reply
with the final text only, ready to post.
Loading