Skip to content

Commit cb21681

Browse files
committed
build a queue
1 parent 70cd200 commit cb21681

3 files changed

Lines changed: 232 additions & 74 deletions

File tree

src/lib/__tests__/workflow-queue.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
WizardWorkflowQueue,
33
createInitialWizardWorkflowQueue,
4+
createPostBootstrapQueue,
5+
parseWorkflowStepsFromSkillMd,
46
type WorkflowStepSeed,
57
} from '../workflow-queue';
68

@@ -70,6 +72,22 @@ describe('WizardWorkflowQueue', () => {
7072
]);
7173
});
7274

75+
it('createPostBootstrapQueue omits bootstrap', () => {
76+
const queue = createPostBootstrapQueue(BASIC_INTEGRATION_STEPS);
77+
const items = queue.toArray();
78+
79+
expect(items[0]).toEqual({
80+
id: 'workflow:1.0-begin',
81+
kind: 'workflow',
82+
referenceFilename: 'basic-integration-1.0-begin.md',
83+
});
84+
expect(items[items.length - 1]).toEqual({
85+
id: 'env-vars',
86+
kind: 'env-vars',
87+
});
88+
expect(items.find((i) => i.id === 'bootstrap')).toBeUndefined();
89+
});
90+
7391
it('supports enqueue and dequeue operations', () => {
7492
const queue = new WizardWorkflowQueue();
7593

@@ -91,3 +109,74 @@ describe('WizardWorkflowQueue', () => {
91109
expect(queue).toHaveLength(0);
92110
});
93111
});
112+
113+
describe('parseWorkflowStepsFromSkillMd', () => {
114+
it('parses workflow steps from SKILL.md frontmatter', () => {
115+
const skillMd = `---
116+
name: integration-nextjs-app-router
117+
description: PostHog integration for Next.js App Router applications
118+
metadata:
119+
author: PostHog
120+
version: dev
121+
workflow:
122+
- step_id: 1.0-begin
123+
reference: basic-integration-1.0-begin.md
124+
title: PostHog Setup - Begin
125+
next:
126+
- basic-integration-1.1-edit.md
127+
- step_id: 1.1-edit
128+
reference: basic-integration-1.1-edit.md
129+
title: PostHog Setup - Edit
130+
next:
131+
- basic-integration-1.2-revise.md
132+
- step_id: 1.2-revise
133+
reference: basic-integration-1.2-revise.md
134+
title: PostHog Setup - Revise
135+
next:
136+
- basic-integration-1.3-conclude.md
137+
- step_id: 1.3-conclude
138+
reference: basic-integration-1.3-conclude.md
139+
title: PostHog Setup - Conclusion
140+
next: []
141+
---
142+
143+
# PostHog integration for Next.js App Router
144+
`;
145+
146+
const steps = parseWorkflowStepsFromSkillMd(skillMd);
147+
148+
expect(steps).toEqual([
149+
{
150+
stepId: '1.0-begin',
151+
referenceFilename: 'basic-integration-1.0-begin.md',
152+
},
153+
{
154+
stepId: '1.1-edit',
155+
referenceFilename: 'basic-integration-1.1-edit.md',
156+
},
157+
{
158+
stepId: '1.2-revise',
159+
referenceFilename: 'basic-integration-1.2-revise.md',
160+
},
161+
{
162+
stepId: '1.3-conclude',
163+
referenceFilename: 'basic-integration-1.3-conclude.md',
164+
},
165+
]);
166+
});
167+
168+
it('returns empty array when no frontmatter', () => {
169+
expect(parseWorkflowStepsFromSkillMd('# No frontmatter')).toEqual([]);
170+
});
171+
172+
it('returns empty array when no workflow key', () => {
173+
const skillMd = `---
174+
name: feature-flags-nextjs
175+
description: docs only
176+
---
177+
178+
# Feature flags
179+
`;
180+
expect(parseWorkflowStepsFromSkillMd(skillMd)).toEqual([]);
181+
});
182+
});

src/lib/agent-runner.ts

Lines changed: 96 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import fs from 'fs';
2+
import path from 'path';
13
import {
24
DEFAULT_PACKAGE_INSTALLATION,
35
type FrameworkConfig,
@@ -39,9 +41,9 @@ import {
3941
} from '../utils/wizard-abort';
4042
import { formatScanReport, writeScanReport } from './yara-hooks';
4143
import {
42-
createInitialWizardWorkflowQueue,
44+
createPostBootstrapQueue,
45+
parseWorkflowStepsFromSkillMd,
4346
type WizardWorkflowQueueItem,
44-
type WorkflowStepSeed,
4547
} from './workflow-queue';
4648

4749
const WIZARD_SKILL_ID_SIGNAL = '[WIZARD-SKILL-ID]';
@@ -274,76 +276,106 @@ export async function runAgentWizard(
274276
? createBenchmarkPipeline(spinner, sessionToOptions(session))
275277
: undefined;
276278

277-
// TODO: S5 — seed from context-mill workflow manifest instead of this static list
278-
const workflowSteps: WorkflowStepSeed[] = [
279-
{
280-
stepId: '1.0-begin',
281-
referenceFilename: 'basic-integration-1.0-begin.md',
282-
},
283-
{ stepId: '1.1-edit', referenceFilename: 'basic-integration-1.1-edit.md' },
284-
{
285-
stepId: '1.2-revise',
286-
referenceFilename: 'basic-integration-1.2-revise.md',
287-
},
279+
// ── Step 1: Bootstrap — install the skill and get its ID ──
280+
281+
let agentResult = await runAgent(
282+
agent,
283+
buildBootstrapPrompt(config, promptContext, frameworkContext),
284+
sessionToOptions(session),
285+
spinner,
288286
{
289-
stepId: '1.3-conclude',
290-
referenceFilename: 'basic-integration-1.3-conclude.md',
287+
estimatedDurationMinutes: config.ui.estimatedDurationMinutes,
288+
spinnerMessage: 'Preparing integration...',
289+
successMessage: 'Integration prepared',
290+
errorMessage: 'Integration failed during bootstrap',
291+
additionalFeatureQueue: [],
292+
requestRemark: false,
293+
captureOutputText: true,
294+
captureSessionId: true,
295+
finalizeMiddleware: false,
291296
},
292-
];
293-
const queue = createInitialWizardWorkflowQueue(workflowSteps);
294-
let queuedSessionId: string | undefined;
295-
let installedSkillId: string | undefined;
296-
let agentResult: Awaited<ReturnType<typeof runAgent>> = {};
297-
298-
while (queue.length > 0) {
299-
const queueItem = queue.dequeue()!;
300-
const prompt = buildQueuedPrompt(
301-
queueItem,
302-
config,
303-
promptContext,
304-
frameworkContext,
297+
middleware,
298+
);
299+
300+
const queuedSessionId = agentResult.sessionId;
301+
const installedSkillId =
302+
extractInstalledSkillId(agentResult.outputText ?? '') ?? undefined;
303+
304+
if (!installedSkillId) {
305+
await wizardAbort({
306+
message:
307+
'The wizard could not determine which integration skill was installed during bootstrap.',
308+
error: new WizardError('Bootstrap step did not emit installed skill id'),
309+
});
310+
}
311+
312+
// ── Step 2: Read SKILL.md and seed the queue from its frontmatter ──
313+
314+
if (!agentResult.error && installedSkillId) {
315+
const skillMdPath = path.join(
316+
session.installDir,
317+
'.claude',
318+
'skills',
305319
installedSkillId,
320+
'SKILL.md',
306321
);
322+
const skillMdContent = fs.readFileSync(skillMdPath, 'utf-8');
323+
const workflowSteps = parseWorkflowStepsFromSkillMd(skillMdContent);
307324

308-
agentResult = await runAgent(
309-
agent,
310-
prompt,
311-
sessionToOptions(session),
312-
spinner,
313-
{
314-
estimatedDurationMinutes: config.ui.estimatedDurationMinutes,
315-
spinnerMessage: getQueueSpinnerMessage(queueItem),
316-
successMessage: getQueueSuccessMessage(queueItem, config),
317-
errorMessage: `Integration failed during ${queueItem.id}`,
318-
additionalFeatureQueue:
319-
queueItem.id === 'env-vars' ? session.additionalFeatureQueue : [],
320-
resumeSessionId: queuedSessionId,
321-
requestRemark: queueItem.id === 'env-vars',
322-
captureOutputText: queueItem.kind === 'bootstrap',
323-
captureSessionId: true,
324-
finalizeMiddleware: queue.length === 0,
325-
},
326-
middleware,
325+
if (workflowSteps.length === 0) {
326+
logToFile(
327+
'[agent-runner] No workflow steps found in SKILL.md frontmatter, aborting',
328+
);
329+
await wizardAbort({
330+
message:
331+
'The installed skill does not contain workflow steps in its metadata.',
332+
error: new WizardError('No workflow steps in SKILL.md frontmatter'),
333+
});
334+
}
335+
336+
logToFile(
337+
`[agent-runner] Seeded queue from SKILL.md: ${workflowSteps
338+
.map((s) => s.stepId)
339+
.join(', ')}`,
327340
);
328341

329-
queuedSessionId = agentResult.sessionId ?? queuedSessionId;
342+
// ── Step 3: Execute workflow steps + env-vars from the queue ──
330343

331-
if (queueItem.kind === 'bootstrap') {
332-
installedSkillId =
333-
extractInstalledSkillId(agentResult.outputText ?? '') ?? undefined;
334-
if (!installedSkillId) {
335-
await wizardAbort({
336-
message:
337-
'The wizard could not determine which integration skill was installed during bootstrap.',
338-
error: new WizardError(
339-
'Bootstrap step did not emit installed skill id',
340-
),
341-
});
342-
}
343-
}
344+
const queue = createPostBootstrapQueue(workflowSteps);
345+
346+
while (queue.length > 0) {
347+
const queueItem = queue.dequeue()!;
348+
const prompt = buildQueuedPrompt(
349+
queueItem,
350+
config,
351+
promptContext,
352+
installedSkillId,
353+
);
354+
355+
agentResult = await runAgent(
356+
agent,
357+
prompt,
358+
sessionToOptions(session),
359+
spinner,
360+
{
361+
estimatedDurationMinutes: config.ui.estimatedDurationMinutes,
362+
spinnerMessage: getQueueSpinnerMessage(queueItem),
363+
successMessage: getQueueSuccessMessage(queueItem, config),
364+
errorMessage: `Integration failed during ${queueItem.id}`,
365+
additionalFeatureQueue:
366+
queueItem.id === 'env-vars' ? session.additionalFeatureQueue : [],
367+
resumeSessionId: queuedSessionId,
368+
requestRemark: queueItem.id === 'env-vars',
369+
captureOutputText: false,
370+
captureSessionId: false,
371+
finalizeMiddleware: queue.length === 0,
372+
},
373+
middleware,
374+
);
344375

345-
if (agentResult.error) {
346-
break;
376+
if (agentResult.error) {
377+
break;
378+
}
347379
}
348380
}
349381

@@ -467,17 +499,9 @@ function buildQueuedPrompt(
467499
host: string;
468500
projectId: number;
469501
},
470-
frameworkContext: Record<string, unknown>,
471-
installedSkillId?: string,
502+
installedSkillId: string,
472503
): string {
473-
if (queueItem.kind === 'bootstrap') {
474-
return buildBootstrapPrompt(config, context, frameworkContext);
475-
}
476-
477504
if (queueItem.kind === 'workflow') {
478-
if (!installedSkillId) {
479-
throw new Error('Workflow step requires installed skill id');
480-
}
481505
return buildWorkflowStepPrompt(
482506
queueItem.referenceFilename,
483507
installedSkillId,

src/lib/workflow-queue.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ export class WizardWorkflowQueue {
4343

4444
/**
4545
* Describes a workflow step that can be seeded into the queue.
46-
* Eventually this comes from a context-mill manifest; for now it's
47-
* passed in by the caller so the queue itself stays generic.
46+
* Parsed from SKILL.md frontmatter's `workflow` array.
4847
*/
4948
export interface WorkflowStepSeed {
5049
/** Unique id for the step, e.g. "1.0-begin" */
@@ -53,6 +52,32 @@ export interface WorkflowStepSeed {
5352
referenceFilename: string;
5453
}
5554

55+
/**
56+
* Parse workflow steps from SKILL.md content.
57+
*
58+
* Extracts `step_id` and `reference` from the YAML frontmatter's
59+
* `workflow` array. Uses simple regex — no YAML library needed
60+
* since we control the output format in skill-generator.
61+
*/
62+
export function parseWorkflowStepsFromSkillMd(
63+
skillMdContent: string,
64+
): WorkflowStepSeed[] {
65+
const fmMatch = skillMdContent.match(/^---\n([\s\S]*?)\n---/);
66+
if (!fmMatch) return [];
67+
const frontmatter = fmMatch[1];
68+
69+
const steps: WorkflowStepSeed[] = [];
70+
const entryRegex = /step_id:\s*(.+)\n\s*reference:\s*(.+)/g;
71+
let match;
72+
while ((match = entryRegex.exec(frontmatter)) !== null) {
73+
steps.push({
74+
stepId: match[1].trim(),
75+
referenceFilename: match[2].trim(),
76+
});
77+
}
78+
return steps;
79+
}
80+
5681
/**
5782
* Build the initial queue from an ordered list of workflow steps.
5883
* The queue is always: bootstrap → workflow steps → env-vars.
@@ -73,3 +98,23 @@ export function createInitialWizardWorkflowQueue(
7398
];
7499
return new WizardWorkflowQueue(items);
75100
}
101+
102+
/**
103+
* Build a queue with only workflow steps + env-vars (no bootstrap).
104+
* Used after bootstrap has already run and SKILL.md has been parsed.
105+
*/
106+
export function createPostBootstrapQueue(
107+
steps: WorkflowStepSeed[],
108+
): WizardWorkflowQueue {
109+
const items: WizardWorkflowQueueItem[] = [
110+
...steps.map(
111+
(step): WizardWorkflowQueueItem => ({
112+
id: `workflow:${step.stepId}`,
113+
kind: 'workflow',
114+
referenceFilename: step.referenceFilename,
115+
}),
116+
),
117+
{ id: 'env-vars', kind: 'env-vars' },
118+
];
119+
return new WizardWorkflowQueue(items);
120+
}

0 commit comments

Comments
 (0)