From 5d7a6d7860414f2a7731561997448351c28d5898 Mon Sep 17 00:00:00 2001 From: Hiroshi Nishio Date: Tue, 21 Apr 2026 19:12:36 -0700 Subject: [PATCH] Add HN body submission; rework social post rules toward business outcomes --- .github/scripts/extract-social-posts.js | 5 +- .github/scripts/post-hackernews.js | 10 ++-- CLAUDE.md | 68 ++++++++++++++++--------- pyproject.toml | 2 +- scripts/git/recent_social_posts.sh | 52 +++++++++++-------- uv.lock | 2 +- 6 files changed, 84 insertions(+), 55 deletions(-) diff --git a/.github/scripts/extract-social-posts.js b/.github/scripts/extract-social-posts.js index c306d1ce9..8b58c22ee 100644 --- a/.github/scripts/extract-social-posts.js +++ b/.github/scripts/extract-social-posts.js @@ -1,9 +1,9 @@ /** * Extracts social media posts from PR body. - * Matrix of author × platform: GitAuto/Wes × X/LinkedIn, plus an optional HN title. + * Matrix of author × platform: GitAuto/Wes × X/LinkedIn, plus an optional HN title + body. * * @param {string} body - PR body text - * @returns {{ gitautoX: string, gitautoLinkedIn: string, wesX: string, wesLinkedIn: string, hnTitle: string }} + * @returns {{ gitautoX: string, gitautoLinkedIn: string, wesX: string, wesLinkedIn: string, hnTitle: string, hnBody: string }} */ function extractSocialPosts(body) { const section = (label) => { @@ -17,6 +17,7 @@ function extractSocialPosts(body) { wesX: section("Wes on X"), wesLinkedIn: section("Wes on LinkedIn"), hnTitle: section("HN Title"), + hnBody: section("HN Body"), }; } diff --git a/.github/scripts/post-hackernews.js b/.github/scripts/post-hackernews.js index 95a7d193d..919fadc97 100644 --- a/.github/scripts/post-hackernews.js +++ b/.github/scripts/post-hackernews.js @@ -28,10 +28,10 @@ async function postHackerNews({ context }) { // Extract social media post from PR body if present const description = context.payload.pull_request.body || ""; - const { hnTitle } = extractSocialPosts(description); + const { hnTitle, hnBody } = extractSocialPosts(description); - if (!hnTitle) { - console.log("No HN Title section found in PR body, skipping HN post"); + if (!hnTitle || !hnBody) { + console.log("HN Title or HN Body section missing from PR body, skipping HN post"); await browser.close(); return; } @@ -40,9 +40,7 @@ async function postHackerNews({ context }) { const url = "https://gitauto.ai?utm_source=hackernews&utm_medium=referral" await page.fill('input[name="title"]', title); await page.fill('input[name="url"]', url); - - // If there's a description, submit as a "text" post with both URL and description - if (description) await page.fill('textarea[name="text"]', description); + await page.fill('textarea[name="text"]', hnBody); await page.click('input[type="submit"]'); await page.waitForLoadState("networkidle"); diff --git a/CLAUDE.md b/CLAUDE.md index 30f5cb551..9a879b8e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,15 +140,16 @@ assert find_test_files("foo.ts", all_files, None) == ["foo.test.ts"] 4. `git push` 5. `gh pr create --title "PR title" --body "" --assignee @me` — create PR immediately, no body 6. Check recent posts: `scripts/git/recent_social_posts.sh` -7. `gh pr edit --body "..."` — add summary and social posts after checking recent posts - - Technical, descriptive title. **No `## Test plan`**. - - **Five posts** (last section, customer-facing only) — four author × platform cells plus an HN title. See `## Social Media Rules` below for the voice, length, and style rules for each. Headers (parsed by `extract-social-posts.js`): +7. `gh pr edit --body "..."` — add social posts after checking recent posts + - Technical, descriptive title. **No `## Summary` and no `## Test plan`** — the commit message and diff already explain what changed; don't restate it. Body is just the social-post sections below. + - **Six sections** (customer-facing only) — four author × platform cells plus HN title and body. See `## Social Media Rules` below for the voice, length, and style rules for each. Headers (parsed by `extract-social-posts.js`): - `## Social Media Post (GitAuto on X)` - `## Social Media Post (GitAuto on LinkedIn)` - `## Social Media Post (Wes on X)` - `## Social Media Post (Wes on LinkedIn)` - `## Social Media Post (HN Title)` - - **Each section is independent.** If a section exists, that post fires; if it's missing, only that post is skipped. No all-or-nothing coupling. + - `## Social Media Post (HN Body)` + - **Each section is independent.** If a section exists, that post fires; if it's missing, only that post is skipped. No all-or-nothing coupling. HN is the one exception: both `HN Title` and `HN Body` must be present for the HN job to fire. 8. If Sentry issue: `python3 scripts/sentry/get_issue.py AGENT-XXX` then `python3 scripts/sentry/resolve_issue.py AGENT-XXX ...` 9. **Blog post** in `../website/app/blog/posts/`: - `YYYY-MM-DD-kebab-case-title.mdx`. Universal dev lesson, not GitAuto internals (exception: deep technical content). @@ -184,14 +185,29 @@ assert find_test_files("foo.ts", all_files, None) == ["foo.test.ts"] ## Social Media Rules -Five posts per PR — four cells of {GitAuto, Wes} × {X, LinkedIn}, plus one HN title. Don't copy text across platforms — each reader and format is different. Before writing, run `scripts/git/recent_social_posts.sh` and vary openers. +Six sections per PR — four cells of {GitAuto, Wes} × {X, LinkedIn}, plus HN title and body. Don't copy text across platforms — each reader and format is different. Before writing, run `scripts/git/recent_social_posts.sh` and vary openers. -**Shared rules (apply to all five):** +**Shared rules (apply to all sections):** - No em dashes (—). No marketing keywords. No negative framing. No internal names. - No small absolute numbers — use relative language ("30% faster", not "2s faster"). - Honest, technical tone. Specify model names (e.g., "Claude Opus 4.6"). - Customer-facing only — skip test/internal changes. +- **Banned openers** (do not start any post with these or close variants): + - "Spent the morning [verb-ing]…" and close variants ("Spent the day…", "Spent the weekend…", "Spent N hours…"). + Open with the concrete finding, the before/after, or the user-facing outcome. Never chronological autobiography. +- **No "we broke X and fixed it" framing.** Every PR is a fix of something, so postmortem-style posts on every PR turn the feed into a wall of failures and debrand the product — even when each story is individually true. Frame posts as **capability, outcome, or design decision**, not as confession: + - Capability: "GitAuto now X" / "X now handles Y case". + - Outcome: "30% faster PR review" / "200 PRs auto-merged last week". + - Design decision: "Why we chose X over Y" / "How X works under the hood". + Confession framing ("we broke X", "we had a bug where…", "we used to do X wrong") is reserved for rare cases where the lesson is genuinely novel and the bug affected customers visibly — and even then, lead with the principle, not the embarrassment. If the only angle is "we fixed our own bug," skip the post. +- **Every post needs business meaning, not just mechanism.** Technical detail without a "so what" reads as inside-baseball and loses non-engineers. Each post must tie the change to one of these outcomes for the reader's business: + - **Time**: dev hours saved, review latency cut, onboarding faster. + - **Money**: infra cost down, headcount load lighter, fewer paid incidents. + - **Risk**: fewer regressions in prod, fewer security holes, fewer compliance gaps. + - **Throughput**: more PRs shipped, more features out, faster release cadence. + - **Quality**: higher test coverage, cleaner diffs, fewer review cycles. + Don't stop at "we refactored the quality gate" — say what an engineering team gets from it (e.g., "your PRs now pass review in one round instead of three"). The mechanism is the evidence; the business outcome is the point. - **URLs are optional.** Most posts should be pure text. Include a URL only when the post genuinely points somewhere (blog post, docs page, demo). When included, put it inline in the text — X and LinkedIn both auto-expand naked URLs into preview cards from the destination's OG tags. ### GitAuto on X @@ -204,36 +220,42 @@ Five posts per PR — four cells of {GitAuto, Wes} × {X, LinkedIn}, plus one HN ### GitAuto on LinkedIn -- **Reader**: engineering managers, founders, buyers. Slower scroll, will click "see more". -- **Voice**: product credibility. "We shipped X because customers hit Y." +- **Reader**: engineering managers, founders, buyers. They care about team throughput, dev time, cost, risk — not about our internal mechanisms. Slower scroll, will click "see more". +- **Voice**: business outcome for the customer. "Your team gets X" / "Engineering leaders can now Y". Not a technical write-up. - **Length**: **400-800 chars**. Line breaks between sentences for scannability. -- **Format**: lead with the user-facing outcome, then the technical specifics, then the link. Concrete lessons from real incidents land well. -- **Avoid**: hashtag spam, emoji walls, tweet-style copy without line breaks, thought-leadership platitudes. +- **Format**: lead with the outcome for their team (hours saved, PRs shipped, regressions avoided, cost cut). Keep any mechanism to a single clause, only if it's the evidence that the outcome is real. Skip code, skip internal terminology, skip model names unless the reader would recognize them. +- **Avoid**: technical deep-dives, mechanism-first framing, hashtag spam, emoji walls, tweet-style copy without line breaks, thought-leadership platitudes, postmortems of our own bugs. ### Wes on X - **Reader**: devs following a build-in-public founder. Looking for signal, humor, and real stories. -- **Voice**: personal. First person. Don't emphasize "GitAuto" — the post is about what you built or broke, not the product. +- **Voice**: personal. First person. Don't emphasize "GitAuto" — the post is about what you built, not the product. - **Length**: Wes has **X Premium** so long-form is allowed (up to 25k chars). Use **first 280 chars as a hook** (this is what shows above "Show more"). Expand below the fold with the full story if the lesson deserves it. -- **Format**: hot takes, counterintuitive findings, specific before/after snippets, self-deprecating failure stories work best. -- **Avoid**: corporate voice, LinkedIn-style "I learned that…" openers, generic advice, starting with the same opener as recent posts. +- **Format**: counterintuitive findings, specific before/after snippets, design-decision takes. Frame as "here's a thing I figured out" rather than "here's a bug I had". +- **Avoid**: corporate voice, LinkedIn-style "I learned that…" openers, generic advice, starting with the same opener as recent posts, confession-style "I broke X" framing. ### Wes on LinkedIn -- **Reader**: professional network. Engineering leaders who know Wes as a technical founder. -- **Voice**: personal, narrative. Don't emphasize "GitAuto". +- **Reader**: professional network. Engineering leaders, founders, and operators. They want perspective on how to run a product, a team, or a technical business — not a code walkthrough. +- **Voice**: personal, narrative, founder-operator. Don't emphasize "GitAuto". Don't emphasize code. - **Length**: **600-1200 chars**. Line breaks between thoughts. -- **Format**: problem → discovery → fix arc. Honest stories of what broke and what you learned. -- **Avoid**: hashtags, selling, tweet-style copy, overly polished "lesson learned" framing. +- **Format**: observation → implication for how you run engineering/product/business. A decision, a tradeoff, or a principle framed at the team/org/company level, not at the code level. +- **Avoid**: technical deep-dives, code snippets, mechanism-first framing, hashtags, selling, tweet-style copy, overly polished "lesson learned" framing, confession-style "I broke X" framing. ### Hacker News -- **Reader**: senior engineers, skeptical, allergic to marketing. Title is ~90% of the click. -- **Header**: `## Social Media Post (HN Title)` — one line, no body. If missing, the HN job skips. -- **Length**: **≤80 chars**, hard cap (truncated in `post-hackernews.js`). -- **Voice**: technical and specific. Postmortem framing ("Why X broke and the 4-line fix") or "Show HN:" for tools. Never changelog-style, never promotional. -- **Avoid**: product names as the subject, marketing adjectives ("revolutionary", "powerful", "simple"), vague claims. The HN crowd downvotes anything that smells like content marketing. -- **Why separate from GitAuto X**: changelog voice (280 char) trims poorly to 80, and HN's audience wants a different framing than a product announcement. +- **Reader**: senior engineers, skeptical, allergic to marketing. Title is ~90% of the click, but a thin body gets flagged as an ad. +- **Format**: submitted with title + URL + body text. `post-hackernews.js` hardcodes the URL to `https://gitauto.ai?utm_source=hackernews&utm_medium=referral`, so the author only writes title and body. Both sections must be present or the HN job skips. +- **`## Social Media Post (HN Title)`**: + - **Length**: **≤80 chars**, hard cap (truncated in `post-hackernews.js`). + - **Voice**: technical and specific. Design-decision framing ("Why X uses Y instead of Z"), mechanism framing ("How X handles Y"), or "Show HN:" for tools. Never changelog-style, never promotional. Postmortem framing about our own bugs is reserved for when the bug was externally visible and the lesson is genuinely novel — don't default to it. + - **Avoid**: product names as the subject, marketing adjectives ("revolutionary", "powerful", "simple"), vague claims. +- **`## Social Media Post (HN Body)`**: + - **Length**: **600-2000 chars**. Appears as the top comment on the submission (HN's behavior when both URL and text are provided), so it should stand on its own as a short engineering write-up. + - **Voice**: engineering write-up. Lead with the concrete problem space, explain the tradeoff or mechanism, show the approach. First person singular or plural is fine. + - **Format**: prose with short paragraphs. Inline code fences are OK for short snippets. No lists of bullet points dressed up as a post. + - **Avoid**: restating the title, cross-posting X/LinkedIn copy, extra links back to `gitauto.ai` (URL is already set), emoji, hashtags, confession-framing about our own bugs. +- **Why separate from GitAuto X**: changelog voice (280 char) trims poorly to 80, and HN's audience wants a design/mechanism write-up rather than a product announcement. ## CRITICAL: Fixing Foxquilt PRs diff --git a/pyproject.toml b/pyproject.toml index 1995c7a9b..0dc88599a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "GitAuto" -version = "1.55.4" +version = "1.55.5" requires-python = ">=3.14" dependencies = [ "annotated-doc==0.0.4", diff --git a/scripts/git/recent_social_posts.sh b/scripts/git/recent_social_posts.sh index 7384177ed..534b4fbdd 100755 --- a/scripts/git/recent_social_posts.sh +++ b/scripts/git/recent_social_posts.sh @@ -1,31 +1,39 @@ #!/bin/bash -# Usage: ./recent_social_posts.sh [gitauto|wes] -# Shows recent social media posts from merged PRs. -# Without argument: shows both GitAuto and Wes posts. -# With "gitauto": shows only GitAuto posts. -# With "wes": shows only Wes posts. +# Usage: ./recent_social_posts.sh [section-key] +# Shows recent social media posts from merged PRs, grouped by section. +# +# Section keys: gitauto-x, gitauto-linkedin, wes-x, wes-linkedin, hn +# Without argument: shows all five sections sequentially. FILTER="${1:-}" -# No argument: run both and exit -if [ -z "$FILTER" ]; then - "$0" gitauto - echo "" - echo "===" - echo "" - "$0" wes - exit 0 -fi +case "$FILTER" in + "") + "$0" gitauto-x + echo ""; echo "==="; echo "" + "$0" gitauto-linkedin + echo ""; echo "==="; echo "" + "$0" wes-x + echo ""; echo "==="; echo "" + "$0" wes-linkedin + echo ""; echo "==="; echo "" + "$0" hn + exit 0 + ;; + gitauto-x) SECTION="## Social Media Post (GitAuto on X)" ;; + gitauto-linkedin) SECTION="## Social Media Post (GitAuto on LinkedIn)" ;; + wes-x) SECTION="## Social Media Post (Wes on X)" ;; + wes-linkedin) SECTION="## Social Media Post (Wes on LinkedIn)" ;; + hn) SECTION="## Social Media Post (HN Title)" ;; + *) + echo "Unknown section key: $FILTER" >&2 + echo "Valid keys: gitauto-x, gitauto-linkedin, wes-x, wes-linkedin, hn" >&2 + exit 1 + ;; +esac -if [ "$FILTER" = "gitauto" ]; then - SECTION="## Social Media Post (GitAuto)" -elif [ "$FILTER" = "wes" ]; then - SECTION="## Social Media Post (Wes)" -else - SECTION="## Social Media Post" -fi +echo "[$FILTER]" -# Fetch PR bodies as JSON array, then extract posts with separators using Python export SECTION gh pr list --state merged --limit 10 --json body --jq '.[].body' \ | python3 -c ' diff --git a/uv.lock b/uv.lock index 214411680..8d2e5c76b 100644 --- a/uv.lock +++ b/uv.lock @@ -596,7 +596,7 @@ wheels = [ [[package]] name = "gitauto" -version = "1.55.4" +version = "1.55.5" source = { virtual = "." } dependencies = [ { name = "annotated-doc" },