Skip to content
Closed
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
20 changes: 20 additions & 0 deletions src/lib/agent/__tests__/agent-prompt-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
});
});
12 changes: 12 additions & 0 deletions src/lib/agent/agent-prompt-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.`;
Expand All @@ -80,6 +91,7 @@ export function assembleTaskPrompt(
projectContext(ctx),
exampleReference(ctx),
commandmentsReference(ctx),
docsReference(ctx),
skillReference(skillPaths),
TASK_BASICS,
body,
Expand Down
59 changes: 55 additions & 4 deletions src/lib/programs/orchestrator/orchestrator-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 `<baseId>-<framework>`. Resolve the agent's bare skill id to that variant:
* exact `<baseId>-<framework>`, 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<string> {
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,
Expand Down Expand Up @@ -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;
Expand All @@ -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})`,
Expand All @@ -228,6 +273,7 @@ export async function runOrchestrator(
host: boot.host,
examplePath,
commandmentsPath,
docsPaths,
};

logToFile(
Expand Down Expand Up @@ -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,
Expand Down
Loading