From 505fbd1dfe82e043da38cb545d69f2c9da6478b6 Mon Sep 17 00:00:00 2001
From: "Vincent (Wen Yu) Ge" <gewenyu99@gmail.com>
Date: Thu, 18 Jun 2026 11:28:54 -0400
Subject: [PATCH 1/3] feat(orchestrator): surface per-framework SDK docs to
 task agents

Collect the reference skill's per-framework docs (everything in references/
except EXAMPLE.md, COMMANDMENTS.md, and the monolith's numbered workflow files)
and inject them via a new docsReference, so docs-only frameworks (Go, Elixir,
Flutter) and every non-JS framework get their real SDK reference instead of the
step-skill's wrong JS/Node docs or nothing.

Also watch the event plan in the run cache dir as well as the project root: the
orchestrator capture step now writes .posthog-events.json under
.posthog-wizard-cache (torn down with the run), while the linear flow still
writes it at the root. The TUI can't tell the flows apart, so it watches both.

Pairs with the context-mill basic-integration skill changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 .../__tests__/agent-prompt-loader.test.ts     | 20 +++++++++++++
 src/lib/agent/agent-prompt-loader.ts          | 12 ++++++++
 .../orchestrator/orchestrator-runner.ts       | 28 +++++++++++++++++--
 src/ui/tui/screens/RunScreen.tsx              | 13 +++++++--
 4 files changed, 68 insertions(+), 5 deletions(-)

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..39c629ec2 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,
@@ -190,6 +190,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 +202,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 +249,7 @@ export async function runOrchestrator(
     host: boot.host,
     examplePath,
     commandmentsPath,
+    docsPaths,
   };
 
   logToFile(
diff --git a/src/ui/tui/screens/RunScreen.tsx b/src/ui/tui/screens/RunScreen.tsx
index ce4406057..d139c598e 100644
--- a/src/ui/tui/screens/RunScreen.tsx
+++ b/src/ui/tui/screens/RunScreen.tsx
@@ -25,6 +25,7 @@ import { TipsCard } from '@ui/tui/components/TipsCard';
 import { useStdoutDimensions } from '@ui/tui/hooks/useStdoutDimensions';
 import { useFileWatcher } from '@ui/tui/hooks/file-watcher';
 import { EVENT_PLAN_FILE } from '@lib/programs/posthog-integration/index';
+import { QUEUE_DIR_NAME } from '@lib/programs/orchestrator/queue';
 import { getProgramConfig } from '@lib/programs/program-registry';
 import { getContentBlocks as getSkillContentBlocks } from '@lib/programs/agent-skill/content/index';
 
@@ -46,7 +47,7 @@ export const RunScreen = ({ store }: RunScreenProps) => {
   // form); `name`/`event`/`description` are legacy fallbacks for skills or
   // one-off runs that drift. Drop any entry that still ends up nameless so
   // the outro never shows blank bullets.
-  useFileWatcher(join(store.session.installDir, EVENT_PLAN_FILE), (parsed) => {
+  const onEventPlan = (parsed: unknown) => {
     if (!Array.isArray(parsed)) return;
     store.setEventPlan(
       parsed
@@ -56,7 +57,15 @@ export const RunScreen = ({ store }: RunScreenProps) => {
         }))
         .filter((e) => e.name),
     );
-  });
+  };
+  // The linear flow writes the plan at the project root; the orchestrator
+  // writes it inside its run cache (cleaned up with the rest, so it never
+  // lands in the project). The TUI can't tell the flows apart, so watch both.
+  useFileWatcher(join(store.session.installDir, EVENT_PLAN_FILE), onEventPlan);
+  useFileWatcher(
+    join(store.session.installDir, QUEUE_DIR_NAME, EVENT_PLAN_FILE),
+    onEventPlan,
+  );
 
   const [columns] = useStdoutDimensions();
 

From a27a6a0edd4bc2ea8db869f14fd9f88fb98662df Mon Sep 17 00:00:00 2001
From: "Vincent (Wen Yu) Ge" <gewenyu99@gmail.com>
Date: Thu, 18 Jun 2026 14:00:57 -0400
Subject: [PATCH 2/3] feat(orchestrator): resolve per-framework step-skill
 variants

The SDK-divergent step-skills (install, init, capture, error-tracking) now ship
per-framework variants ided as <baseId>-<framework>. Resolve each agent's bare
skill id to the variant for the run's framework before installing, mirroring
resolveReferenceSkillId: exact match, else first granular variant, else the bare
id unchanged for the generic single-variant steps (identify, report, dashboard,
build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 .../orchestrator/orchestrator-runner.ts       | 31 ++++++++++++++++++-
 1 file changed, 30 insertions(+), 1 deletion(-)

diff --git a/src/lib/programs/orchestrator/orchestrator-runner.ts b/src/lib/programs/orchestrator/orchestrator-runner.ts
index 39c629ec2..ff10a420b 100644
--- a/src/lib/programs/orchestrator/orchestrator-runner.ts
+++ b/src/lib/programs/orchestrator/orchestrator-runner.ts
@@ -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,
@@ -347,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,

From 3db6a36eb736b41cd35b9b9fc1b47673c46a8d98 Mon Sep 17 00:00:00 2001
From: "Vincent (Wen Yu) Ge" <gewenyu99@gmail.com>
Date: Thu, 18 Jun 2026 14:10:15 -0400
Subject: [PATCH 3/3] revert: keep the event-plan watcher out of the shared
 RunScreen

The orchestrator's cache-dir event-plan watcher ran in the default RunScreen,
hitting every flow ungated. All orchestrator behaviour must sit behind the
wizard-orchestrator flag. Restore RunScreen to its main version; the orchestrator
event-plan surfacing will be re-added inside the flag-gated orchestrator path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 src/ui/tui/screens/RunScreen.tsx | 13 ++-----------
 1 file changed, 2 insertions(+), 11 deletions(-)

diff --git a/src/ui/tui/screens/RunScreen.tsx b/src/ui/tui/screens/RunScreen.tsx
index d139c598e..ce4406057 100644
--- a/src/ui/tui/screens/RunScreen.tsx
+++ b/src/ui/tui/screens/RunScreen.tsx
@@ -25,7 +25,6 @@ import { TipsCard } from '@ui/tui/components/TipsCard';
 import { useStdoutDimensions } from '@ui/tui/hooks/useStdoutDimensions';
 import { useFileWatcher } from '@ui/tui/hooks/file-watcher';
 import { EVENT_PLAN_FILE } from '@lib/programs/posthog-integration/index';
-import { QUEUE_DIR_NAME } from '@lib/programs/orchestrator/queue';
 import { getProgramConfig } from '@lib/programs/program-registry';
 import { getContentBlocks as getSkillContentBlocks } from '@lib/programs/agent-skill/content/index';
 
@@ -47,7 +46,7 @@ export const RunScreen = ({ store }: RunScreenProps) => {
   // form); `name`/`event`/`description` are legacy fallbacks for skills or
   // one-off runs that drift. Drop any entry that still ends up nameless so
   // the outro never shows blank bullets.
-  const onEventPlan = (parsed: unknown) => {
+  useFileWatcher(join(store.session.installDir, EVENT_PLAN_FILE), (parsed) => {
     if (!Array.isArray(parsed)) return;
     store.setEventPlan(
       parsed
@@ -57,15 +56,7 @@ export const RunScreen = ({ store }: RunScreenProps) => {
         }))
         .filter((e) => e.name),
     );
-  };
-  // The linear flow writes the plan at the project root; the orchestrator
-  // writes it inside its run cache (cleaned up with the rest, so it never
-  // lands in the project). The TUI can't tell the flows apart, so watch both.
-  useFileWatcher(join(store.session.installDir, EVENT_PLAN_FILE), onEventPlan);
-  useFileWatcher(
-    join(store.session.installDir, QUEUE_DIR_NAME, EVENT_PLAN_FILE),
-    onEventPlan,
-  );
+  });
 
   const [columns] = useStdoutDimensions();
 
