Skip to content

Commit 63c0e48

Browse files
gewenyu99claude
andcommitted
feat(orchestrator): task instructions are ephemeral, not keepable skills
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) <noreply@anthropic.com>
1 parent cf2068f commit 63c0e48

3 files changed

Lines changed: 58 additions & 4 deletions

File tree

src/lib/programs/orchestrator/__tests__/agent-prompt-loader.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import * as os from 'os';
33
import * as path from 'path';
44
import {
55
agentRunTools,
6+
assembleTaskPrompt,
67
buildRegistry,
78
parseAgentPrompt,
89
resolveTask,
910
type AgentPrompt,
1011
type AgentRegistry,
12+
type OrchestratorPromptContext,
1113
} from '../agent-prompt-loader';
1214
import { QueueStore } from '../queue';
1315

@@ -203,3 +205,25 @@ describe('resolveTask', () => {
203205
);
204206
});
205207
});
208+
209+
describe('assembleTaskPrompt', () => {
210+
const ctx: OrchestratorPromptContext = {
211+
projectId: 1,
212+
projectApiKey: 'phc_x',
213+
host: 'https://us.posthog.com',
214+
};
215+
216+
it('points the agent at its installed task instructions', () => {
217+
const assembled = assembleTaskPrompt(ctx, 'do the task', [
218+
'.posthog-wizard/skills/capture/SKILL.md',
219+
]);
220+
expect(assembled).toContain('.posthog-wizard/skills/capture/SKILL.md');
221+
expect(assembled).toContain('do the task');
222+
});
223+
224+
it('omits the instructions section when no skills are installed', () => {
225+
expect(assembleTaskPrompt(ctx, 'do the task')).not.toContain(
226+
'task instructions',
227+
);
228+
});
229+
});

src/lib/programs/orchestrator/agent-prompt-loader.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ function exampleReference(ctx: OrchestratorPromptContext): string | null {
4949
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.`;
5050
}
5151

52+
/**
53+
* Points the agent at its installed task instructions (the HOW). They live under
54+
* the wizard's run dir, not `.claude/skills/`, so the SDK does not auto-load
55+
* them — the prompt has to name them.
56+
*/
57+
function skillReference(paths: readonly string[]): string | null {
58+
if (paths.length === 0) return null;
59+
const list = paths.map((p) => `\`${p}\``).join(', ');
60+
return `Your task instructions are at ${list}. Read them before you start and follow them. They are wizard scaffolding, not part of the project.`;
61+
}
62+
5263
/** The framework's rules ship with the reference skill; every task follows them. */
5364
function commandmentsReference(ctx: OrchestratorPromptContext): string | null {
5465
if (!ctx.commandmentsPath) return null;
@@ -63,11 +74,13 @@ const SEED_BASICS = `You are the orchestrator. Plan the work and seed the queue
6374
export function assembleTaskPrompt(
6475
ctx: OrchestratorPromptContext,
6576
body: string,
77+
skillPaths: readonly string[] = [],
6678
): string {
6779
return [
6880
projectContext(ctx),
6981
exampleReference(ctx),
7082
commandmentsReference(ctx),
83+
skillReference(skillPaths),
7184
TASK_BASICS,
7285
body,
7386
]

src/lib/programs/orchestrator/orchestrator-runner.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* stays product-ignorant: it is the queue, the executor, and the loader.
1212
*/
1313
import { randomUUID } from 'crypto';
14-
import { existsSync } from 'fs';
14+
import { existsSync, rmSync } from 'fs';
1515
import * as path from 'path';
1616
import {
1717
initializeAgent,
@@ -210,18 +210,27 @@ export async function runOrchestrator(
210210
// parallel, the seed's graph being the only schedule. Each task resolves to
211211
// its agent prompt (the WHAT) and the mini-skills it needs (the HOW), then
212212
// runs on its own model and tools.
213+
const taskSkillsRoot = path.join(QUEUE_DIR_NAME, 'skills');
213214
const runTask: RunTask = async (task) => {
214215
renderQueue();
215216
try {
216217
const resolved = resolveTask(registry, task, store);
217218
const agent = await initializeAgent(agentConfigFor(task.id), options);
219+
// Task instructions are one-run scaffolding, not durable skills, so they
220+
// install under the run dir rather than .claude/skills — the SDK must not
221+
// auto-load them and they must never land in the project (or a CI PR).
222+
// The prompt points the agent at them instead.
223+
const skillPaths: string[] = [];
218224
for (const skillId of resolved.skills) {
219225
const result = await installSkillById(
220226
skillId,
221227
session.installDir,
222228
boot.skillsBaseUrl,
229+
taskSkillsRoot,
223230
);
224-
if (result.kind !== 'ok') {
231+
if (result.kind === 'ok') {
232+
skillPaths.push(path.join(result.path, 'SKILL.md'));
233+
} else {
225234
logToFile(
226235
`[orchestrator] skill install failed type=${task.type} skill=${skillId} ${result.kind}`,
227236
);
@@ -234,7 +243,7 @@ export async function runOrchestrator(
234243
allowedTools: resolved.allowedTools,
235244
disallowedTools: resolved.disallowedTools,
236245
},
237-
assembleTaskPrompt(promptContext, resolved.prompt),
246+
assembleTaskPrompt(promptContext, resolved.prompt, skillPaths),
238247
options,
239248
spinner,
240249
// Empty messages suppress the per-task spinner lines (the spinner renders
@@ -252,7 +261,15 @@ export async function runOrchestrator(
252261
renderQueue();
253262
}
254263
};
255-
await drainQueue(store, runTask);
264+
try {
265+
await drainQueue(store, runTask);
266+
} finally {
267+
// Success or failure, the installed task instructions never outlive the run.
268+
rmSync(path.join(session.installDir, taskSkillsRoot), {
269+
recursive: true,
270+
force: true,
271+
});
272+
}
256273

257274
renderQueue();
258275

0 commit comments

Comments
 (0)