From aa39bcd8c36f62f4f28d748d4a3017a58e2c8785 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 10 Jun 2026 16:55:06 -0400 Subject: [PATCH] feat(orchestrator): task instructions are ephemeral, not keepable skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-task mini-skills install under .posthog-wizard/skills/ instead of .claude/skills/ — the SDK must not auto-load them, the keep-skills outro must not offer them, and they must never land in the project or a CI PR. The task prompt points the agent at its SKILL.md instead, and the whole directory is removed when the run ends, success or failure. Closes #632 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/agent-prompt-loader.test.ts | 24 ++++++++++++++++++ .../orchestrator/agent-prompt-loader.ts | 13 ++++++++++ .../orchestrator/orchestrator-runner.ts | 25 ++++++++++++++++--- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/lib/programs/orchestrator/__tests__/agent-prompt-loader.test.ts b/src/lib/programs/orchestrator/__tests__/agent-prompt-loader.test.ts index 64a4bdab..8252e791 100644 --- a/src/lib/programs/orchestrator/__tests__/agent-prompt-loader.test.ts +++ b/src/lib/programs/orchestrator/__tests__/agent-prompt-loader.test.ts @@ -3,11 +3,13 @@ import * as os from 'os'; import * as path from 'path'; import { agentRunTools, + assembleTaskPrompt, buildRegistry, parseAgentPrompt, resolveTask, type AgentPrompt, type AgentRegistry, + type OrchestratorPromptContext, } from '../agent-prompt-loader'; import { QueueStore } from '../queue'; @@ -203,3 +205,25 @@ describe('resolveTask', () => { ); }); }); + +describe('assembleTaskPrompt', () => { + const ctx: OrchestratorPromptContext = { + projectId: 1, + projectApiKey: 'phc_x', + host: 'https://us.posthog.com', + }; + + it('points the agent at its installed task instructions', () => { + const assembled = assembleTaskPrompt(ctx, 'do the task', [ + '.posthog-wizard/skills/capture/SKILL.md', + ]); + expect(assembled).toContain('.posthog-wizard/skills/capture/SKILL.md'); + expect(assembled).toContain('do the task'); + }); + + it('omits the instructions section when no skills are installed', () => { + expect(assembleTaskPrompt(ctx, 'do the task')).not.toContain( + 'task instructions', + ); + }); +}); diff --git a/src/lib/programs/orchestrator/agent-prompt-loader.ts b/src/lib/programs/orchestrator/agent-prompt-loader.ts index ee351db8..902adaee 100644 --- a/src/lib/programs/orchestrator/agent-prompt-loader.ts +++ b/src/lib/programs/orchestrator/agent-prompt-loader.ts @@ -49,6 +49,17 @@ function exampleReference(ctx: OrchestratorPromptContext): string | null { return `A reference PostHog integration for this framework is at \`${ctx.examplePath}\`. It shows the target implementation pattern. Reference its patterns and conventions, adapting them to this codebase.`; } +/** + * Points the agent at its installed task instructions (the HOW). They live under + * the wizard's run dir, not `.claude/skills/`, so the SDK does not auto-load + * them — the prompt has to name them. + */ +function skillReference(paths: readonly string[]): string | null { + if (paths.length === 0) return null; + const list = paths.map((p) => `\`${p}\``).join(', '); + return `Your task instructions are at ${list}. Read them before you start and follow them. They are wizard scaffolding, not part of the project.`; +} + /** The framework's rules ship with the reference skill; every task follows them. */ function commandmentsReference(ctx: OrchestratorPromptContext): string | null { if (!ctx.commandmentsPath) return null; @@ -63,11 +74,13 @@ const SEED_BASICS = `You are the orchestrator. Plan the work and seed the queue export function assembleTaskPrompt( ctx: OrchestratorPromptContext, body: string, + skillPaths: readonly string[] = [], ): string { return [ projectContext(ctx), exampleReference(ctx), commandmentsReference(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 978a8f31..8f2aac90 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 } from 'fs'; +import { existsSync, rmSync } from 'fs'; import * as path from 'path'; import { initializeAgent, @@ -210,18 +210,27 @@ export async function runOrchestrator( // parallel, the seed's graph being the only schedule. Each task resolves to // its agent prompt (the WHAT) and the mini-skills it needs (the HOW), then // runs on its own model and tools. + const taskSkillsRoot = path.join(QUEUE_DIR_NAME, 'skills'); const runTask: RunTask = async (task) => { renderQueue(); try { const resolved = resolveTask(registry, task, store); const agent = await initializeAgent(agentConfigFor(task.id), options); + // Task instructions are one-run scaffolding, not durable skills, so they + // install under the run dir rather than .claude/skills — the SDK must not + // 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) { const result = await installSkillById( skillId, session.installDir, boot.skillsBaseUrl, + taskSkillsRoot, ); - if (result.kind !== 'ok') { + if (result.kind === 'ok') { + skillPaths.push(path.join(result.path, 'SKILL.md')); + } else { logToFile( `[orchestrator] skill install failed type=${task.type} skill=${skillId} ${result.kind}`, ); @@ -234,7 +243,7 @@ export async function runOrchestrator( allowedTools: resolved.allowedTools, disallowedTools: resolved.disallowedTools, }, - assembleTaskPrompt(promptContext, resolved.prompt), + assembleTaskPrompt(promptContext, resolved.prompt, skillPaths), options, spinner, // Empty messages suppress the per-task spinner lines (the spinner renders @@ -252,7 +261,15 @@ export async function runOrchestrator( renderQueue(); } }; - await drainQueue(store, runTask); + try { + await drainQueue(store, runTask); + } finally { + // Success or failure, the installed task instructions never outlive the run. + rmSync(path.join(session.installDir, taskSkillsRoot), { + recursive: true, + force: true, + }); + } renderQueue();