---
phase: 03-public-launch
plan: 02
subsystem: launch-content
tags: [dog-04, show-hn, tweet-thread, blog-post, launch-telemetry, discord-alert]
type: execute
wave: 2
depends_on: [03-01, 03-03]
files_modified:
  - app/blog/claude-code-beachhead/page.js
  - app/blog/layout.js
  - docs/launch/hn-post.md
  - docs/launch/tweet-thread.md
  - docs/launch/blog-post.md
  - app/lib/notification-adapters/discord.js
  - app/api/actions/route.js
  - app/lib/repositories/actions.repository.js
  - __tests__/unit/blog-post-claude-code-beachhead.test.jsx
  - __tests__/unit/connect-complete-discord-alert.test.js
  - __tests__/unit/launch-content-assertions.test.js
  - scripts/check-launch-content.mjs
autonomous: false
requirements: [DOG-04]
nyquist_compliant: true
wave_0_complete: true

user_setup:
  - service: hackernews
    why: "Show HN submission (D-17 Tue-Thu 8-11am ET); Wes-authored (D-19)"
    env_vars: []
    dashboard_config:
      - task: "Post Show HN with homepage URL (title ≤ 80 chars, 60-75 recommended per Pitfall 10)"
        location: "https://news.ycombinator.com/submit — Tue, Wed, or Thu during 8-11am ET window"
      - task: "Reply cadence — every top-level comment within first 30 min (research §Launch Mechanics: tighten to 5 min during first 2 hours)"
        location: "HN post thread"
  - service: x-twitter
    why: "Launch tweet thread (D-18); Wes-authored founder-voice"
    env_vars: []
    dashboard_config:
      - task: "Post tweet thread (6-10 tweets) aligned with prepared draft; pin first tweet for launch window"
        location: "https://x.com"
  - service: discord-webhook
    why: "Launch-window new-connect alert (Claude's Discretion from CONTEXT.md — optional instrumentation)"
    env_vars:
      - name: DASHCLAW_ALERTS_DISCORD
        source: "Discord webhook URL in a channel Wes monitors — already documented in .env.example from Phase 2"
    dashboard_config: []

must_haves:
  truths:
    - "A blog post on dashclaw.io at /blog/claude-code-beachhead exists with problem → demo → dogfood story structure (D-20)"
    - "The blog post embeds the published flagship video (same VideoHero or equivalent iframe)"
    - "Draft HN post body + tweet thread + blog post all committed to docs/launch/ BEFORE the launch-day checkpoint"
    - "HN post title is ≤80 chars and follows 'Show HN: <Product> – <value prop>' format (Pitfall 10)"
    - "All 3 launch content pieces (HN body, tweet thread, blog post) include the MON-01 50-integration trigger commitment (D-03 location 2)"
    - "Tweet thread first tweet opens with a concrete problem (Pitfall 11: not a company intro)"
    - "Discord new-connect alert fires on first action_record per org (optional instrumentation per Claude's Discretion)"
    - "Hard-ordering gate held: 03-01 homepage live + incognito-verified + /pricing live BEFORE HN submission (Pitfall 1: HN URL-change after submission kills rank)"
  artifacts:
    - path: "app/blog/claude-code-beachhead/page.js"
      provides: "Live dashclaw.io/blog/claude-code-beachhead blog post (D-20)"
      exports: ["default"]
      min_lines: 80
    - path: "app/blog/layout.js"
      provides: "Blog segment layout (PublicNavbar + PublicFooter + container) — enables future blog posts without re-wiring"
      exports: ["default"]
    - path: "docs/launch/hn-post.md"
      provides: "Show HN post body draft (150-300 words, problem → demo → differentiation → ask for feedback)"
      min_lines: 20
    - path: "docs/launch/tweet-thread.md"
      provides: "6-10 tweet thread draft (first tweet = concrete problem, last tweet = 50-integration trigger commitment + dashclaw.io link)"
      min_lines: 30
    - path: "docs/launch/blog-post.md"
      provides: "Working doc for the blog post copy — source of truth for app/blog/claude-code-beachhead/page.js content"
    - path: "__tests__/unit/blog-post-claude-code-beachhead.test.jsx"
      provides: "Blog post renders; contains trigger text; embeds video; same voice as homepage"
    - path: "__tests__/unit/launch-content-assertions.test.js"
      provides: "Static content assertions on docs/launch/* — title length, trigger mention, first-tweet shape"
    - path: "__tests__/unit/connect-complete-discord-alert.test.js"
      provides: "Discord alert fires on first per-org action_record (launch-window telemetry)"
    - path: "scripts/check-launch-content.mjs"
      provides: "Runnable guardrail asserting launch content is ready for same-day blitz (D-16)"
  key_links:
    - from: "app/blog/claude-code-beachhead/page.js"
      to: "app/components/VideoHero.js (from 03-01)"
      via: "import + render"
      pattern: "VideoHero|<iframe"
    - from: "docs/launch/hn-post.md"
      to: "dashclaw.io homepage URL"
      via: "post body links to homepage"
      pattern: "dashclaw\\.io"
    - from: "docs/launch/hn-post.md + tweet-thread.md + blog-post.md"
      to: "50-integration trigger commitment"
      via: "explicit mention per D-03 location 2"
      pattern: "50 verified"
    - from: "app/api/actions/route.js (POST handler)"
      to: "DASHCLAW_ALERTS_DISCORD webhook"
      via: "fire after first action per org"
      pattern: "DASHCLAW_ALERTS_DISCORD|new-connect-alert"
---

<objective>
Ship the coordinated launch content (Show HN post draft, tweet thread draft, blog post on dashclaw.io) and opt-in launch-window instrumentation (Discord alert on first per-org `/connect` completion). All 3 content pieces draft and commit BEFORE the launch day; posting happens in the same-day 2-hour blitz (D-16) as a human-action checkpoint.

**Purpose:** DOG-04 is the launch itself. The blog post lives in the repo and ships with the homepage rewrite. HN and tweet content are prepared drafts committed to `docs/launch/*` so the launch-day human action is copy-paste, not improvisation. The Discord new-connect alert provides launch-window signal that Vercel Analytics alone doesn't (Pitfall 12 — analytics is page-view only).

**Depends on:** Plan 03-01 (homepage + video + CSP live + incognito-verified) AND Plan 03-03 (/pricing live with counter). HN URL-change after submission kills ranking (Pitfall 1) — hard gate.

**Output:**
- New blog post at `app/blog/claude-code-beachhead/page.js` (A1 resolution: in this monorepo, no sibling repo exists)
- New blog segment layout `app/blog/layout.js` (reusable for future posts)
- Draft HN post + tweet thread + blog post copy in `docs/launch/` (source of truth)
- Discord webhook adapter hooked to fire on first per-org action (optional instrumentation)
- Wave-0 tests covering content assertions
- `scripts/check-launch-content.mjs` runnable before launch day
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/STATE.md
@.planning/phases/03-public-launch/03-CONTEXT.md
@.planning/phases/03-public-launch/03-RESEARCH.md
@.planning/phases/03-public-launch/03-VALIDATION.md
@.planning/phases/03-public-launch/03-01-PLAN.md
@.planning/phases/03-public-launch/03-03-PLAN.md
@CLAUDE.md
@.impeccable.md
@docs/homepage-draft-claude-code.md
@app/guides/claude-code/page.js
@app/lib/notification-adapters/discord.js
@app/lib/discordApprovals.js
@app/api/actions/route.js
@app/lib/repositories/actions.repository.js

<interfaces>
<!-- Contracts executors use without exploring the codebase -->

From Plan 03-01 (dependency — will be GREEN before this plan runs):
```javascript
// app/components/VideoHero.js — SSR iframe wrapper, Loom/YouTube allowlist
export default function VideoHero({ src, title }): JSX.Element

// Hero video URL (backfilled by Plan 03-01 Task 4) — reusable in blog post
```

From existing Phase 2 code (verified):
```javascript
// app/lib/notification-adapters/discord.js — existing Discord webhook/bot helpers
// Mirror fireDiscordApproval pattern (app/lib/discordApprovals.js) for launch alert

// Env var already documented in .env.example per Phase 2
// DASHCLAW_ALERTS_DISCORD = <webhook URL>
```

From hooks/dashclaw_pretool.py:75 (verified):
```python
AGENT_ID default = "claude-code"
// First-action detection: see if any prior action_record exists for this org
```

From app/guides/claude-code/page.js (SSR pattern mirror for blog post):
```javascript
// Server component with PublicNavbar + PublicFooter + main content container
// Uses CSS tokens exclusively (no hardcoded hex after Phase 2 info-level note)
```

Show HN 2026 mechanics (from RESEARCH §Launch Mechanics):
- Title ≤80 chars (hard limit); target 60-75 for UI buffer (Pitfall 10)
- Title format: `Show HN: <Product> – <one-line value prop>`
- Post body: 150-300 words — Problem → demo link → differentiation → ask for feedback
- Day: Tue/Wed/Thu (D-17); Time: 08:00-11:00 ET (D-17); specific hour: ~09:00 ET sweet spot

Tweet thread mechanics (from RESEARCH):
- 6-10 tweets
- First tweet = concrete problem (Pitfall 11: NOT company intro)
- Last tweet = 50-integration trigger commitment + dashclaw.io link
- Founder-voice, first person
- Pin first tweet for launch window

Blog post mechanics (D-20 + RESEARCH):
- 600-1200 words
- H1 title / H2 sections (Problem / Demo / How it works / What's free / The 50-integration commitment / What's next)
- 1 image per 200-300 words; hero GIF + /decisions screenshot + /my-agent screenshot; all real dogfood, no stock
- Voice matches homepage (terse + technical)
- Posted MINUTES before HN so HN body can link to it
</interfaces>

<assumption_resolution>
**A1 RESOLVED (blog location):** This monorepo. Verified 2026-04-22 by inspecting the app/ tree (no app/blog/ segment) and the `../` parent directory (no dashclaw-website sibling repo). Plan 03-02 creates `app/blog/claude-code-beachhead/page.js` as a simple SSR segment route, following `app/guides/claude-code/page.js` pattern. Single post for Phase 3; blog layout is reusable for future posts. No MDX dependency added — content is JSX within the page.js.

**Counter (A8) / /pricing / requireTier:** Not this plan's concern — owned by 03-03.
</assumption_resolution>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Launch content drafts + static assertion tests + scripts/check-launch-content.mjs (RED → GREEN)</name>
  <files>docs/launch/hn-post.md, docs/launch/tweet-thread.md, docs/launch/blog-post.md, __tests__/unit/launch-content-assertions.test.js, scripts/check-launch-content.mjs</files>
  <read_first>
    - `.planning/phases/03-public-launch/03-CONTEXT.md` D-16 (same-day blitz 2-hour window), D-17 (HN Tue-Thu 8-11am ET), D-18 (tweet tone technical+specific+personal), D-19 (HN reply strategy), D-20 (blog post scope)
    - `.planning/phases/03-public-launch/03-RESEARCH.md` §Launch Mechanics (HN timing + title format + reply cadence; tweet thread structure; blog post structure)
    - `.planning/phases/03-public-launch/03-RESEARCH.md` Pitfalls 10 (HN title length), 11 (first tweet hook)
    - `docs/homepage-draft-claude-code.md` (806-word Phase 2 handoff — source material to draw from; re-verify per Pitfall 8)
    - `README.md` (3-step install block + Claude-Code-first lead — blog post's "how it works" section should echo this, not contradict)
    - `.planning/PROJECT.md` + Plan 03-03 updates (trigger commitment exact text to mirror)
    - Examples of existing Phase 2 docs (`docs/homepage-draft-claude-code.md`) for voice calibration
  </read_first>
  <behavior>
    **docs/launch/hn-post.md behavior:**
    - File exists with structure: Title line + Body paragraphs
    - Title line format: `Show HN: DashClaw – <value prop>` — ≤80 chars, target 60-75 (Pitfall 10)
    - Body 150-300 words: Problem → demo link → differentiation → ask for feedback
    - Body contains "50 verified Claude Code integrations" (D-03 location 2 — launch content commitment trigger phrase)
    - Body ALSO contains the commitment clause — matches `/(pro|paid|monetization).*(?:launches|unlocks|fires|kicks in|when)/i` in the same document (D-03 full commitment, not just the trigger number — if the commitment clause is dropped post-draft the 4-location wall silently degrades to 3). Two-check approach chosen for readable test failures.
    - Body links to dashclaw.io homepage and dashclaw.io/blog/claude-code-beachhead
    - Body explicitly mentions the dogfood story (Wes's own use) per D-20

    **docs/launch/tweet-thread.md behavior:**
    - File exists with 6-10 numbered tweet entries
    - Tweet 1 opens with concrete problem, NOT company intro (Pitfall 11). Acceptable examples: "I got tired of Claude Code running rm -rf." or "Claude Code force-pushed to main. I built a pause button." (per D-18 sample direction)
    - Tweet 1 does NOT contain "Hi I'm" / "I'm building" / "Introducing" (test: negative regex)
    - Each tweet ≤ 280 chars (X's hard limit)
    - Last tweet (or the thread as a whole) contains BOTH: (i) "50 verified Claude Code integrations" trigger phrase AND (ii) the commitment clause `/(pro|paid|monetization).*(?:launches|unlocks|fires|kicks in|when)/i` — D-03 location 2 full commitment, not just trigger number. Last tweet also contains dashclaw.io link.
    - Middle tweets contain at least one concrete command example (e.g., "rm -rf", "git push --force", "npm install")

    **docs/launch/blog-post.md behavior:**
    - File exists with H1 title + at least 5 H2 sections (Problem, Demo, How it works, What's free, The 50-integration commitment, What's next — D-20 + RESEARCH)
    - Word count 600-1200 (D-20 range)
    - Contains BOTH: (i) "50 verified Claude Code integrations" trigger phrase AND (ii) the commitment clause `/(pro|paid|monetization).*(?:launches|unlocks|fires|kicks in|when)/i` — D-03 location 2 full commitment. Two separate assertions per the checker's readable-failure guidance.
    - Contains at least one "I" or "my" reference (founder-voice, first person per D-20)
    - References the flagship video (placeholder `<SCREENCAST_URL>` or by descriptive text — will be backfilled with real URL in Task 2)
    - Does NOT contain marketing voice giveaways: negative regex on /revolutionize|game-chang|next generation|cutting-edge/i

    **scripts/check-launch-content.mjs behavior:**
    - Reads all 3 docs/launch/*.md files
    - Verifies HN title line length ≤ 80 chars
    - Verifies each tweet ≤ 280 chars
    - Verifies blog post word count in [600, 1200]
    - Verifies all 3 files contain BOTH the trigger phrase ("50 verified Claude Code integrations") AND the commitment clause (`/(pro|paid|monetization).*(?:launches|unlocks|fires|kicks in|when)/i`) — D-03 full commitment wall
    - Verifies all 3 files contain NONE of the broadened secret-pattern set:
      ```javascript
      const SECRET_PATTERNS = [
        /DASHCLAW_API_KEY\s*=\s*\S{20,}/,
        /DISCORD_BOT_TOKEN\s*=\s*\S{40,}/,
        /DATABASE_URL\s*=\s*(postgres|postgresql):\/\/[^\s]+/,
        /sk-ant-[A-Za-z0-9]{48,}/,                                   // Anthropic
        /sk-[A-Za-z0-9]{40,}/,                                        // OpenAI
        /gh[oprsu]_[A-Za-z0-9]{36,}/,                                 // GitHub tokens
        /discord\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_\-]{60,}/,  // Discord webhook
      ];
      ```
    - Exits 0 if all checks pass; exits 1 with specific per-check error on failure
    - Runnable pre-launch as a go/no-go gate
  </behavior>
  <action>
    1. **Create `docs/launch/` directory** (simple mkdir, no scaffolding needed).
    2. **Create `docs/launch/hn-post.md`** — draft the HN post body:
       - Title: pick a candidate like `Show HN: DashClaw – The approval layer for Claude Code` (57 chars — safely under 80). Alternative: `Show HN: DashClaw – Govern Claude Code before it runs rm -rf` (60 chars). Executor picks one that aligns with homepage hero headline chosen in Plan 03-01 (voice consistency across surfaces — RESEARCH note).
       - Body structure:
         - Paragraph 1 (problem): Concrete Claude Code pain point — e.g., agent runs `rm -rf`, force-pushes to main, runs untrusted package install.
         - Paragraph 2 (demo): Link to the ≤3-min video at dashclaw.io + one-sentence overview. Link to dashclaw.io/blog/claude-code-beachhead.
         - Paragraph 3 (differentiation): Discord approval from phone in ≤10s; semantic guard free forever (bring your own LLM key); audit ledger.
         - Paragraph 4 (dogfood + commitment): Wes uses it daily. Free while we grow. **"Pro tier launches when we hit 50 verified Claude Code integrations in the wild."** Committed publicly on /pricing + in PROJECT.md + README.
         - Paragraph 5 (ask): "Honest feedback welcome. What breaks? What's missing?"
       - Voice: match homepage hero — terse, technical, first-person. No marketing adjectives.
    3. **Create `docs/launch/tweet-thread.md`** — 8 tweets (a balanced count within the 6-10 target):
       - Tweet 1 (hook, concrete problem): `I got tired of Claude Code running rm -rf. So I built a pause button. https://dashclaw.io` (or similar — Pitfall 11 compliance)
       - Tweet 2-3 (what it does): One-sentence behavior per tweet. Screenshot/gif placeholder markers.
       - Tweet 4-6 (specific examples): Real command / real block / real phone approval. `Claude Code tries: rm -rf node_modules // DashClaw: approval required // I tap Allow from my phone. 8 seconds. ✅`
       - Tweet 7 (dogfood): `I've been running this on my own Claude Code for months. The Discord approval flow is the demo — and it's live at [URL].`
       - Tweet 8 (commitment + link): `Free forever until 50 verified Claude Code integrations. Then Pro launches (multi-user orgs, custom policy packs, audit export). Committed publicly: dashclaw.io/pricing. Full story: [blog URL]`
       - Each tweet annotated with character count in a comment for verification
    4. **Create `docs/launch/blog-post.md`** — 800-word draft per D-20 structure:
       - H1: `DashClaw: govern Claude Code before it surprises you` (or similar; match homepage voice)
       - H2: Problem — Claude Code runs bash, edits files, installs packages. Most of the time this is fine. Sometimes it's not. I got tired of not knowing.
       - H2: Demo — The approval flow in 3 minutes. [Embed video] (placeholder marker — Task 2 backfills with real URL).
       - H2: How it works — Install hook + paste workspace token + configure Discord. 3 steps. Match README's 3-step block.
       - H2: What's free — Solo-dev Claude Code integration, Discord + Telegram approvals, /decisions ledger, **semantic guard with your own LLM key**, /activity + /my-agent. All free forever.
       - H2: The 50-integration commitment — Pro tier launches when DashClaw hits 50 verified Claude Code integrations in the wild. No time-boxed backstop. Committed at /pricing + PROJECT.md + README.
       - H2: What's next — Closed-loop growth flywheel seed sentence: "I'm building DashClaw-governed agents that do the research and content work for DashClaw itself. Phase 4. Public status page coming."
    5. **Create `__tests__/unit/launch-content-assertions.test.js`** — Node `readFileSync` + regex assertions per behavior section above. Cover all 3 files.
    6. **Create `scripts/check-launch-content.mjs`** — ES module mirroring `scripts/check-readme-lead.mjs` style. Runs the same assertions as the test file but exits 0/1 for pre-launch gate use. Usable as `node scripts/check-launch-content.mjs` in the launch-day runbook.
    7. Verify:
       ```bash
       npm test -- --run __tests__/unit/launch-content-assertions.test.js
       node scripts/check-launch-content.mjs
       ```
    8. Commit: `docs(03-02): add launch content drafts + assertion guardrail (DOG-04)`

    **Threat mitigation:** T-03-02-01 (secret leak in launch content) — drafts are reviewed by tests + pre-launch script that assert NONE of the broadened `SECRET_PATTERNS` set appears. Same regex set lives in BOTH `scripts/check-launch-content.mjs` AND `__tests__/unit/launch-content-assertions.test.js` (single source of truth — executor may factor to a shared constant or duplicate; both locations must stay in lockstep). Patterns cover: `DASHCLAW_API_KEY`, `DISCORD_BOT_TOKEN`, `DATABASE_URL` (postgres / postgresql), Anthropic `sk-ant-...`, OpenAI `sk-...`, GitHub tokens `gh[oprsu]_...`, and Discord webhook URLs. Applied to all 3 draft files.

    **Threat mitigation (D-03 full commitment wall):** The tightened two-check assertion (trigger phrase AND commitment clause, both required, in all 3 drafts) mitigates a silent-degradation failure mode where post-draft edits could drop the commitment clause and reduce the 4-location wall to 3 without any test failing. Readable test failures: separate assertions so the failure message names which half is missing.
  </action>
  <verify>
    <automated>npm test -- --run __tests__/unit/launch-content-assertions.test.js && node scripts/check-launch-content.mjs</automated>
  </verify>
  <done>
    - `docs/launch/hn-post.md` exists; title ≤ 80 chars; body 150-300 words; trigger commitment present; dashclaw.io links present
    - `docs/launch/tweet-thread.md` exists; 6-10 tweets; first tweet is concrete problem (no company intro); last tweet contains trigger commitment + dashclaw.io link; each tweet ≤ 280 chars
    - `docs/launch/blog-post.md` exists; 600-1200 words; ≥5 H2 sections; trigger commitment present; founder-voice first person
    - `scripts/check-launch-content.mjs` exists; exits 0 on all assertions
    - `__tests__/unit/launch-content-assertions.test.js` PASSES — including BOTH commitment-wall checks (trigger phrase + commitment clause) in all 3 draft files AND the broadened 7-pattern secret-regex set
    - `scripts/check-launch-content.mjs` mirrors the same two-check commitment assertion + same 7-pattern secret-regex set (single source of truth between script and test)
    - No secrets leaked in drafts (broadened `SECRET_PATTERNS` covers DASHCLAW_API_KEY, DISCORD_BOT_TOKEN, DATABASE_URL, sk-ant-, sk-, gh[oprsu]_, and Discord webhook URL shapes)
    - Commit lands as `docs(03-02): add launch content drafts + assertion guardrail (DOG-04)`
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Blog post live on dashclaw.io/blog/claude-code-beachhead (SSR) + video embed</name>
  <files>app/blog/claude-code-beachhead/page.js, app/blog/layout.js, __tests__/unit/blog-post-claude-code-beachhead.test.jsx</files>
  <read_first>
    - `app/guides/claude-code/page.js` full file (SSR page pattern mirror — PublicNavbar + main + PublicFooter)
    - `app/connect/page.js` (another SSR shell pattern reference)
    - `app/components/PublicNavbar.js` + `app/components/PublicFooter.js` (imports available)
    - `app/components/VideoHero.js` (shipped by Plan 03-01 — reusable on blog post)
    - `docs/launch/blog-post.md` (Task 1 source — blog post content JSX is a translation of this markdown into CSS-token-styled JSX)
    - `.impeccable.md` (design gate — CSS tokens only, no hardcoded hex)
    - `app/guides/claude-code/page.js:204` pre-existing `bg-[#0a0a0a]` — DO NOT mirror this violation (per Phase 2 VERIFICATION info-level note)
    - VideoHero src from Plan 03-01 Task 4 SUMMARY (the real EMBED_URL backfilled into app/page.js)
  </read_first>
  <behavior>
    **Blog post page (app/blog/claude-code-beachhead/page.js) behavior:**
    - Test: renders without error
    - Test: rendered HTML contains "50 verified Claude Code integrations" (D-03 location 2 echo)
    - Test: rendered HTML contains VideoHero iframe OR equivalent embed (assert `<iframe` with src matching loom.com or youtube-nocookie.com)
    - Test: rendered HTML contains ≥ 5 H2 section headings (matches D-20 structure)
    - Test: word count in main content (strip tags) in [600, 1200] range (D-20 length lock)
    - Test: founder-voice present — contains " I " or " my " or "I'm " at least 3 times (first-person per D-20)
    - Test: voice compliance — rendered HTML does NOT contain /revolutionize|game-chang|next generation|cutting-edge/i (rejected-framings discipline extended to blog post)
    - Test: NO hardcoded hex classes in the file (grep `#[0-9a-fA-F]{6}` returns zero matches within this file's content)

    **Blog layout (app/blog/layout.js) behavior:**
    - Test: wraps children in PublicNavbar + PublicFooter shell
    - Test: provides a main container with max-width suitable for long-form reading (prose width — typically max-w-3xl or similar)
  </behavior>
  <action>
    1. **Create `app/blog/layout.js`** — server component blog layout:
       ```javascript
       import PublicNavbar from '../components/PublicNavbar';
       import PublicFooter from '../components/PublicFooter';

       export default function BlogLayout({ children }) {
         return (
           <div className="min-h-screen bg-surface-primary text-text-primary">
             <PublicNavbar />
             <main className="px-6 pb-20 pt-28">
               <article className="mx-auto max-w-3xl prose prose-invert">
                 {children}
               </article>
             </main>
             <PublicFooter />
           </div>
         );
       }
       ```
       (Adjust class set to project's existing Tailwind `prose` variants if available, else use plain token-based spacing.)
    2. **Create `app/blog/claude-code-beachhead/page.js`** — SSR page rendering the blog content:
       - Translate `docs/launch/blog-post.md` (Task 1) into JSX
       - Use `<h1>`, `<h2>`, `<p>`, `<ul>`, `<li>`, `<code>` with appropriate token classes (text-text-primary, text-text-secondary, text-text-tertiary)
       - Embed `<VideoHero src={VIDEO_URL} title="DashClaw walkthrough" />` in the "Demo" H2 section — VIDEO_URL is the same real embed URL Plan 03-01 Task 4 backfilled into app/page.js
       - Links to `/guides/claude-code`, `/pricing`, and `dashclaw.io/pricing` where the trigger commitment is referenced
       - Metadata: `export const metadata = { title: '...', description: '...' }`
       - `export const dynamic = 'force-dynamic'` NOT needed — static render is acceptable; executor picks based on whether the post needs live data (it doesn't — the blog is static; /pricing has the live counter)
    3. **Create `__tests__/unit/blog-post-claude-code-beachhead.test.jsx`** covering behavior cases above. RTL render + string assertions.
    4. Verify:
       ```bash
       npm test -- --run __tests__/unit/blog-post-claude-code-beachhead.test.jsx
       npm test -- --run   # full suite non-regression
       npm run route-sql:check
       npm run docs:check
       ```
    5. **Livingcode auto-regen**: new app segment route may trigger regen — commit artifacts alongside.
    6. Commit: `feat(03-02): blog post on dashclaw.io/blog/claude-code-beachhead (DOG-04)`

    **Threat mitigation:** T-03-01-02 extends to blog — the blog post URL is ALSO linked from HN, so it must be live + incognito-verified before HN submission. Captured in Task 3 checkpoint precondition.
  </action>
  <verify>
    <automated>npm test -- --run __tests__/unit/blog-post-claude-code-beachhead.test.jsx && npm test -- --run</automated>
  </verify>
  <done>
    - `app/blog/layout.js` exists — PublicNavbar + PublicFooter + article container
    - `app/blog/claude-code-beachhead/page.js` exists — full blog post content in JSX + VideoHero embed with real URL
    - Blog post word count in [600, 1200]
    - Blog post contains 50-integration trigger + ≥5 H2 sections + founder-voice
    - NO hardcoded hex in blog post files
    - `__tests__/unit/blog-post-claude-code-beachhead.test.jsx` PASSES
    - Full suite `npm test -- --run` passes
    - Livingcode auto-regen artifacts committed alongside
    - Commit lands as `feat(03-02): blog post on dashclaw.io/blog/claude-code-beachhead (DOG-04)`
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: Discord new-connect alert (optional instrumentation per Claude's Discretion)</name>
  <files>app/api/actions/route.js, app/lib/repositories/actions.repository.js, app/lib/notification-adapters/discord.js, __tests__/unit/connect-complete-discord-alert.test.js</files>
  <read_first>
    - `app/api/actions/route.js` full file (POST handler — where createAction is called; hook site for alert)
    - `app/lib/repositories/actions.repository.js` (may need to add `isFirstActionForOrg(sql, orgId)` helper if not present)
    - `app/lib/notification-adapters/discord.js` (existing Discord webhook helper — mirror for alert)
    - `app/lib/discordApprovals.js` (existing fireDiscordApproval pattern — template)
    - `.env.example` (DASHCLAW_ALERTS_DISCORD should already be documented from Phase 2; verify and add if missing)
    - `.planning/phases/03-public-launch/03-RESEARCH.md` §Pattern 5 (Discord alert hook site + cheaper alternative)
    - `.planning/phases/03-public-launch/03-CONTEXT.md` (Claude's Discretion — launch-window telemetry is optional)
  </read_first>
  <behavior>
    **Hook site: app/api/actions/route.js POST handler**
    - After `createAction()` succeeds, check: is this the first action_record for this org_id? (SELECT 1 FROM action_records WHERE org_id = $1 AND action_id != $newly_created LIMIT 1 — if no row, this is the first)
    - If yes AND `DASHCLAW_ALERTS_DISCORD` env var is set AND NODE_ENV is production OR explicit opt-in flag set: fire a Discord webhook with a lightweight payload (org_id masked or counted, agent_id, timestamp). NEVER include API keys or secrets.
    - If no OR env var unset: skip silently. Alert is purely additive instrumentation.

    **Error handling:**
    - Webhook failure MUST NOT block the action creation (fire-and-forget, catch-and-log only)
    - Timeout on webhook: 2 seconds max (don't hang the request path)

    **Test cases:**
    - Case 1: First action for org + env var set → webhook called with POST + body containing org_id hash/mask + agent_id + timestamp
    - Case 2: Second action for same org → webhook NOT called (not first)
    - Case 3: First action for org + env var unset → webhook NOT called (opt-in)
    - Case 4: Webhook call fails → action creation still returns success
    - Case 5: Response body contains NO API keys, NO DASHCLAW_API_KEY, NO DISCORD_BOT_TOKEN, NO user PII beyond org_id

    **Repository helper (new):**
    ```javascript
    // app/lib/repositories/actions.repository.js — add
    export async function isFirstActionForOrg(sql, orgId, excludingActionId) {
      const rows = await sql`
        SELECT 1 FROM action_records
        WHERE org_id = ${orgId}
          AND action_id != ${excludingActionId}
        LIMIT 1
      `;
      return rows.length === 0;
    }
    ```
  </behavior>
  <action>
    1. **Extend `app/lib/repositories/actions.repository.js`** with `isFirstActionForOrg(sql, orgId, excludingActionId)` helper. Repository-pattern-compliant (route will not call raw SQL).
    2. **Extend `app/lib/notification-adapters/discord.js`** with a `fireNewConnectAlert(context)` helper that POSTs to `DASHCLAW_ALERTS_DISCORD` webhook. Mirror the existing `fireDiscordApproval` / webhook patterns. Payload shape:
       ```javascript
       {
         embeds: [{
           title: "🚀 New /connect completion",
           fields: [
             { name: "org_id", value: maskedOrgId(orgId), inline: true },
             { name: "agent_id", value: agentId, inline: true },
             { name: "timestamp", value: new Date().toISOString(), inline: false }
           ],
           color: 0xF97316  // brand orange (hex here is fine — Discord API expects integer color)
         }]
       }
       ```
       `maskedOrgId` returns first 8 chars + "..." (privacy-by-default — operator still knows which org, but the raw ID isn't preserved in chat history).
       Timeout: 2 seconds. Catch + log on failure.
    3. **Modify `app/api/actions/route.js` POST handler**: after `createAction()` succeeds, call `isFirstActionForOrg` via the repository; if true AND `process.env.DASHCLAW_ALERTS_DISCORD` is set, call `fireNewConnectAlert(...)` in a fire-and-forget manner (`void Promise.resolve().then(() => fireNewConnectAlert(...))` or `setImmediate` — do NOT `await` and do NOT let errors propagate to the response). Route file should ONLY call repository helpers — zero raw SQL introduced.
    4. **Create `__tests__/unit/connect-complete-discord-alert.test.js`** — mock `getSql`, mock `fetch` (or the notification adapter), mock env. Cover the 5 test cases.
    5. **Update `.env.example`** if `DASHCLAW_ALERTS_DISCORD` is missing (check first — per Phase 2 it should already be there). If absent, add with comment:
       ```
       # Optional: Discord webhook URL for launch-window new-connect alerts (DOG-04 telemetry)
       DASHCLAW_ALERTS_DISCORD=
       ```
    6. Verify:
       ```bash
       npm test -- --run __tests__/unit/connect-complete-discord-alert.test.js
       npm test -- --run   # full suite non-regression
       npm run route-sql:check   # baseline held — route has no new raw SQL
       ```
    7. **Livingcode auto-regen**: touches `app/api/*` + `app/lib/*` — expect regen on commit.
    8. Commit: `feat(03-02): Discord new-connect alert on first per-org action (DOG-04 telemetry)`

    **Threat mitigation:** T-03-02-01 extended — alert payload contains NO API keys / bot tokens / PII beyond masked org_id. Verified by case 5 negative assertions. Fire-and-forget pattern prevents webhook failure from affecting action creation (availability).
  </action>
  <verify>
    <automated>npm test -- --run __tests__/unit/connect-complete-discord-alert.test.js && npm run route-sql:check && npm test -- --run</automated>
  </verify>
  <done>
    - `isFirstActionForOrg` added to actions.repository.js
    - `fireNewConnectAlert` added to discord notification adapter
    - `app/api/actions/route.js` calls `isFirstActionForOrg` → if true + env var set, fires alert (fire-and-forget)
    - Route file has NO new raw SQL (repository pattern preserved)
    - Alert payload contains NO secrets/keys/tokens; only masked org_id + agent_id + timestamp
    - 5 test cases PASS
    - Webhook failure does NOT break action creation (availability test)
    - Full suite passes; route-sql baseline held
    - Livingcode auto-regen artifacts committed
    - Commit lands as `feat(03-02): Discord new-connect alert on first per-org action (DOG-04 telemetry)`
  </done>
</task>

<task type="checkpoint:human-action" gate="blocking">
  <name>Task 4: CHECKPOINT — Same-day 2-hour launch blitz (Tue-Thu 8-11am ET per D-16, D-17)</name>
  <what-built>
    Tasks 1-3 have built:
    - Draft HN post body in `docs/launch/hn-post.md`
    - Draft tweet thread in `docs/launch/tweet-thread.md`
    - Draft blog post content in `docs/launch/blog-post.md`
    - Live blog post at `dashclaw.io/blog/claude-code-beachhead`
    - Discord new-connect alert wired (fires on first per-org action when env var set)
    - `scripts/check-launch-content.mjs` runnable gate

    Plan 03-01 has built:
    - Live homepage on dashclaw.io with video embed
    - Single-page `/connect` runbook
    - CSP frame-src permitting video iframes

    Plan 03-03 has built:
    - Live `/pricing` page with N/50 counter
    - Trigger commitment in PROJECT.md + README.md
    - requireTier helper (dormant Pro tier gate)

    All technical work is done. What remains: posting HN + tweet + blog-go-live in a coordinated 2-hour window. This is strictly human-only work.
  </what-built>
  <how-to-verify>
    **This is a human-action checkpoint.** Claude cannot post to HN, cannot post to X, cannot browse from a phone hotspot in incognito. Wes performs steps 1-7; Claude resumes at step 8 (post-launch telemetry capture).

    **PRE-LAUNCH GATE (must all be TRUE before posting):**
    1. ☐ Plan 03-01 Task 4 manual incognito verification PASSED (homepage + video embed loads clean from phone hotspot)
    2. ☐ Plan 03-03 /pricing page live on dashclaw.io, N/50 counter renders (not "—" fallback)
    3. ☐ `curl -sI https://dashclaw.io | head -1` returns `HTTP/2 200`
    4. ☐ `curl -sI https://dashclaw.io/blog/claude-code-beachhead | head -1` returns `HTTP/2 200`
    5. ☐ `curl -sI https://dashclaw.io/pricing | head -1` returns `HTTP/2 200`
    6. ☐ `curl -sI https://dashclaw.io/connect | head -1` returns `HTTP/2 200`
    7. ☐ `node scripts/check-launch-content.mjs` exits 0 (all drafts ready)
    8. ☐ Pitfall 4 env sanity: `$DASHCLAW_BASE_URL` is not pointing at a demo container; real prod confirmed

    **IF ANY GATE FAILS: do NOT submit HN. Ranking penalty from URL-change after submission is rank-killing (Pitfall 1).**

    **LAUNCH SEQUENCE (same-day 2-hour window — Tue/Wed/Thu 8-11am ET per D-17):**

    **T+0 (09:00 ET sweet spot — per RESEARCH §Launch Mechanics):**
    - Post blog post first (already live from Task 2; verify in incognito one more time)
    - Tweet 1 of thread on X (pin it)
    - Submit Show HN with the prepared title from `docs/launch/hn-post.md`. Body = prepared body. URL = `https://dashclaw.io`

    **T+5 min:**
    - Tweet thread: post tweets 2-8 in sequence (pause ~30 sec between each)
    - Monitor HN post position in New queue

    **T+30 min (HN reply window opens):**
    - Reply to every top-level comment within 30 min (D-19). First 2 hours: tighten to 5 min per reply (RESEARCH recommendation).
    - Honest "fair criticism" replies. No defensive tone.

    **T+2 hours:**
    - Same-day blitz window complete. Log observable signals:
      - HN post URL + vote count + comment count
      - Tweet 1 impression count
      - Discord alert fires (if `DASHCLAW_ALERTS_DISCORD` set — count of alerts received = count of new integrations that day)
      - dashclaw.io/pricing N/50 counter value at T+2h

    **PASS CRITERIA (all must be TRUE to resume Claude):**
    - All 3 content pieces posted within 2-hour window (D-16)
    - HN post URL did NOT change after submission (Pitfall 1 held)
    - No CSP violations observed by any incognito visitor (T-03-01-01 held — verify via DevTools console if a tester reports issues)
    - No secret exposure in any posted content (T-03-02-01 held — verify post-facto by searching for DASHCLAW_API_KEY / DISCORD_BOT_TOKEN patterns in the posted text)
  </how-to-verify>
  <resume-signal>
    Provide launch post URLs + T+2h telemetry for Claude to write the SUMMARY:

    ```
    HN_POST_URL: https://news.ycombinator.com/item?id=XXXXX
    HN_VOTE_COUNT_T2H: <integer>
    HN_COMMENT_COUNT_T2H: <integer>
    TWEET_THREAD_URL: https://x.com/<user>/status/XXXXX
    TWEET1_IMPRESSIONS_T2H: <integer>
    BLOG_POST_LIVE: true
    PRICING_COUNTER_T2H: <integer> / 50
    DISCORD_ALERTS_FIRED_T2H: <integer>  (N/A if DASHCLAW_ALERTS_DISCORD not set)
    ANY_CSP_VIOLATIONS_REPORTED: <true|false>
    ```

    Alternative resume signals:
    - `launch delayed <new date>` — defer to a later Tue/Wed/Thu window
    - `launch aborted <reason>` — halt Phase 3 close (SUMMARY still written documenting the abort)
  </resume-signal>
  <files>N/A — human-action checkpoint. Wes posts HN + tweet thread + publishes blog go-live timing; no files modified by Claude during this task.</files>
  <action>
    Claude halts and surfaces the &lt;what-built&gt; and &lt;how-to-verify&gt; blocks to the operator. The PRE-LAUNCH GATE checklist in &lt;how-to-verify&gt; MUST be satisfied (all 8 gate items ☐→☑) before operator proceeds to LAUNCH SEQUENCE. No Claude-side files modified, no commands executed. Claude resumes only on the operator's &lt;resume-signal&gt; containing the telemetry block (HN_POST_URL, HN_VOTE_COUNT_T2H, etc.), or on `launch delayed` / `launch aborted`.
  </action>
  <verify>
    <automated>N/A — launch submissions are human-only (HN + X posting). Pre-launch curl gates are automatable but are run by the operator as part of the PRE-LAUNCH GATE checklist; pass criteria confirmed via resume-signal telemetry block.</automated>
  </verify>
  <done>
    Operator provides the full telemetry block per resume-signal format (HN_POST_URL, HN_VOTE_COUNT_T2H, HN_COMMENT_COUNT_T2H, TWEET_THREAD_URL, TWEET1_IMPRESSIONS_T2H, BLOG_POST_LIVE, PRICING_COUNTER_T2H, DISCORD_ALERTS_FIRED_T2H, ANY_CSP_VIOLATIONS_REPORTED), OR explicitly signals `launch delayed <date>` / `launch aborted <reason>`. Claude writes the SUMMARY capturing the outcome and closes Phase 3.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Repo → docs/launch/* (public markdown) | Draft content is public-facing pre-launch; must not contain secrets |
| dashclaw.io → HN/Twitter/blog public surfaces | Homepage + blog + /pricing all become HN-linked URLs; changing URL after HN submission kills rank |
| app/api/actions/route.js POST → Discord webhook | Alert fires to operator's Discord; payload must not contain API keys or PII |
| Launch content author (Wes) → public post | Single-shot; no content-agent drafts (D-19); human-in-the-loop mitigates injection/drift |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-02-01 | Information Disclosure | Secret leak in launch content (HN body / tweet / blog) | mitigate | All 3 drafts in docs/launch/ have test assertions blocking API-key / DISCORD_BOT_TOKEN regex patterns. scripts/check-launch-content.mjs runs the same checks pre-launch. Task 4 checkpoint reminds operator to frame-scrub any terminal screenshots |
| T-03-02-02 | Tampering (reputational) | HN post URL changes after submission → rank-killing | mitigate | Task 4 PRE-LAUNCH GATE requires 4 URL smoke-tests (homepage, blog, pricing, connect) all returning 200 BEFORE HN submission. This is the hard ordering constraint from 03-01 Task 4 manual incognito check |
| T-03-02-03 | Information Disclosure | Discord alert webhook leaks org_id / user PII | mitigate | fireNewConnectAlert payload masks org_id to first 8 chars + '...'; never includes API keys or user emails. Test case 5 asserts no secrets in payload |
| T-03-02-04 | Denial of Service | Webhook failure blocks action creation | mitigate | Fire-and-forget pattern: webhook call is NOT awaited; errors are caught and logged; action creation returns success regardless. 2-second timeout caps worst case |
| T-03-02-05 | Spoofing | Launch content references a video URL that doesn't exist yet | mitigate | Task 2 blog post consumes the real EMBED_URL backfilled by Plan 03-01 Task 4; dependency chain enforces ordering. If 03-01 reverts to placeholder, blog post test asserts loom.com|youtube-nocookie.com host (not PLACEHOLDER) |
| T-03-02-06 | Repudiation | Wes-authored HN replies can't be audited | accept | D-19 explicitly locks Wes-authored only. Content-agent drafts are Phase 4 territory. Human-in-the-loop is the mitigation by design |
| T-03-02-07 | Tampering (hijacked launch) | Malicious actor posts a fake "Show HN" before Wes | accept | Mitigation: HN username ownership. If this materializes, Wes reports + posts the authentic version. Low-likelihood consumer risk |
</threat_model>

<verification>
Plan-level verification (after Task 4 checkpoint resumes):

```bash
# Launch content guardrails
node scripts/check-launch-content.mjs    # exits 0

# Blog post live + test pass
npm test -- --run __tests__/unit/blog-post-claude-code-beachhead.test.jsx

# Launch content assertion tests pass
npm test -- --run __tests__/unit/launch-content-assertions.test.js

# Discord alert test pass
npm test -- --run __tests__/unit/connect-complete-discord-alert.test.js

# Full suite non-regression
npm test -- --run

# Static guardrails
npm run route-sql:check
npm run openapi:check
npm run api:inventory:check
npm run docs:check

# HTTP smoke tests (performed by human at Task 4 PRE-LAUNCH GATE)
curl -sI https://dashclaw.io | head -1                              # 200
curl -sI https://dashclaw.io/blog/claude-code-beachhead | head -1    # 200
curl -sI https://dashclaw.io/pricing | head -1                       # 200
curl -sI https://dashclaw.io/connect | head -1                       # 200
```

**Dependency gate (entering Wave 2 / this plan):**
- 03-01 Task 4 complete: homepage + video embed live + incognito-verified
- 03-03 Task 3 complete: /pricing + PROJECT.md + README.md trigger commitments landed

Only after both preconditions are met does 03-02 Task 4 (launch blitz) fire.
</verification>

<success_criteria>
1. Draft HN post committed to `docs/launch/hn-post.md` — title ≤80 chars, body 150-300 words, trigger commitment present (DOG-04 D-17 + Pitfall 10)
2. Draft tweet thread committed to `docs/launch/tweet-thread.md` — 6-10 tweets, first tweet is concrete problem (Pitfall 11), last tweet has 50-integration trigger + dashclaw.io link (D-18)
3. Draft blog post committed to `docs/launch/blog-post.md` — 600-1200 words, ≥5 H2 sections, founder-voice first-person (D-20)
4. Blog post live on `dashclaw.io/blog/claude-code-beachhead` with VideoHero embed using the real video URL (DOG-04 D-20)
5. Discord new-connect alert wires to fire on first per-org action_record when `DASHCLAW_ALERTS_DISCORD` env var is set (optional instrumentation per Claude's Discretion)
6. `scripts/check-launch-content.mjs` exits 0 — drafts ready for launch day
7. HN post + tweet thread + blog post all live within 2-hour window on launch day (D-16), with Tue-Thu 8-11am ET window (D-17) enforced by Task 4 checkpoint guidance
8. Pre-launch gate PASSED: curl HTTP 200 on homepage + blog + pricing + /connect BEFORE HN submission (Pitfall 1 mitigation)
9. No secret leak in any launch content (API-key / DISCORD_BOT_TOKEN regex negative assertions)
10. Full test suite `npm test -- --run` passes; route-sql:check baseline held; openapi + api-inventory + docs checks green
11. Discord alert payload contains NO secrets / API keys / PII beyond masked org_id (T-03-02-03)
12. Launch telemetry captured (HN vote/comment counts, tweet impressions, Pricing counter delta, Discord alert count) in the SUMMARY per Task 4 resume-signal format
</success_criteria>

<output>
After completion, create `.planning/phases/03-public-launch/03-02-SUMMARY.md` documenting:
- HN post URL + T+2h vote/comment counts
- Tweet thread URL + T+2h impressions on tweet 1
- Blog post live confirmation
- /pricing N/50 counter at T+2h vs. T+0 (launch-day delta)
- Discord alert count (if env var set)
- Any CSP violations reported or rank-killing URL changes (ideally none)
- D-03 four-location commitment closure: all 4 locations now live (PROJECT.md + README.md + /pricing from 03-03; tweet/HN from 03-02)
- A1 resolved definitively: blog lives in this monorepo at app/blog/claude-code-beachhead/page.js
- DOG-02, DOG-03, DOG-04, MON-01, MON-02 all closed (update REQUIREMENTS.md traceability table)
- Phase 3 complete → ready for Phase 4 Growth Flywheel
</output>
