Skip to content

Commit 4344f09

Browse files
committed
Queue with interface to insert items
1 parent cb21681 commit 4344f09

12 files changed

Lines changed: 389 additions & 46 deletions

File tree

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,93 @@ To make your version of a tool usable with a one-line `npx` command:
198198
your project directory
199199
3. Now you can run it with `npx yourpackagename`
200200

201+
# Workflow queue
202+
203+
The wizard executes agent work through a queue-backed runner. Instead of one monolithic prompt, each workflow step is a separate continued query.
204+
205+
## How it works
206+
207+
1. **Bootstrap** runs first as a standalone query — installs the skill and emits the skill ID.
208+
2. The runner reads `SKILL.md` from the installed skill and parses the `workflow` array from its YAML frontmatter to discover the step list.
209+
3. A `WizardWorkflowQueue` is seeded from those steps plus an `env-vars` step at the end.
210+
4. The runner pops items from the queue and issues one continued query per item, preserving the conversation across steps.
211+
212+
## SKILL.md frontmatter format
213+
214+
The skill generator in `context-mill` writes a `workflow` array into each integration skill's frontmatter:
215+
216+
```yaml
217+
---
218+
name: integration-nextjs-app-router
219+
workflow:
220+
- step_id: 1.0-begin
221+
reference: basic-integration-1.0-begin.md
222+
title: PostHog Setup - Begin
223+
next:
224+
- basic-integration-1.1-edit.md
225+
- step_id: 1.1-edit
226+
reference: basic-integration-1.1-edit.md
227+
title: PostHog Setup - Edit
228+
next:
229+
- basic-integration-1.2-revise.md
230+
# ...
231+
---
232+
```
233+
234+
- `step_id` — unique identifier for the step
235+
- `reference` — filename in the skill's `references/` directory
236+
- `title` — human-readable label shown in the TUI progress list
237+
- `next` — array of next step references (for future parallelization)
238+
239+
## Queue item types
240+
241+
```typescript
242+
type WizardWorkflowQueueItem =
243+
| { id: 'bootstrap'; kind: 'bootstrap'; label: string }
244+
| { id: string; kind: 'workflow'; referenceFilename: string; label: string }
245+
| { id: 'env-vars'; kind: 'env-vars'; label: string };
246+
```
247+
248+
## Enqueueing work dynamically
249+
250+
The queue is exposed to the UI via `store.workQueue`. To add work during a run:
251+
252+
```typescript
253+
// Insert at front of queue (runs next)
254+
store.workQueue.enqueueNext({
255+
id: 'my-task',
256+
kind: 'workflow',
257+
referenceFilename: 'my-reference.md',
258+
label: 'My custom step',
259+
});
260+
261+
// Append to end of queue
262+
store.workQueue.enqueue({
263+
id: 'my-task',
264+
kind: 'workflow',
265+
referenceFilename: 'my-reference.md',
266+
label: 'My custom step',
267+
});
268+
```
269+
270+
The queue is reactive — mutations trigger UI re-renders. Items enqueued while the runner loop is active will be picked up when the current step finishes.
271+
272+
## TUI progress display
273+
274+
The RunScreen shows a stage-grouped progress list:
275+
276+
```
277+
☑ PostHog Setup - Begin
278+
▶ PostHog Setup - Edit
279+
☑ Add PostHog to auth.ts
280+
▶ Add PostHog to checkout.ts
281+
○ PostHog Setup - Revise
282+
○ PostHog Setup - Conclusion
283+
○ Environment variables
284+
```
285+
286+
Stage headers come from queue item labels. Nested tasks come from the agent's `TodoWrite` calls. Tasks reset when the runner advances to a new stage.
287+
201288
# Health checks
202289

203290
`src/lib/health-checks/` checks external status pages and PostHog-owned

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

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,25 @@ import {
77
} from '../workflow-queue';
88

99
const BASIC_INTEGRATION_STEPS: WorkflowStepSeed[] = [
10-
{ stepId: '1.0-begin', referenceFilename: 'basic-integration-1.0-begin.md' },
11-
{ stepId: '1.1-edit', referenceFilename: 'basic-integration-1.1-edit.md' },
10+
{
11+
stepId: '1.0-begin',
12+
referenceFilename: 'basic-integration-1.0-begin.md',
13+
title: 'PostHog Setup - Begin',
14+
},
15+
{
16+
stepId: '1.1-edit',
17+
referenceFilename: 'basic-integration-1.1-edit.md',
18+
title: 'PostHog Setup - Edit',
19+
},
1220
{
1321
stepId: '1.2-revise',
1422
referenceFilename: 'basic-integration-1.2-revise.md',
23+
title: 'PostHog Setup - Revise',
1524
},
1625
{
1726
stepId: '1.3-conclude',
1827
referenceFilename: 'basic-integration-1.3-conclude.md',
28+
title: 'PostHog Setup - Conclusion',
1929
},
2030
];
2131

@@ -24,51 +34,65 @@ describe('WizardWorkflowQueue', () => {
2434
const queue = createInitialWizardWorkflowQueue(BASIC_INTEGRATION_STEPS);
2535

2636
expect(queue.toArray()).toEqual([
27-
{ id: 'bootstrap', kind: 'bootstrap' },
37+
{ id: 'bootstrap', kind: 'bootstrap', label: 'Preparing integration' },
2838
{
2939
id: 'workflow:1.0-begin',
3040
kind: 'workflow',
3141
referenceFilename: 'basic-integration-1.0-begin.md',
42+
label: 'PostHog Setup - Begin',
3243
},
3344
{
3445
id: 'workflow:1.1-edit',
3546
kind: 'workflow',
3647
referenceFilename: 'basic-integration-1.1-edit.md',
48+
label: 'PostHog Setup - Edit',
3749
},
3850
{
3951
id: 'workflow:1.2-revise',
4052
kind: 'workflow',
4153
referenceFilename: 'basic-integration-1.2-revise.md',
54+
label: 'PostHog Setup - Revise',
4255
},
4356
{
4457
id: 'workflow:1.3-conclude',
4558
kind: 'workflow',
4659
referenceFilename: 'basic-integration-1.3-conclude.md',
60+
label: 'PostHog Setup - Conclusion',
4761
},
48-
{ id: 'env-vars', kind: 'env-vars' },
62+
{ id: 'env-vars', kind: 'env-vars', label: 'Environment variables' },
4963
]);
5064
});
5165

5266
it('builds a queue from arbitrary steps, not just basic-integration', () => {
5367
const customSteps: WorkflowStepSeed[] = [
54-
{ stepId: 'setup', referenceFilename: 'feature-flags-setup.md' },
55-
{ stepId: 'verify', referenceFilename: 'feature-flags-verify.md' },
68+
{
69+
stepId: 'setup',
70+
referenceFilename: 'feature-flags-setup.md',
71+
title: 'Setup',
72+
},
73+
{
74+
stepId: 'verify',
75+
referenceFilename: 'feature-flags-verify.md',
76+
title: 'Verify',
77+
},
5678
];
5779
const queue = createInitialWizardWorkflowQueue(customSteps);
5880

5981
expect(queue.toArray()).toEqual([
60-
{ id: 'bootstrap', kind: 'bootstrap' },
82+
{ id: 'bootstrap', kind: 'bootstrap', label: 'Preparing integration' },
6183
{
6284
id: 'workflow:setup',
6385
kind: 'workflow',
6486
referenceFilename: 'feature-flags-setup.md',
87+
label: 'Setup',
6588
},
6689
{
6790
id: 'workflow:verify',
6891
kind: 'workflow',
6992
referenceFilename: 'feature-flags-verify.md',
93+
label: 'Verify',
7094
},
71-
{ id: 'env-vars', kind: 'env-vars' },
95+
{ id: 'env-vars', kind: 'env-vars', label: 'Environment variables' },
7296
]);
7397
});
7498

@@ -80,31 +104,43 @@ describe('WizardWorkflowQueue', () => {
80104
id: 'workflow:1.0-begin',
81105
kind: 'workflow',
82106
referenceFilename: 'basic-integration-1.0-begin.md',
107+
label: 'PostHog Setup - Begin',
83108
});
84109
expect(items[items.length - 1]).toEqual({
85110
id: 'env-vars',
86111
kind: 'env-vars',
112+
label: 'Environment variables',
87113
});
88114
expect(items.find((i) => i.id === 'bootstrap')).toBeUndefined();
89115
});
90116

91117
it('supports enqueue and dequeue operations', () => {
92118
const queue = new WizardWorkflowQueue();
93119

94-
queue.enqueue({ id: 'bootstrap', kind: 'bootstrap' });
120+
queue.enqueue({ id: 'bootstrap', kind: 'bootstrap', label: 'Bootstrap' });
95121
queue.enqueue({
96122
id: 'workflow:1.0-begin',
97123
kind: 'workflow',
98124
referenceFilename: 'basic-integration-1.0-begin.md',
125+
label: 'Begin',
99126
});
100127

101-
expect(queue.peek()).toEqual({ id: 'bootstrap', kind: 'bootstrap' });
102-
expect(queue.dequeue()).toEqual({ id: 'bootstrap', kind: 'bootstrap' });
128+
expect(queue.peek()).toEqual({
129+
id: 'bootstrap',
130+
kind: 'bootstrap',
131+
label: 'Bootstrap',
132+
});
133+
expect(queue.dequeue()).toEqual({
134+
id: 'bootstrap',
135+
kind: 'bootstrap',
136+
label: 'Bootstrap',
137+
});
103138
expect(queue).toHaveLength(1);
104139
expect(queue.dequeue()).toEqual({
105140
id: 'workflow:1.0-begin',
106141
kind: 'workflow',
107142
referenceFilename: 'basic-integration-1.0-begin.md',
143+
label: 'Begin',
108144
});
109145
expect(queue).toHaveLength(0);
110146
});
@@ -149,18 +185,22 @@ workflow:
149185
{
150186
stepId: '1.0-begin',
151187
referenceFilename: 'basic-integration-1.0-begin.md',
188+
title: 'PostHog Setup - Begin',
152189
},
153190
{
154191
stepId: '1.1-edit',
155192
referenceFilename: 'basic-integration-1.1-edit.md',
193+
title: 'PostHog Setup - Edit',
156194
},
157195
{
158196
stepId: '1.2-revise',
159197
referenceFilename: 'basic-integration-1.2-revise.md',
198+
title: 'PostHog Setup - Revise',
160199
},
161200
{
162201
stepId: '1.3-conclude',
163202
referenceFilename: 'basic-integration-1.3-conclude.md',
203+
title: 'PostHog Setup - Conclusion',
164204
},
165205
]);
166206
});

src/lib/agent-runner.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,9 +342,13 @@ export async function runAgentWizard(
342342
// ── Step 3: Execute workflow steps + env-vars from the queue ──
343343

344344
const queue = createPostBootstrapQueue(workflowSteps);
345+
getUI().setWorkQueue(queue);
345346

346347
while (queue.length > 0) {
347348
const queueItem = queue.dequeue()!;
349+
350+
getUI().setCurrentQueueItem({ id: queueItem.id, label: queueItem.label });
351+
348352
const prompt = buildQueuedPrompt(
349353
queueItem,
350354
config,
@@ -373,10 +377,13 @@ export async function runAgentWizard(
373377
middleware,
374378
);
375379

380+
getUI().completeQueueItem({ id: queueItem.id, label: queueItem.label });
381+
376382
if (agentResult.error) {
377383
break;
378384
}
379385
}
386+
getUI().setCurrentQueueItem(null);
380387
}
381388

382389
// Handle error cases detected in agent output

src/lib/stage.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { WizardSession } from './wizard-session';
2+
3+
/**
4+
* A workflow step is the primary unit of the wizard's execution model.
5+
*
6+
* It can own:
7+
* - a screen in the TUI (optional — some steps are headless)
8+
* - agent work via a workflow reference (optional — some steps are UI-only)
9+
* - local state needs (selectors it depends on)
10+
* - completion and visibility predicates
11+
*
12+
* The current PostHog integration flow is one ordered list of steps.
13+
* Future flows (e.g. feature-flag builder) register a different step list.
14+
*/
15+
export interface WorkflowStep {
16+
/** Unique identifier for this step */
17+
id: string;
18+
19+
/**
20+
* TUI screen this step owns, if any.
21+
* Matches the Screen enum values (e.g. 'intro', 'run', 'outro').
22+
*/
23+
screen?: string;
24+
25+
/**
26+
* Whether this step should be visible in the current flow.
27+
* If omitted, the step is always visible.
28+
*/
29+
show?: (session: WizardSession) => boolean;
30+
31+
/**
32+
* Whether this step is complete.
33+
* The flow engine advances past complete steps.
34+
*/
35+
isComplete?: (session: WizardSession) => boolean;
36+
37+
/**
38+
* Workflow reference filename this step executes, if any.
39+
* When set, the runner issues a continued query for this reference.
40+
* e.g. "basic-integration-1.0-begin.md"
41+
*/
42+
workflowReference?: string;
43+
44+
/**
45+
* Whether this step blocks downstream code via a gate promise.
46+
* e.g. "setup" and "health-check" gate bin.ts before runWizard().
47+
*/
48+
gate?: 'setup' | 'health';
49+
50+
/**
51+
* Hook called when the step becomes active.
52+
*/
53+
onEnter?: () => void;
54+
55+
/**
56+
* Hook called when the step completes.
57+
*/
58+
onComplete?: () => void;
59+
}
60+
61+
/**
62+
* An ordered list of workflow steps that defines a wizard flow.
63+
* The first flow is the current PostHog integration.
64+
* Future flows register different step lists.
65+
*/
66+
export type Workflow = WorkflowStep[];

0 commit comments

Comments
 (0)