Repository navigation
feat: integrate Gingiris launch playbook + upgrade AI generation - #6
Conversation
… 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds a new Social/KOL platform and a cross-platform ChangesSocial Platform and Cross-Platform Launch Plan
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (47)
README.mdapp/api/generate/route.tsapp/runs/[id]/page.tsxcomponents/launch-plan/LaunchPlan/LaunchPlan.test.tsxcomponents/launch-plan/LaunchPlan/LaunchPlan.tsxcomponents/launch-plan/LaunchPlan/LaunchPlan.types.tscomponents/launch-plan/LaunchPlan/index.tscomponents/post-kit/PostKit/PostKit.tsxcomponents/post-kit/PostKit/PostKit.types.tscomponents/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsxcomponents/post-kit/platforms/RedditSections/RedditSections.tsxcomponents/post-kit/platforms/SocialSections/SocialSections.test.tsxcomponents/post-kit/platforms/SocialSections/SocialSections.tsxcomponents/post-kit/platforms/SocialSections/index.tscomponents/post-kit/platforms/registry.tscomponents/post-kit/sections/LaunchOpsSection/LaunchOpsSection.tsxcomponents/preview/SocialPreview/SocialPreview.tsxcomponents/preview/SocialPreview/index.tslib/export/plan-to-markdown.test.tslib/export/plan-to-markdown.tslib/export/to-markdown.test.tslib/export/to-markdown.tslib/generation/core/core.prompt.tslib/generation/core/core.schema.tslib/generation/mock-provider.test.tslib/generation/mock-provider.tslib/generation/openrouter-provider.tslib/generation/plan/plan.prompt.tslib/generation/plan/plan.schema.test.tslib/generation/plan/plan.schema.tslib/generation/platforms/appsumo/prompt.tslib/generation/platforms/hacker-news/prompt.tslib/generation/platforms/hacker-news/schema.tslib/generation/platforms/product-hunt/prompt.tslib/generation/platforms/product-hunt/schema.tslib/generation/platforms/reddit/prompt.tslib/generation/platforms/reddit/schema.tslib/generation/platforms/registry.tslib/generation/platforms/social/prompt.tslib/generation/platforms/social/schema.tslib/generation/prompt-shared.tslib/generation/prompt.test.tslib/generation/provider.types.tslib/generation/schema.test.tslib/platforms.tslib/preview/registry.tslib/types.ts
| // 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 | ||
| }) |
There was a problem hiding this comment.
🧹 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.
| async function copyPlan() { | ||
| const md = planMarkdown() | ||
| if (!md) return | ||
| await navigator.clipboard.writeText(md) | ||
| toast.success('Launch plan copied') | ||
| } |
There was a problem hiding this comment.
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.
| 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> |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
| <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.
| 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) | ||
| }) |
There was a problem hiding this comment.
🧹 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.
| 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.
| const keywords = Array.from( | ||
| new Set([...context.techStack, ...context.features.map((f) => f.toLowerCase())]), | ||
| ).slice(0, 8) |
There was a problem hiding this comment.
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), wherereplace(/[^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.
| 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.
| 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()), |
There was a problem hiding this comment.
🧩 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=tsxRepository: 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 -100Repository: 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.tsRepository: 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.
| 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.
| 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"] |
There was a problem hiding this comment.
🧩 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 -100Repository: 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
etiquettefrom 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.
| 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()) }), | ||
| }) |
There was a problem hiding this comment.
🧹 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)ontweetsto ensure at least one tweet.max(12)ontweetsto prevent excessively long threads.min(1).max(10)onhashtags.min(10)ontwitter/linkedin/telegramto 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.
| 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>
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)
PLAYBOOK_PRINCIPLESprepended 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).LaunchCorewithvalueProp,icp, andkeywords— distilled once and inherited by every platform viarenderCore, and used to make SEO/GEO and targeting product‑specific.New features (the "implement all tips" ask)
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/generateagainst 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 typecheckpnpm lintpnpm buildChecklist
.envvalues committed (.envis gitignored; only an out‑of‑date model comment in the local.envwas corrected)Screenshots / notes
docs/screenshots/kit.pngis now slightly out of date.temperaturesetting on the OpenRouter request for tighter JSON.Summary by CodeRabbit
New Features
Enhancements