Skip to content

feat: integrate Gingiris launch playbook + upgrade AI generation - #6

Merged
AhmedFr merged 7 commits into
mainfrom
feat/gingiris-playbook-integration
Jun 21, 2026
Merged

AhmedFr merged 7 commits into
mainfrom
feat/gingiris-playbook-integration

Conversation

@AhmedFr

@AhmedFr AhmedFr commented Jun 21, 2026 •

Copy link
Copy Markdown
Owner

What & why

Folds the gingiris-launch playbook into Launch Kit and upgrades the AI generation across the board. The playbook's deep reference files are not public (they 404), so this works from its principles, structure, and benchmarks — it does not invent verbatim scripts.

Better prompts (the "enhance the AI generation" ask)

  • New shared PLAYBOOK_PRINCIPLES prepended to every system prompt (core + all platforms): user‑value‑first, content > channel, global brand / local execution, concentrate on real people. Each platform also gained a tactical line (PH momentum, HN authentic engagement, Reddit depth‑over‑breadth, AppSumo concrete ROI).
  • Enriched LaunchCore with valueProp, icp, and keywords — distilled once and inherited by every platform via renderCore, and used to make SEO/GEO and targeting product‑specific.
  • Product Hunt: hour‑by‑hour launch‑day battle plan, upvote‑momentum tactics, comment‑moderation guidance.
  • Hacker News: posting etiquette for authentic, non‑spammy engagement.
  • Reddit: per‑subreddit rules notes + recommended posting timing.

New features (the "implement all tips" ask)

  • New Social / KOL platform kit — an X (Twitter) launch thread, cold‑outreach DM templates for X / LinkedIn / Telegram, and a UGC ask. Wired through the registry, mock provider, edit sections, an X‑thread preview, and Markdown export.
  • Cross‑platform Launch Plan (AI‑generated, product‑tailored) — the L‑6w→L+4w timeline, 30/7/48h countdowns, a keyword‑driven SEO/GEO checklist, and a post‑launch momentum cadence. Generated once per run, shown above the per‑platform kits, with its own copy/export.

All new schema/type fields are optional and guarded so runs persisted before this change still load and render. Default generation model is unchanged (anthropic/claude-sonnet-4.6).

Scope was confirmed with the maintainer: comprehensive, new KOL/social kit, AI‑generated product‑tailored guide content.

How it was tested

Ran all four gates (green), plus an end‑to‑end smoke test of POST /api/generate against a production build with the mock provider: all 5 platforms generated (none failed), the launch plan attached (7 phases, SEO/GEO, momentum, countdowns), the enriched core populated, and every new per‑platform field present.

  • pnpm test — 86 passed (15 files)
  • pnpm typecheck
  • pnpm lint
  • pnpm build

Checklist

  • Follows the single-responsibility / per-component folder structure
  • No secrets, API keys, or .env values committed (.env is gitignored; only an out‑of‑date model comment in the local .env was corrected)
  • README / docs updated if behavior or configuration changed
  • Screenshots included for UI changes

Screenshots / notes

  • Screenshots not captured in this environment (would require running the app against a real model). UI additions to verify visually: a new Social / KOL tab in the platform selector with an X‑thread preview, and a Launch plan panel rendered above the platform kits with Copy/Export. docs/screenshots/kit.png is now slightly out of date.
  • The playbook's deep tactics (hour‑by‑hour scripts, hunter database) are paywalled/404 in the public repo, so concrete copy is synthesized from its principles + benchmarks rather than reproduced.
  • Out of scope / follow‑ups: multilingual output (the playbook is zh/en/ja/ko; output stays English), a "regenerate plan" button (the plan is produced during full generation), and an optional temperature setting on the OpenRouter request for tighter JSON.

Summary by CodeRabbit

  • New Features

    • Added Social/X platform support with launch thread generation, KOL outreach templates, and UGC ask prompts
    • Introduced cross-platform launch plan feature with phased timelines, countdown checklists, SEO/GEO guidance, and post-launch momentum tracking
    • Added ability to export or copy launch plans to markdown
  • Enhancements

    • Product Hunt: Added hour-by-hour battle plan, upvote momentum tactics, and comment moderation guidance
    • Hacker News: Added etiquette guidance to posting tips
    • Reddit: Added rules notes and best timing recommendations
    • Updated README with comprehensive launch strategy documentation

AhmedFr and others added 6 commits June 21, 2026 13:54
… prompt

Add a shared PLAYBOOK_PRINCIPLES constant (user-value-first; content>channel;
global brand/local execution; concentrate on real people) and prepend it to the
core + 4 platform system prompts, plus platform-specific tactical lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The distillation step now extracts a one-sentence value proposition, a sharp ICP,
and SEO/topic keywords. Fields are optional for back-compat with cores persisted
before they existed; renderCore emits them only when present, and every platform
prompt inherits them via renderCore. Mock provider derives sane defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Product Hunt: hour-by-hour launch-day timeline, upvote-momentum tactics, and
  comment-moderation guidance (rendered in LaunchOpsSection + Markdown export).
- Hacker News: posting etiquette for authentic, non-spammy engagement.
- Reddit: per-subreddit rules notes and recommended posting timing.

All new fields are optional for back-compat with persisted kits and guarded in the
UI and Markdown. Mock provider populates realistic defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fifth platform that generates an X (Twitter) launch thread, cold-outreach DM
templates for X / LinkedIn / Telegram KOLs, and a UGC ask to turn early users into
creators. Wired through the generation registry, mock provider, edit-view sections,
an X-thread preview, and Markdown export, with schema/markdown/render tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new product-tailored launch plan generated once per run from the shared core:
the L-6w→L+4w timeline, the 30/7/48h pre-launch countdowns, a keyword-driven
SEO/GEO checklist, and the post-launch momentum cadence. Added a generatePlan
provider method (OpenRouter + mock), best-effort wiring in the generate route, a
LaunchPlan panel above the platform kits in PostKit, and a dedicated
plan-to-markdown export with copy/export handlers. Generation.plan is optional for
back-compat with persisted runs. Covered by schema, markdown, mock, and render tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
launch-kit Ready Ready Preview, Comment Jun 21, 2026 12:03pm

@coderabbitai

coderabbitai Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AhmedFr, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 16 minutes and 21 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5c8595e5-86ce-4a20-aac4-54117aea061b

📥 Commits

Reviewing files that changed from the base of the PR and between defbf36 and 6cccca0.

📒 Files selected for processing (3)
  • components/post-kit/PostKit/PostKit.test.tsx
  • components/post-kit/PostKit/PostKit.tsx
  • components/post-kit/platforms/ProductHuntSections/ProductHuntSections.tsx
📝 Walkthrough

Walkthrough

Adds a new Social/KOL platform and a cross-platform LaunchPlan feature. This includes new Zod schemas, LLM prompts, mock and OpenRouter provider implementations, React UI components (SocialSections, SocialPreview, LaunchPlan), markdown export utilities, and enriched content fields for Product Hunt, Hacker News, and Reddit. The API route now generates the plan concurrently with platform content.

Changes

Social Platform and Cross-Platform Launch Plan

Layer / File(s) Summary
Core types, schemas, and platform registry
lib/types.ts, lib/platforms.ts, lib/generation/core/core.schema.ts, lib/generation/plan/plan.schema.ts, lib/generation/platforms/product-hunt/schema.ts, lib/generation/platforms/reddit/schema.ts, lib/generation/platforms/hacker-news/schema.ts
LaunchCore gains optional keywords/icp/valueProp; SocialContent and LaunchPlan types are added; Generation gains optional plan; PlatformId and PLATFORMS include social; existing platform schemas extended with new optional fields.
Shared prompt infrastructure and provider contract
lib/generation/prompt-shared.ts, lib/generation/provider.types.ts, lib/generation/platforms/appsumo/prompt.ts, lib/generation/platforms/hacker-news/prompt.ts, lib/generation/platforms/product-hunt/prompt.ts, lib/generation/platforms/reddit/prompt.ts
PLAYBOOK_PRINCIPLES constant added and prepended to all platform system prompts; renderCore conditionally emits enrichment fields; GenerationProvider interface gains generatePlan.
Social platform generation
lib/generation/platforms/social/prompt.ts, lib/generation/platforms/social/schema.ts, lib/generation/platforms/registry.ts
SOCIAL_SYSTEM, buildSocialPrompt, and socialSchema added; PLATFORM_GENERATORS extended with social entry.
Launch plan generation
lib/generation/plan/plan.schema.ts, lib/generation/plan/plan.prompt.ts, lib/generation/openrouter-provider.ts, lib/generation/mock-provider.ts, app/api/generate/route.ts
launchPlanSchema, PLAN_SYSTEM, and buildPlanPrompt defined; generatePlan implemented in both providers; API route fires planPromise concurrently and conditionally attaches result to generation.plan.
MockProvider existing platform expansion
lib/generation/mock-provider.ts
buildProductHunt gains hourByHour/momentumTactics/commentModeration; buildHackerNews gains etiquette; buildReddit gains rulesNote per subreddit and postingTiming.
Social platform UI
components/post-kit/platforms/SocialSections/..., components/preview/SocialPreview/..., lib/preview/registry.ts, components/post-kit/platforms/registry.ts
SocialSections renders four cards (launch thread, KOL outreach, UGC ask, posting tips); SocialPreview renders tweet thread with hashtags; both registered in their platform maps.
Existing platform UI enhancements
components/post-kit/sections/LaunchOpsSection/LaunchOpsSection.tsx, components/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsx, components/post-kit/platforms/RedditSections/RedditSections.tsx
LaunchOpsSection adds hour-by-hour/upvote-momentum/comment-moderation blocks; HackerNewsSections adds etiquette list; RedditSections adds rulesNote and "Best timing" row.
LaunchPlan UI component, markdown export, and PostKit wiring
components/launch-plan/LaunchPlan/..., lib/export/plan-to-markdown.ts, components/post-kit/PostKit/PostKit.tsx, components/post-kit/PostKit/PostKit.types.ts, app/runs/[id]/page.tsx
LaunchPlan component renders phase timeline and checklist sections with Copy/Export buttons; planToMarkdown converts plan to markdown; PostKit conditionally renders LaunchPlan; runs/[id]/page.tsx adds exportPlan/copyPlan handlers.
Markdown export: social and plan
lib/export/to-markdown.ts, lib/export/plan-to-markdown.ts
socialMarkdown generator added and wired into platformToMarkdown; PH/HN/Reddit generators extended with new optional sections; planToMarkdown formats LaunchPlan into a structured markdown document.
Tests
lib/generation/plan/plan.schema.test.ts, lib/generation/schema.test.ts, lib/generation/mock-provider.test.ts, lib/generation/prompt.test.ts, components/post-kit/platforms/SocialSections/SocialSections.test.tsx, components/launch-plan/LaunchPlan/LaunchPlan.test.tsx, lib/export/plan-to-markdown.test.ts, lib/export/to-markdown.test.ts
Schema, mock provider, prompt, component, and markdown export tests added/extended for social platform and launch plan.
README updates
README.md
Hero tagline, "What you get" bullets, platform table, and post-table paragraph updated to reflect Social/KOL and LaunchPlan additions.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant GenerateRoute
  participant Provider
  participant Planner

  Browser->>GenerateRoute: submit generate request
  GenerateRoute->>Provider: generateCore
  Provider-->>GenerateRoute: core
  GenerateRoute->>Planner: generatePlan with core
  GenerateRoute->>Provider: generatePlatform for each platform
  Provider-->>GenerateRoute: platform outputs
  Planner-->>GenerateRoute: plan or undefined
  GenerateRoute-->>Browser: generation response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AhmedFr/launch-kit#1: Both PRs modify the POST handler in app/api/generate/route.ts to extend the generation response shape with new fields and platform content.
  • AhmedFr/launch-kit#2: Both PRs extend the same app/api/generate/route.ts + app/runs/[id]/page.tsx + PostKit pipeline, with this PR adding concurrent plan generation and export wiring on top of the existing multi-platform flow.

Poem

🐇 Hop hop, a new platform's born,
Social threads and KOL outreach adorn,
The plan unfolds week by week,
SEO, countdown, momentum speak—
Five native posts, one launch kit complete! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: integrate Gingiris launch playbook + upgrade AI generation' accurately and concisely summarizes the main changes: integrating a playbook into the codebase and enhancing AI generation. It is specific, clear, and appropriately conveys the primary scope without being vague or misleading.
Description check ✅ Passed The PR description comprehensively addresses all required template sections: 'What & why' explains the playbook integration and AI enhancements with clear rationale, 'How it was tested' confirms all four gates passed with specific test counts, and the 'Checklist' is fully completed with checkbox selections and justifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gingiris-playbook-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/generate/route.ts`:
- Around line 38-45: The error logging in the catch block of the planPromise
handler is only capturing the error message with `err?.message ?? err`, which
loses valuable stack trace information needed for debugging. Modify the
console.error call in the catch handler (where the plan generation fails) to log
the complete error object instead of just the message, so the full stack trace
is preserved in production logs. You can pass the error object directly as part
of the logged output to maintain debugging information.

In `@app/runs/`[id]/page.tsx:
- Around line 130-135: The copyPlan function does not handle potential errors
from navigator.clipboard.writeText, which can fail due to permission issues or
clipboard unavailability. Wrap the await navigator.clipboard.writeText(md) call
inside the copyPlan function with a try-catch block to catch any errors. In the
catch block, display an error toast message to the user indicating that the copy
operation failed. Keep the existing success toast message for when the operation
succeeds.

In `@components/launch-plan/LaunchPlan/LaunchPlan.tsx`:
- Line 10: The list items in the map functions are using only the string value
(i) as the React key, which can cause collisions and unstable reconciliation
when duplicate strings appear in AI-generated lists. Update all three list
rendering locations (the one at line 10 using items.map, and the two others
referenced in the comment) to use collision-safe keys by including the array
index in the key. For each map callback like items.map((i) => ...), add the
index parameter to the callback and create a composite key using both the index
and the value, such as using a template string combining the index position with
the item value, ensuring each key is unique even when duplicate items exist.

In `@components/post-kit/platforms/SocialSections/SocialSections.tsx`:
- Line 24: The mapped list items in the social sections component use non-unique
keys based on the content values (key={tweet} and key={h}). When the same value
appears multiple times, this causes React to confuse component identity and
state can bleed between rows. Replace the key prop in both the tweet list item
(the li element with key={tweet}) and the other mapped items with key={h} to use
a stable unique identifier such as the array index combined with a prefix
string, or better yet, use a unique ID property from the data if available.

In `@components/post-kit/sections/LaunchOpsSection/LaunchOpsSection.tsx`:
- Line 37: The list reconciliation for launch.momentumTactics uses the value
itself as the key (key={t}), which will cause collisions when duplicate items
are generated, leading to unstable list behavior. Replace the value-based key
with the index parameter from the map function to ensure stable reconciliation.
Apply the same fix to the similar map operation on line 43 that has the same
issue with key={c}.
- Line 24: The `<ol>` element in the LaunchOpsSection component is missing CSS
classes required for proper ordered list styling. Add the Tailwind classes
"list-decimal pl-5" to the className attribute of the `<ol>` element (currently
containing "mt-1 space-y-1") to ensure the timeline renders with decimal
numbering and appropriate left padding instead of displaying as plain text.

In `@components/preview/SocialPreview/SocialPreview.tsx`:
- Line 12: The div elements in the SocialPreview component are using tweet and
hashtag text values directly as React keys, which causes reconciliation issues
when duplicate values exist. Replace the key prop from key={tweet} to use a
unique identifier instead, such as an index from the array iteration or a unique
ID from your data source. Apply this fix to both occurrences: the tweet
rendering using the tweet value as the key and the similar hashtag rendering
pattern elsewhere in the component.

In `@lib/export/plan-to-markdown.ts`:
- Line 3: The bullets helper function is duplicated across multiple export
files. Create a new shared utility module at lib/export/utils.ts and move the
bullets function definition there, then remove the bullets function definition
from both lib/export/plan-to-markdown.ts and lib/export/to-markdown.ts,
replacing them with imports of bullets from the new utils module to eliminate
the duplication.

In `@lib/generation/mock-provider.test.ts`:
- Around line 42-49: The test 'produces a schema-valid launch plan spanning the
timeline' currently validates schema conformance and checks that the phases and
seoGeo arrays are non-empty, but it does not verify that the countdown arrays
(countdown30, countdown7, countdown48h) and momentum fields are populated. Add
additional expect assertions after the existing checks to verify that these
countdown arrays and the momentum field are non-empty to ensure the generated
plan contains all expected content.

In `@lib/generation/mock-provider.ts`:
- Around line 22-24: The keywords array construction in mock-provider.ts is
using entire feature descriptions as keywords by mapping full feature strings to
lowercase, which creates multi-word phrases unsuitable for SEO tasks, hashtag
generation, and keyword-based targeting. Instead of using
context.features.map((f) => f.toLowerCase()) which treats the entire feature
string as a keyword, extract only the first word from each feature (e.g.,
transform "Fast and reliable processing" to "fast") or use only the
context.techStack items if features aren't suitable for keyword extraction. This
will ensure clean, single-word keywords are generated instead of concatenated
hashtags and multi-word phrases that flow through buildPlan and buildSocial
functions.

In `@lib/generation/plan/plan.schema.ts`:
- Around line 5-11: The launchPlanSchema allows phases and tasks arrays to be
empty, which produces invalid outputs. Add the .nonempty() constraint to the
z.array() calls for the phases array and the tasks array nested within the
phases object to enforce that both arrays must contain at least one element.
This prevents semantically invalid data from being generated at the schema level
rather than relying on defensive handling downstream in the Checklist and
LaunchPlan components.

In `@lib/generation/platforms/hacker-news/prompt.ts`:
- Around line 14-21: The SHAPE constant includes the "etiquette" field, treating
it as required, but the corresponding Zod schema marks this field as optional
using .optional(), creating a mismatch between what the LLM will generate and
what the schema expects. Either remove the "etiquette" field from the SHAPE
constant to reflect that it is optional in the schema, or if the etiquette field
must always be generated by the LLM, remove the .optional() marking from the
schema and add a comment explaining why it must always be present despite
allowing omission for backward compatibility.

In `@lib/generation/platforms/social/schema.ts`:
- Around line 4-9: The socialSchema object is missing validation constraints
that could prevent invalid data. Add the following validation bounds to the
schema: apply `.min(1).max(12)` to the tweets array within the thread object to
ensure at least one tweet but prevent excessively long threads, apply
`.min(1).max(10)` to the hashtags array within the postingTips object, and apply
`.min(10)` to each of the string fields twitter, linkedin, and telegram within
the kolOutreach object to ensure they contain meaningful content, and add
`.min(10)` to the ugcAsk string field to validate minimum length for DM
templates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 142c9f97-b934-4e5b-999d-b7ad913cf07e

📥 Commits

Reviewing files that changed from the base of the PR and between 334e11b and defbf36.

📒 Files selected for processing (47)
  • README.md
  • app/api/generate/route.ts
  • app/runs/[id]/page.tsx
  • components/launch-plan/LaunchPlan/LaunchPlan.test.tsx
  • components/launch-plan/LaunchPlan/LaunchPlan.tsx
  • components/launch-plan/LaunchPlan/LaunchPlan.types.ts
  • components/launch-plan/LaunchPlan/index.ts
  • components/post-kit/PostKit/PostKit.tsx
  • components/post-kit/PostKit/PostKit.types.ts
  • components/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsx
  • components/post-kit/platforms/RedditSections/RedditSections.tsx
  • components/post-kit/platforms/SocialSections/SocialSections.test.tsx
  • components/post-kit/platforms/SocialSections/SocialSections.tsx
  • components/post-kit/platforms/SocialSections/index.ts
  • components/post-kit/platforms/registry.ts
  • components/post-kit/sections/LaunchOpsSection/LaunchOpsSection.tsx
  • components/preview/SocialPreview/SocialPreview.tsx
  • components/preview/SocialPreview/index.ts
  • lib/export/plan-to-markdown.test.ts
  • lib/export/plan-to-markdown.ts
  • lib/export/to-markdown.test.ts
  • lib/export/to-markdown.ts
  • lib/generation/core/core.prompt.ts
  • lib/generation/core/core.schema.ts
  • lib/generation/mock-provider.test.ts
  • lib/generation/mock-provider.ts
  • lib/generation/openrouter-provider.ts
  • lib/generation/plan/plan.prompt.ts
  • lib/generation/plan/plan.schema.test.ts
  • lib/generation/plan/plan.schema.ts
  • lib/generation/platforms/appsumo/prompt.ts
  • lib/generation/platforms/hacker-news/prompt.ts
  • lib/generation/platforms/hacker-news/schema.ts
  • lib/generation/platforms/product-hunt/prompt.ts
  • lib/generation/platforms/product-hunt/schema.ts
  • lib/generation/platforms/reddit/prompt.ts
  • lib/generation/platforms/reddit/schema.ts
  • lib/generation/platforms/registry.ts
  • lib/generation/platforms/social/prompt.ts
  • lib/generation/platforms/social/schema.ts
  • lib/generation/prompt-shared.ts
  • lib/generation/prompt.test.ts
  • lib/generation/provider.types.ts
  • lib/generation/schema.test.ts
  • lib/platforms.ts
  • lib/preview/registry.ts
  • lib/types.ts

Comment thread app/api/generate/route.ts
Comment on lines +38 to +45
// Full generation: distill the core once, then fan out to every platform in
// parallel, plus the cross-platform launch plan. The plan is best-effort — a
// failure there must not sink the whole generation.
const core = await provider.generateCore(input)
const planPromise = provider.generatePlan(core, input).catch((err) => {
console.error('[api/generate] plan failed:', err?.message ?? err)
return undefined
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider logging the full error stack for plan failures.

The current error logging only captures err?.message ?? err, which loses stack trace information that could be valuable for debugging plan generation failures in production.

📋 Proposed enhancement
 const planPromise = provider.generatePlan(core, input).catch((err) => {
-  console.error('[api/generate] plan failed:', err?.message ?? err)
+  console.error('[api/generate] plan failed:', err)
   return undefined
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/generate/route.ts` around lines 38 - 45, The error logging in the
catch block of the planPromise handler is only capturing the error message with
`err?.message ?? err`, which loses valuable stack trace information needed for
debugging. Modify the console.error call in the catch handler (where the plan
generation fails) to log the complete error object instead of just the message,
so the full stack trace is preserved in production logs. You can pass the error
object directly as part of the logged output to maintain debugging information.

Comment thread app/runs/[id]/page.tsx
Comment on lines +130 to +135
async function copyPlan() {
const md = planMarkdown()
if (!md) return
await navigator.clipboard.writeText(md)
toast.success('Launch plan copied')
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle clipboard failures in copyPlan to avoid unhandled promise rejections.

navigator.clipboard.writeText can fail; this path currently has no guard or user-visible error fallback.

Suggested fix
   async function copyPlan() {
     const md = planMarkdown()
     if (!md) return
-    await navigator.clipboard.writeText(md)
-    toast.success('Launch plan copied')
+    try {
+      await navigator.clipboard.writeText(md)
+      toast.success('Launch plan copied')
+    } catch {
+      toast.error('Failed to copy launch plan')
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function copyPlan() {
const md = planMarkdown()
if (!md) return
await navigator.clipboard.writeText(md)
toast.success('Launch plan copied')
}
async function copyPlan() {
const md = planMarkdown()
if (!md) return
try {
await navigator.clipboard.writeText(md)
toast.success('Launch plan copied')
} catch {
toast.error('Failed to copy launch plan')
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/runs/`[id]/page.tsx around lines 130 - 135, The copyPlan function does
not handle potential errors from navigator.clipboard.writeText, which can fail
due to permission issues or clipboard unavailability. Wrap the await
navigator.clipboard.writeText(md) call inside the copyPlan function with a
try-catch block to catch any errors. In the catch block, display an error toast
message to the user indicating that the copy operation failed. Keep the existing
success toast message for when the operation succeeds.

return (
<div>
<div className="text-xs uppercase text-muted-foreground">{title}</div>
<ul className="mt-1 list-disc pl-5">{items.map((i) => <li key={i}>{i}</li>)}</ul>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use collision-safe keys for generated list items.

These lists are AI-generated text; duplicate strings are plausible, so value-only keys can collide and cause unstable reconciliation.

Proposed fix
-      <ul className="mt-1 list-disc pl-5">{items.map((i) => <li key={i}>{i}</li>)}</ul>
+      <ul className="mt-1 list-disc pl-5">{items.map((i, idx) => <li key={`${i}-${idx}`}>{i}</li>)}</ul>
...
-          {plan.phases.map((phase) => (
-            <li key={phase.window} className="border-l-2 border-primary/40 pl-3">
+          {plan.phases.map((phase, idx) => (
+            <li key={`${phase.window}-${idx}`} className="border-l-2 border-primary/40 pl-3">
...
-              <ul className="mt-1 list-disc pl-5">{phase.tasks.map((t) => <li key={t}>{t}</li>)}</ul>
+              <ul className="mt-1 list-disc pl-5">{phase.tasks.map((t, tIdx) => <li key={`${t}-${tIdx}`}>{t}</li>)}</ul>

Also applies to: 29-29, 34-34

🧰 Tools
🪛 React Doctor (0.5.6)

[warning] 10-10: This JSX crashes because React isn't in scope.

If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import React at the top of the file.

(react-in-jsx-scope)


[warning] 10-10: This JSX crashes because React isn't in scope.

If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import React at the top of the file.

(react-in-jsx-scope)


[warning] 10-10: Your users can see & submit the wrong data when this list reorders or filters, so use a stable id like key={item.id}, not the array index "i".

Use a stable id from the item, like key={item.id} or key={item.slug}. Index keys break when the list reorders or filters.

(no-array-index-as-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/launch-plan/LaunchPlan/LaunchPlan.tsx` at line 10, The list items
in the map functions are using only the string value (i) as the React key, which
can cause collisions and unstable reconciliation when duplicate strings appear
in AI-generated lists. Update all three list rendering locations (the one at
line 10 using items.map, and the two others referenced in the comment) to use
collision-safe keys by including the array index in the key. For each map
callback like items.map((i) => ...), add the index parameter to the callback and
create a composite key using both the index and the value, such as using a
template string combining the index position with the item value, ensuring each
key is unique even when duplicate items exist.

</div>
<ol className="space-y-2">
{social.thread.tweets.map((tweet, i) => (
<li key={tweet} className="rounded-xl border border-border px-3 py-2">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use stable unique keys for mapped social items.

key={tweet} and key={h} are not unique when generated content repeats values. In the tweet list this can cause row identity bugs (e.g., CopyButton state appearing on the wrong row).

Suggested fix
-          {social.thread.tweets.map((tweet, i) => (
-            <li key={tweet} className="rounded-xl border border-border px-3 py-2">
+          {social.thread.tweets.map((tweet, i) => (
+            <li key={`${i}-${tweet}`} className="rounded-xl border border-border px-3 py-2">
@@
-            {social.postingTips.hashtags.map((h) => (
-              <span key={h} className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{h}</span>
+            {social.postingTips.hashtags.map((h, i) => (
+              <span key={`${i}-${h}`} className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{h}</span>
             ))}

Also applies to: 63-63

🧰 Tools
🪛 React Doctor (0.5.6)

[warning] 24-24: This JSX crashes because React isn't in scope.

If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import React at the top of the file.

(react-in-jsx-scope)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/post-kit/platforms/SocialSections/SocialSections.tsx` at line 24,
The mapped list items in the social sections component use non-unique keys based
on the content values (key={tweet} and key={h}). When the same value appears
multiple times, this causes React to confuse component identity and state can
bleed between rows. Replace the key prop in both the tweet list item (the li
element with key={tweet}) and the other mapped items with key={h} to use a
stable unique identifier such as the array index combined with a prefix string,
or better yet, use a unique ID property from the data if available.

{launch.hourByHour && launch.hourByHour.length > 0 && (
<div>
<div className="text-xs uppercase text-muted-foreground">Hour-by-hour (PT)</div>
<ol className="mt-1 space-y-1">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hour-by-hour list won’t show numbering with current classes.

<ol> is missing list-decimal pl-5, so the ordered timeline can render like plain text instead of a numbered sequence.

Suggested fix
-          <ol className="mt-1 space-y-1">
+          <ol className="mt-1 list-decimal space-y-1 pl-5">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ol className="mt-1 space-y-1">
<ol className="mt-1 list-decimal space-y-1 pl-5">
🧰 Tools
🪛 React Doctor (0.5.6)

[warning] 24-24: This JSX crashes because React isn't in scope.

If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import React at the top of the file.

(react-in-jsx-scope)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/post-kit/sections/LaunchOpsSection/LaunchOpsSection.tsx` at line
24, The `<ol>` element in the LaunchOpsSection component is missing CSS classes
required for proper ordered list styling. Add the Tailwind classes "list-decimal
pl-5" to the className attribute of the `<ol>` element (currently containing
"mt-1 space-y-1") to ensure the timeline renders with decimal numbering and
appropriate left padding instead of displaying as plain text.

Comment on lines +42 to +49
it('produces a schema-valid launch plan spanning the timeline', async () => {
const provider = new MockProvider()
const core = await provider.generateCore(input)
const plan = await provider.generatePlan(core, input)
expect(() => launchPlanSchema.parse(plan)).not.toThrow()
expect(plan.phases.length).toBeGreaterThan(0)
expect(plan.seoGeo.length).toBeGreaterThan(0)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider verifying all LaunchPlan fields are populated.

The test validates schema conformance and checks phases and seoGeo are non-empty, but doesn't verify the countdown arrays (countdown30, countdown7, countdown48h) or momentum are populated. While schema validation catches structural issues, explicitly asserting all expected content is present improves test robustness.

✅ Optional: verify all fields
   expect(() => launchPlanSchema.parse(plan)).not.toThrow()
   expect(plan.phases.length).toBeGreaterThan(0)
+  expect(plan.countdown30.length).toBeGreaterThan(0)
+  expect(plan.countdown7.length).toBeGreaterThan(0)
+  expect(plan.countdown48h.length).toBeGreaterThan(0)
   expect(plan.seoGeo.length).toBeGreaterThan(0)
+  expect(plan.momentum.length).toBeGreaterThan(0)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('produces a schema-valid launch plan spanning the timeline', async () => {
const provider = new MockProvider()
const core = await provider.generateCore(input)
const plan = await provider.generatePlan(core, input)
expect(() => launchPlanSchema.parse(plan)).not.toThrow()
expect(plan.phases.length).toBeGreaterThan(0)
expect(plan.seoGeo.length).toBeGreaterThan(0)
})
it('produces a schema-valid launch plan spanning the timeline', async () => {
const provider = new MockProvider()
const core = await provider.generateCore(input)
const plan = await provider.generatePlan(core, input)
expect(() => launchPlanSchema.parse(plan)).not.toThrow()
expect(plan.phases.length).toBeGreaterThan(0)
expect(plan.countdown30.length).toBeGreaterThan(0)
expect(plan.countdown7.length).toBeGreaterThan(0)
expect(plan.countdown48h.length).toBeGreaterThan(0)
expect(plan.seoGeo.length).toBeGreaterThan(0)
expect(plan.momentum.length).toBeGreaterThan(0)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/generation/mock-provider.test.ts` around lines 42 - 49, The test
'produces a schema-valid launch plan spanning the timeline' currently validates
schema conformance and checks that the phases and seoGeo arrays are non-empty,
but it does not verify that the countdown arrays (countdown30, countdown7,
countdown48h) and momentum fields are populated. Add additional expect
assertions after the existing checks to verify that these countdown arrays and
the momentum field are non-empty to ensure the generated plan contains all
expected content.

Comment on lines +22 to +24
const keywords = Array.from(
new Set([...context.techStack, ...context.features.map((f) => f.toLowerCase())]),
).slice(0, 8)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keyword extraction produces full feature descriptions, not keywords.

The logic lowercases entire feature strings (e.g., "Fast and reliable processing") and treats them as keywords. This produces multi-word phrases in the keywords array instead of semantic keywords.

Impact:

  • Keywords flow to SEO/GEO tasks in buildPlan (line 212, 215, 227, 242)
  • Keywords become hashtags in buildSocial (line 186), where replace(/[^a-zA-Z0-9]/g, '') removes spaces, creating concatenated hashtags like #fastandreliableprocessing
  • Keywords may be used for Product Hunt topics and subreddit targeting

Extract actual keywords (e.g., first word from each feature, or only use techStack items) instead of full feature descriptions.

🔧 Proposed fix to extract keywords, not sentences
-  const keywords = Array.from(
-    new Set([...context.techStack, ...context.features.map((f) => f.toLowerCase())]),
-  ).slice(0, 8)
+  const keywords = Array.from(
+    new Set([
+      ...context.techStack,
+      ...context.features.map((f) => f.split(/\s+/)[0].toLowerCase())
+    ]),
+  ).slice(0, 8)

This extracts the first word from each feature (e.g., "Fast and reliable processing" → "fast"). Alternatively, consider using only techStack items if features aren't suitable for keyword extraction.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const keywords = Array.from(
new Set([...context.techStack, ...context.features.map((f) => f.toLowerCase())]),
).slice(0, 8)
const keywords = Array.from(
new Set([
...context.techStack,
...context.features.map((f) => f.split(/\s+/)[0].toLowerCase())
]),
).slice(0, 8)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/generation/mock-provider.ts` around lines 22 - 24, The keywords array
construction in mock-provider.ts is using entire feature descriptions as
keywords by mapping full feature strings to lowercase, which creates multi-word
phrases unsuitable for SEO tasks, hashtag generation, and keyword-based
targeting. Instead of using context.features.map((f) => f.toLowerCase()) which
treats the entire feature string as a keyword, extract only the first word from
each feature (e.g., transform "Fast and reliable processing" to "fast") or use
only the context.techStack items if features aren't suitable for keyword
extraction. This will ensure clean, single-word keywords are generated instead
of concatenated hashtags and multi-word phrases that flow through buildPlan and
buildSocial functions.

Comment on lines +5 to +11
export const launchPlanSchema = z.object({
phases: z.array(z.object({ window: z.string(), goal: z.string(), tasks: z.array(z.string()) })),
countdown30: z.array(z.string()),
countdown7: z.array(z.string()),
countdown48h: z.array(z.string()),
seoGeo: z.array(z.string()),
momentum: z.array(z.string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how downstream code handles plan.phases

# Find LaunchPlan component rendering logic
ast-grep --pattern $'phases$$$map$$$'

# Check plan-to-markdown export logic  
ast-grep --pattern $'function$$$Plan$$$phases$$$'

# Look for defensive checks on empty arrays
rg -nC3 'phases\.length' --type=ts --type=tsx

Repository: AhmedFr/launch-kit

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Find LaunchPlan component and plan-to-markdown logic

# Search for LaunchPlan component files
fd -e ts -e tsx | head -20

# Find files that might contain LaunchPlan component
rg -l "LaunchPlan" --type ts -g '*.ts' -g '*.tsx'

# Find plan-to-markdown conversion logic
rg -l "plan.*markdown\|markdown.*plan" --type ts -g '*.ts' -g '*.tsx'

# Search for defensive checks on empty arrays (phases, tasks)
rg -nC2 "phases\|tasks" --type ts -g '*.ts' -g '*.tsx' | head -100

Repository: AhmedFr/launch-kit

Length of output: 1188


🏁 Script executed:

#!/bin/bash
# Read LaunchPlan component
echo "=== LaunchPlan Component ==="
cat -n components/launch-plan/LaunchPlan/LaunchPlan.tsx

echo -e "\n=== plan-to-markdown conversion ==="
cat -n lib/export/plan-to-markdown.ts

Repository: AhmedFr/launch-kit

Length of output: 3332


Enforce non-empty constraints for phases and tasks arrays.

The schema allows phases and tasks arrays to be empty, producing invalid outputs. While the Checklist component defensively handles other empty arrays (countdown, seoGeo, momentum), the LaunchPlan component renders empty sections if phases is empty (line 28-36), and plan-to-markdown creates empty section headers with no content if phases or tasks are empty (lines 10-12). Adding .nonempty() constraints to both would prevent semantically invalid data at the source rather than relying on inconsistent downstream handling.

Suggested schema constraints
 export const launchPlanSchema = z.object({
-  phases: z.array(z.object({ window: z.string(), goal: z.string(), tasks: z.array(z.string()) })),
+  phases: z.array(z.object({ 
+    window: z.string(), 
+    goal: z.string(), 
+    tasks: z.array(z.string()).nonempty() 
+  })).nonempty(),
   countdown30: z.array(z.string()),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const launchPlanSchema = z.object({
phases: z.array(z.object({ window: z.string(), goal: z.string(), tasks: z.array(z.string()) })),
countdown30: z.array(z.string()),
countdown7: z.array(z.string()),
countdown48h: z.array(z.string()),
seoGeo: z.array(z.string()),
momentum: z.array(z.string()),
export const launchPlanSchema = z.object({
phases: z.array(z.object({
window: z.string(),
goal: z.string(),
tasks: z.array(z.string()).nonempty()
})).nonempty(),
countdown30: z.array(z.string()),
countdown7: z.array(z.string()),
countdown48h: z.array(z.string()),
seoGeo: z.array(z.string()),
momentum: z.array(z.string()),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/generation/plan/plan.schema.ts` around lines 5 - 11, The launchPlanSchema
allows phases and tasks arrays to be empty, which produces invalid outputs. Add
the .nonempty() constraint to the z.array() calls for the phases array and the
tasks array nested within the phases object to enforce that both arrays must
contain at least one element. This prevents semantically invalid data from being
generated at the schema level rather than relying on defensive handling
downstream in the Checklist and LaunchPlan components.

Comment on lines 14 to +21
const SHAPE = `{
"title": "a 'Show HN:' title — honest and specific, no hype, ideally <80 chars",
"postBody": "the text post: what it does, why you built it, how it works technically. First-person, humble, 2-4 short paragraphs; \\n for line breaks",
"firstComment": "your first comment: extra technical context + one specific question inviting feedback",
"postingTips": {
"bestTimeET": "best day and time to post, in US Eastern Time",
"avoid": ["3-4 things to avoid when posting this on HN"]
"avoid": ["3-4 things to avoid when posting this on HN"],
"etiquette": ["3-4 rules for engaging authentically: answer critique directly, stay humble, no vote-asking, reply technically"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify hackerNewsSchema includes postingTips.etiquette

# Check the schema definition for the etiquette field
ast-grep --pattern $'postingTips: z.object({
  $$$
  etiquette: $$$
  $$$
})'

Repository: AhmedFr/launch-kit

Length of output: 44


🏁 Script executed:

fd -t f "schema.ts" lib/generation/platforms/hacker-news/

Repository: AhmedFr/launch-kit

Length of output: 109


🏁 Script executed:

cat -n lib/generation/platforms/hacker-news/schema.ts | head -100

Repository: AhmedFr/launch-kit

Length of output: 621


Align SHAPE description with schema optionality.

The etiquette field exists in the Zod schema but is marked as .optional() for back-compat, while the SHAPE constant treats it as a required field. The LLM will consistently generate etiquette based on the SHAPE description, yet the schema allows it to be omitted. Update either:

  • Remove etiquette from SHAPE if it should be optional, or
  • Document in the schema comment why the LLM must always generate it despite the .optional() marking.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/generation/platforms/hacker-news/prompt.ts` around lines 14 - 21, The
SHAPE constant includes the "etiquette" field, treating it as required, but the
corresponding Zod schema marks this field as optional using .optional(),
creating a mismatch between what the LLM will generate and what the schema
expects. Either remove the "etiquette" field from the SHAPE constant to reflect
that it is optional in the schema, or if the etiquette field must always be
generated by the LLM, remove the .optional() marking from the schema and add a
comment explaining why it must always be present despite allowing omission for
backward compatibility.

Comment on lines +4 to +9
export const socialSchema = z.object({
thread: z.object({ tweets: z.array(z.string()) }),
kolOutreach: z.object({ twitter: z.string(), linkedin: z.string(), telegram: z.string() }),
ugcAsk: z.string(),
postingTips: z.object({ bestTimeET: z.string(), hashtags: z.array(z.string()) }),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding validation constraints.

The schema accepts unbounded arrays for tweets and hashtags, and unconstrained strings for DM templates. Consider adding:

  • .min(1) on tweets to ensure at least one tweet
  • .max(12) on tweets to prevent excessively long threads
  • .min(1).max(10) on hashtags
  • .min(10) on twitter/linkedin/telegram to ensure meaningful DM content
🛡️ Proposed validation enhancements
 export const socialSchema = z.object({
-  thread: z.object({ tweets: z.array(z.string()) }),
-  kolOutreach: z.object({ twitter: z.string(), linkedin: z.string(), telegram: z.string() }),
+  thread: z.object({ tweets: z.array(z.string()).min(1).max(12) }),
+  kolOutreach: z.object({ 
+    twitter: z.string().min(10), 
+    linkedin: z.string().min(10), 
+    telegram: z.string().min(10) 
+  }),
   ugcAsk: z.string(),
-  postingTips: z.object({ bestTimeET: z.string(), hashtags: z.array(z.string()) }),
+  postingTips: z.object({ 
+    bestTimeET: z.string(), 
+    hashtags: z.array(z.string()).min(1).max(10) 
+  }),
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const socialSchema = z.object({
thread: z.object({ tweets: z.array(z.string()) }),
kolOutreach: z.object({ twitter: z.string(), linkedin: z.string(), telegram: z.string() }),
ugcAsk: z.string(),
postingTips: z.object({ bestTimeET: z.string(), hashtags: z.array(z.string()) }),
})
export const socialSchema = z.object({
thread: z.object({ tweets: z.array(z.string()).min(1).max(12) }),
kolOutreach: z.object({
twitter: z.string().min(10),
linkedin: z.string().min(10),
telegram: z.string().min(10)
}),
ugcAsk: z.string(),
postingTips: z.object({
bestTimeET: z.string(),
hashtags: z.array(z.string()).min(1).max(10)
}),
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/generation/platforms/social/schema.ts` around lines 4 - 9, The
socialSchema object is missing validation constraints that could prevent invalid
data. Add the following validation bounds to the schema: apply `.min(1).max(12)`
to the tweets array within the thread object to ensure at least one tweet but
prevent excessively long threads, apply `.min(1).max(10)` to the hashtags array
within the postingTips object, and apply `.min(10)` to each of the string fields
twitter, linkedin, and telegram within the kolOutreach object to ensure they
contain meaningful content, and add `.min(10)` to the ugcAsk string field to
validate minimum length for DM templates.

Launch-day ops were the last of five Product Hunt sections, so reaching the
checklists, hour-by-hour battle plan, momentum, moderation, and outreach meant
scrolling past copy, topics, gallery, and video. Add a third 'Launch ops' view
beside Edit | Preview, shown only for Product Hunt (the sole platform with
launch-day ops) and falling back to Edit elsewhere. Ops are removed from the Edit
view so they live in exactly one place. Covered by a new PostKit render test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AhmedFr
AhmedFr merged commit 6394bdf into main Jun 21, 2026
4 checks passed
@AhmedFr
AhmedFr deleted the feat/gingiris-playbook-integration branch June 21, 2026 12:04

This branch was successfully deployed

1 active deployment
Preview — 6cccca09 Deployed Jun 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant