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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to /lib/agent

Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
);
});
});
13 changes: 13 additions & 0 deletions src/lib/programs/orchestrator/agent-prompt-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
]
Expand Down
25 changes: 21 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 } from 'fs';
import { existsSync, rmSync } from 'fs';
import * as path from 'path';
import {
initializeAgent,
Expand Down Expand Up @@ -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}`,
);
Expand All @@ -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
Expand All @@ -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();

Expand Down
Loading