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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .github/hooks/post-edit-invalidate.sh
100644 → 100755
Empty file.
Empty file modified .github/hooks/pre-amend-block.sh
100644 → 100755
Empty file.
Empty file modified .github/hooks/pre-commit-block.sh
100644 → 100755
Empty file.
Empty file modified .github/hooks/pre-force-push-block.sh
100644 → 100755
Empty file.
Empty file modified .github/hooks/pre-layer-import.sh
100644 → 100755
Empty file.
Empty file modified .github/hooks/pre-layer-mock.sh
100644 → 100755
Empty file.
Empty file modified .github/hooks/pre-push-block.sh
100644 → 100755
Empty file.
Empty file modified .github/hooks/pre-reexport-block.sh
100644 → 100755
Empty file.
37 changes: 33 additions & 4 deletions src/L4-agents/BlogAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ interface WriteBlogArgs {

function buildSystemPrompt(ideaContext = ''): string {
const brand = getBrandConfig()
const hasWebSearch = Boolean(getConfig().EXA_API_KEY)
const researchWorkflow = hasWebSearch
? `1. First use the "web_search_exa" tool to search for relevant articles and resources to link to. Search for key topics from the video.
2. Then call "write_blog" with the complete blog post including frontmatter and body.
- Weave the search result links organically into the post text (don't dump them at the end).
- Reference the video and any shorts naturally.`
: `1. Use the transcript and summary as your source of truth for factual details.
2. Then call "write_blog" with the complete blog post including frontmatter and body.
- Do not invent links or unsupported claims when external research is unavailable.
- Reference the video and any shorts naturally.`

return `You are a technical blog writer for dev.to, writing from the perspective of ${brand.name} (${brand.handle}).${ideaContext}

Expand All @@ -43,12 +53,31 @@ The blog post MUST include:
5. Key Takeaways section
6. A conclusion
7. A footer referencing the original video
8. A community engagement CTA inviting readers to comment, share experiences, or ask questions

Workflow:
1. First use the "web_search_exa" tool to search for relevant articles and resources to link to. Search for key topics from the video.
2. Then call "write_blog" with the complete blog post including frontmatter and body.
- Weave the search result links organically into the post text (don't dump them at the end).
- Reference the video and any shorts naturally.
${researchWorkflow}

Word count enforcement:
- The blog MUST be at least 1,200 words (body only, excluding frontmatter). The 800-word minimum is a floor, not a target.
- If your draft is under 1,000 words, add more depth: expand explanations, add code examples, and include more personal narrative.

Voice requirements:
- Write in FIRST PERSON as the video creator: "I built", "I discovered", "here's what I learned"
- Use the personal developer narrative style of dev.to — conversational, opinionated, and grounded in real experience
- Avoid detached product-brochure phrasing like "the pipeline demonstrates impressive automation capabilities"

Code snippet requirement:
- If the video discusses any code, tools, or technical implementation, include at least 2 fenced code blocks with language tags
- Show real examples such as configuration snippets, command-line invocations, or project code relevant to the transcript

Link quality:
- NEVER use placeholder links like [text](#) or [text](link) — either use a real URL or omit the link entirely
- A blog post with 0 links is better than one with fake or irrelevant links

Section markers:
- Use emoji section markers for visual appeal, such as ## 🚀 The Problem, ## 💡 The Solution, and ## 🔑 Key Takeaways
- Include at least 4 major sections between the introduction and conclusion

Always call "write_blog" exactly once with the complete post.`
}
Expand Down
35 changes: 34 additions & 1 deletion src/__tests__/unit/L4-agents/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const mockState = vi.hoisted(() => {
const state = {
capturedTools: [] as any[],
capturedSystemPrompt: '' as string,
exaApiKey: '' as string,
mockSession: {
sendAndWait: async () => ({ data: { content: '' } }),
on: (event: string, handler: (...args: any[]) => void) => {
Expand Down Expand Up @@ -66,7 +67,7 @@ vi.mock('../../../L1-infra/config/environment.js', () => ({
OUTPUT_DIR: '/tmp/test-output',
LLM_PROVIDER: 'copilot',
LLM_MODEL: '',
EXA_API_KEY: '',
EXA_API_KEY: mockState.exaApiKey,
EXA_MCP_URL: 'https://mcp.exa.ai/mcp',
MODEL_OVERRIDES: {},
}),
Expand Down Expand Up @@ -710,6 +711,7 @@ describe('Real SummaryAgent', () => {
describe('Real BlogAgent', () => {
beforeEach(() => {
mockState.capturedTools.length = 0;
mockState.exaApiKey = '';
});

it('exposes write_blog tool; handler works (search is via MCP)', async () => {
Expand All @@ -736,6 +738,37 @@ describe('Real BlogAgent', () => {

expect(writeResult).toContain('success');
});

it('strengthens the prompt and falls back cleanly when EXA is unavailable', async () => {
const { generateBlogPost } = await import('../../../L4-agents/BlogAgent.js');

try {
await generateBlogPost(mockVideo, mockTranscript, mockSummary);
} catch {
// Expected: "BlogAgent did not produce any blog content"
}

const systemPrompt = mockState.capturedSystemPrompt;
expect(systemPrompt).toContain('community engagement CTA');
expect(systemPrompt).toContain('FIRST PERSON as the video creator');
expect(systemPrompt).toContain('at least 1,200 words');
expect(systemPrompt).toContain('at least 2 fenced code blocks');
expect(systemPrompt).toContain('Do not invent links or unsupported claims');
expect(systemPrompt).not.toContain('web_search_exa');
});

it('mentions web_search_exa when EXA research is configured', async () => {
const { generateBlogPost } = await import('../../../L4-agents/BlogAgent.js');
mockState.exaApiKey = 'test-exa-key';

try {
await generateBlogPost(mockVideo, mockTranscript, mockSummary);
} catch {
// Expected: "BlogAgent did not produce any blog content"
}

expect(mockState.capturedSystemPrompt).toContain('web_search_exa');
});
});

// ── SocialMediaAgent (REAL) ─────────────────────────────────────────────────
Expand Down