diff --git a/src/lib/agent/__tests__/agent-prompt-loader.test.ts b/src/lib/agent/__tests__/agent-prompt-loader.test.ts index d2ad5b7ae..0cfc92138 100644 --- a/src/lib/agent/__tests__/agent-prompt-loader.test.ts +++ b/src/lib/agent/__tests__/agent-prompt-loader.test.ts @@ -296,4 +296,24 @@ describe('assembleTaskPrompt', () => { 'task instructions', ); }); + + it('surfaces the per-framework SDK docs when present', () => { + const assembled = assembleTaskPrompt( + { + ...ctx, + docsPaths: ['.posthog-wizard-cache/reference/references/go.md'], + }, + 'do the task', + ); + expect(assembled).toContain('SDK reference docs'); + expect(assembled).toContain( + '.posthog-wizard-cache/reference/references/go.md', + ); + }); + + it('omits the SDK docs section when there are none', () => { + expect(assembleTaskPrompt(ctx, 'do the task')).not.toContain( + 'SDK reference docs', + ); + }); }); diff --git a/src/lib/agent/agent-prompt-loader.ts b/src/lib/agent/agent-prompt-loader.ts index 8b83ff9ac..2854c10be 100644 --- a/src/lib/agent/agent-prompt-loader.ts +++ b/src/lib/agent/agent-prompt-loader.ts @@ -32,6 +32,9 @@ export interface OrchestratorPromptContext { examplePath?: string; /** Path to the framework's rules (COMMANDMENTS.md), if available. */ commandmentsPath?: string; + /** Paths to the framework's SDK reference docs (the per-framework `.md` files + * packaged in the reference skill), if any. */ + docsPaths?: readonly string[]; } function projectContext(ctx: OrchestratorPromptContext): string { @@ -55,6 +58,14 @@ function commandmentsReference(ctx: OrchestratorPromptContext): string | null { return `Framework rules for this integration are at \`${ctx.commandmentsPath}\`. Read them before you edit and follow them.`; } +/** The per-framework SDK docs ship with the reference skill. For docs-only + * frameworks (no EXAMPLE.md) they are the primary implementation signal. */ +function docsReference(ctx: OrchestratorPromptContext): string | null { + if (!ctx.docsPaths || ctx.docsPaths.length === 0) return null; + const list = ctx.docsPaths.map((p) => `\`${p}\``).join(', '); + return `The PostHog SDK reference docs for this framework are at ${list}. Read the ones relevant to your task before you edit.`; +} + const TASK_BASICS = `You are one isolated task in a larger PostHog workflow, run as a fresh agent with no memory of the other tasks beyond the context you are given. Do only your task, then report exactly once by calling complete_task with a structured handoff: what your goal was, what you did, and what the next agent should know. When you are given context from previous steps, trust it — those agents already did their work, so do not re-verify or re-read what their handoffs tell you. Build on it and move fast. Read a file before you edit it, so your own changes do not duplicate what is already there. Work only inside this project's own directory: never read, list, or search (find, ls, grep, glob) outside it — not the OS, not other projects, not global package caches. If your task seems to need something outside this directory, it does not — skip that part and say so in your handoff rather than hunting across the filesystem. If your task does not apply to this project — there is genuinely nothing for it to do — report it with status \`skipped\` and say why, rather than marking it done.`; const SEED_BASICS = `You are the orchestrator. Plan the work and seed the queue with enqueue_task — each call returns an id you can pass as a dependency to a later task. Give each task a short label for the UI — the action in a few words, not file names, class names, or other specifics. You are not a task yourself: do not call complete_task and do not edit the project.`; @@ -80,6 +91,7 @@ export function assembleTaskPrompt( projectContext(ctx), exampleReference(ctx), commandmentsReference(ctx), + docsReference(ctx), skillReference(skillPaths), TASK_BASICS, body, diff --git a/src/lib/programs/orchestrator/orchestrator-runner.ts b/src/lib/programs/orchestrator/orchestrator-runner.ts index 6353d8c80..ff10a420b 100644 --- a/src/lib/programs/orchestrator/orchestrator-runner.ts +++ b/src/lib/programs/orchestrator/orchestrator-runner.ts @@ -11,7 +11,7 @@ * stays product-ignorant: it is the queue, the executor, and the loader. */ import { randomUUID } from 'crypto'; -import { existsSync, rmSync } from 'fs'; +import { existsSync, readdirSync, rmSync } from 'fs'; import * as path from 'path'; import { initializeAgent, @@ -96,6 +96,30 @@ async function resolveReferenceSkillId( return ids.find((id) => id.startsWith(`integration-${framework}-`)); } +/** + * A step-skill (the HOW for one task) may ship per-framework variants — install, + * init, capture, and error-tracking each package the framework's docs page, ided + * as `-`. Resolve the agent's bare skill id to that variant: + * exact `-`, else the first granular variant under it, else + * the bare id unchanged (the generic single-variant steps — identify, report, + * dashboard, build — and the no-framework case). + */ +async function resolveStepSkillId( + skillsBaseUrl: string, + baseId: string, + framework: string | null | undefined, +): Promise { + if (!framework) return baseId; + const menu = await fetchSkillMenu(skillsBaseUrl); + if (!menu) return baseId; + const ids = Object.values(menu.categories) + .flat() + .map((s) => s.id); + const exact = `${baseId}-${framework}`; + if (ids.includes(exact)) return exact; + return ids.find((id) => id.startsWith(`${baseId}-${framework}-`)) ?? baseId; +} + export async function runOrchestrator( session: WizardSession, programConfig: ProgramConfig, @@ -190,6 +214,7 @@ export async function runOrchestrator( // skill — only the example file is read, when the agent's prompt points at it. let examplePath: string | undefined; let commandmentsPath: string | undefined; + let docsPaths: string[] = []; const referenceSkillId = session.skillId ? await resolveReferenceSkillId(boot.skillsBaseUrl, session.skillId) : undefined; @@ -201,14 +226,34 @@ export async function runOrchestrator( path.join(QUEUE_DIR_NAME, 'reference'), ); if (ref.kind === 'ok') { - const example = path.join(ref.path, 'references', 'EXAMPLE.md'); + const refDir = path.join(ref.path, 'references'); + const example = path.join(refDir, 'EXAMPLE.md'); if (existsSync(path.join(session.installDir, example))) { examplePath = example; } - const commandments = path.join(ref.path, 'references', 'COMMANDMENTS.md'); + const commandments = path.join(refDir, 'COMMANDMENTS.md'); if (existsSync(path.join(session.installDir, commandments))) { commandmentsPath = commandments; } + // Surface the per-framework SDK docs (e.g. `go.md`, `next-js.md`) the + // skill packages — the HOW for this framework, and the only HOW signal + // for docs-only frameworks with no EXAMPLE.md. Skip the structural files: + // EXAMPLE.md and COMMANDMENTS.md are handled above, and the monolith's + // numbered workflow files (`1-begin.md` …) are its linear-flow narrative, + // not SDK reference — match those by their numeric prefix. + const refDirAbs = path.join(session.installDir, refDir); + if (existsSync(refDirAbs)) { + docsPaths = readdirSync(refDirAbs) + .filter( + (f) => + f.endsWith('.md') && + f !== 'EXAMPLE.md' && + f !== 'COMMANDMENTS.md' && + !/^\d+-/.test(f), + ) + .sort() + .map((f) => path.join(refDir, f)); + } } else { logToFile( `[orchestrator] reference unavailable: ${ref.kind} (${referenceSkillId})`, @@ -228,6 +273,7 @@ export async function runOrchestrator( host: boot.host, examplePath, commandmentsPath, + docsPaths, }; logToFile( @@ -325,7 +371,12 @@ export async function runOrchestrator( // auto-load them and they must never land in the project (or a CI PR). // The prompt points the agent at them instead. const skillPaths: string[] = []; - for (const skillId of resolved.skills) { + for (const baseSkillId of resolved.skills) { + const skillId = await resolveStepSkillId( + boot.skillsBaseUrl, + baseSkillId, + session.skillId, + ); const result = await installSkillById( skillId, session.installDir,