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
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,93 @@ To make your version of a tool usable with a one-line `npx` command:
your project directory
3. Now you can run it with `npx yourpackagename`

# Workflow queue

The wizard executes agent work through a queue-backed runner. Instead of one monolithic prompt, each workflow step is a separate continued query.

## How it works

1. **Bootstrap** runs first as a standalone query — installs the skill and emits the skill ID.
2. The runner reads `SKILL.md` from the installed skill and parses the `workflow` array from its YAML frontmatter to discover the step list.
3. A `WizardWorkflowQueue` is seeded from those steps plus an `env-vars` step at the end.
4. The runner pops items from the queue and issues one continued query per item, preserving the conversation across steps.

## SKILL.md frontmatter format

The skill generator in `context-mill` writes a `workflow` array into each integration skill's frontmatter:

```yaml
---
name: integration-nextjs-app-router
workflow:
- step_id: 1.0-begin
reference: basic-integration-1.0-begin.md
title: PostHog Setup - Begin
next:
- basic-integration-1.1-edit.md
- step_id: 1.1-edit
reference: basic-integration-1.1-edit.md
title: PostHog Setup - Edit
next:
- basic-integration-1.2-revise.md
# ...
---
```

- `step_id` — unique identifier for the step
- `reference` — filename in the skill's `references/` directory
- `title` — human-readable label shown in the TUI progress list
- `next` — array of next step references (for future parallelization)

## Queue item types

```typescript
type WizardWorkflowQueueItem =
| { id: 'bootstrap'; kind: 'bootstrap'; label: string }
| { id: string; kind: 'workflow'; referenceFilename: string; label: string }
| { id: 'env-vars'; kind: 'env-vars'; label: string };
```

## Enqueueing work dynamically

The queue is exposed to the UI via `store.workQueue`. To add work during a run:

```typescript
// Insert at front of queue (runs next)
store.workQueue.enqueueNext({
id: 'my-task',
kind: 'workflow',
referenceFilename: 'my-reference.md',
label: 'My custom step',
});

// Append to end of queue
store.workQueue.enqueue({
id: 'my-task',
kind: 'workflow',
referenceFilename: 'my-reference.md',
label: 'My custom step',
});
```

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.

## TUI progress display

The RunScreen shows a stage-grouped progress list:

```
☑ PostHog Setup - Begin
▶ PostHog Setup - Edit
☑ Add PostHog to auth.ts
▶ Add PostHog to checkout.ts
○ PostHog Setup - Revise
○ PostHog Setup - Conclusion
○ Environment variables
```

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.

# Health checks

`src/lib/health-checks/` checks external status pages and PostHog-owned
Expand Down
62 changes: 51 additions & 11 deletions src/lib/__tests__/workflow-queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,25 @@ import {
} from '../workflow-queue';

const BASIC_INTEGRATION_STEPS: WorkflowStepSeed[] = [
{ stepId: '1.0-begin', referenceFilename: 'basic-integration-1.0-begin.md' },
{ stepId: '1.1-edit', referenceFilename: 'basic-integration-1.1-edit.md' },
{
stepId: '1.0-begin',
referenceFilename: 'basic-integration-1.0-begin.md',
title: 'PostHog Setup - Begin',
},
{
stepId: '1.1-edit',
referenceFilename: 'basic-integration-1.1-edit.md',
title: 'PostHog Setup - Edit',
},
{
stepId: '1.2-revise',
referenceFilename: 'basic-integration-1.2-revise.md',
title: 'PostHog Setup - Revise',
},
{
stepId: '1.3-conclude',
referenceFilename: 'basic-integration-1.3-conclude.md',
title: 'PostHog Setup - Conclusion',
},
];

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

expect(queue.toArray()).toEqual([
{ id: 'bootstrap', kind: 'bootstrap' },
{ id: 'bootstrap', kind: 'bootstrap', label: 'Preparing integration' },
{
id: 'workflow:1.0-begin',
kind: 'workflow',
referenceFilename: 'basic-integration-1.0-begin.md',
label: 'PostHog Setup - Begin',
},
{
id: 'workflow:1.1-edit',
kind: 'workflow',
referenceFilename: 'basic-integration-1.1-edit.md',
label: 'PostHog Setup - Edit',
},
{
id: 'workflow:1.2-revise',
kind: 'workflow',
referenceFilename: 'basic-integration-1.2-revise.md',
label: 'PostHog Setup - Revise',
},
{
id: 'workflow:1.3-conclude',
kind: 'workflow',
referenceFilename: 'basic-integration-1.3-conclude.md',
label: 'PostHog Setup - Conclusion',
},
{ id: 'env-vars', kind: 'env-vars' },
{ id: 'env-vars', kind: 'env-vars', label: 'Environment variables' },
]);
});

it('builds a queue from arbitrary steps, not just basic-integration', () => {
const customSteps: WorkflowStepSeed[] = [
{ stepId: 'setup', referenceFilename: 'feature-flags-setup.md' },
{ stepId: 'verify', referenceFilename: 'feature-flags-verify.md' },
{
stepId: 'setup',
referenceFilename: 'feature-flags-setup.md',
title: 'Setup',
},
{
stepId: 'verify',
referenceFilename: 'feature-flags-verify.md',
title: 'Verify',
},
];
const queue = createInitialWizardWorkflowQueue(customSteps);

expect(queue.toArray()).toEqual([
{ id: 'bootstrap', kind: 'bootstrap' },
{ id: 'bootstrap', kind: 'bootstrap', label: 'Preparing integration' },
{
id: 'workflow:setup',
kind: 'workflow',
referenceFilename: 'feature-flags-setup.md',
label: 'Setup',
},
{
id: 'workflow:verify',
kind: 'workflow',
referenceFilename: 'feature-flags-verify.md',
label: 'Verify',
},
{ id: 'env-vars', kind: 'env-vars' },
{ id: 'env-vars', kind: 'env-vars', label: 'Environment variables' },
]);
});

Expand All @@ -80,31 +104,43 @@ describe('WizardWorkflowQueue', () => {
id: 'workflow:1.0-begin',
kind: 'workflow',
referenceFilename: 'basic-integration-1.0-begin.md',
label: 'PostHog Setup - Begin',
});
expect(items[items.length - 1]).toEqual({
id: 'env-vars',
kind: 'env-vars',
label: 'Environment variables',
});
expect(items.find((i) => i.id === 'bootstrap')).toBeUndefined();
});

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

queue.enqueue({ id: 'bootstrap', kind: 'bootstrap' });
queue.enqueue({ id: 'bootstrap', kind: 'bootstrap', label: 'Bootstrap' });
queue.enqueue({
id: 'workflow:1.0-begin',
kind: 'workflow',
referenceFilename: 'basic-integration-1.0-begin.md',
label: 'Begin',
});

expect(queue.peek()).toEqual({ id: 'bootstrap', kind: 'bootstrap' });
expect(queue.dequeue()).toEqual({ id: 'bootstrap', kind: 'bootstrap' });
expect(queue.peek()).toEqual({
id: 'bootstrap',
kind: 'bootstrap',
label: 'Bootstrap',
});
expect(queue.dequeue()).toEqual({
id: 'bootstrap',
kind: 'bootstrap',
label: 'Bootstrap',
});
expect(queue).toHaveLength(1);
expect(queue.dequeue()).toEqual({
id: 'workflow:1.0-begin',
kind: 'workflow',
referenceFilename: 'basic-integration-1.0-begin.md',
label: 'Begin',
});
expect(queue).toHaveLength(0);
});
Expand Down Expand Up @@ -149,18 +185,22 @@ workflow:
{
stepId: '1.0-begin',
referenceFilename: 'basic-integration-1.0-begin.md',
title: 'PostHog Setup - Begin',
},
{
stepId: '1.1-edit',
referenceFilename: 'basic-integration-1.1-edit.md',
title: 'PostHog Setup - Edit',
},
{
stepId: '1.2-revise',
referenceFilename: 'basic-integration-1.2-revise.md',
title: 'PostHog Setup - Revise',
},
{
stepId: '1.3-conclude',
referenceFilename: 'basic-integration-1.3-conclude.md',
title: 'PostHog Setup - Conclusion',
},
]);
});
Expand Down
7 changes: 7 additions & 0 deletions src/lib/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,13 @@ export async function runAgentWizard(
// ── Step 3: Execute workflow steps + env-vars from the queue ──

const queue = createPostBootstrapQueue(workflowSteps);
getUI().setWorkQueue(queue);

while (queue.length > 0) {
const queueItem = queue.dequeue()!;

getUI().setCurrentQueueItem({ id: queueItem.id, label: queueItem.label });

const prompt = buildQueuedPrompt(
queueItem,
config,
Expand Down Expand Up @@ -373,10 +377,13 @@ export async function runAgentWizard(
middleware,
);

getUI().completeQueueItem({ id: queueItem.id, label: queueItem.label });

if (agentResult.error) {
break;
}
}
getUI().setCurrentQueueItem(null);
}

// Handle error cases detected in agent output
Expand Down
66 changes: 66 additions & 0 deletions src/lib/stage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { WizardSession } from './wizard-session';

/**
* A workflow step is the primary unit of the wizard's execution model.
*
* It can own:
* - a screen in the TUI (optional — some steps are headless)
* - agent work via a workflow reference (optional — some steps are UI-only)
* - local state needs (selectors it depends on)
* - completion and visibility predicates
*
* The current PostHog integration flow is one ordered list of steps.
* Future flows (e.g. feature-flag builder) register a different step list.
*/
export interface WorkflowStep {
/** Unique identifier for this step */
id: string;

/**
* TUI screen this step owns, if any.
* Matches the Screen enum values (e.g. 'intro', 'run', 'outro').
*/
screen?: string;

/**
* Whether this step should be visible in the current flow.
* If omitted, the step is always visible.
*/
show?: (session: WizardSession) => boolean;

/**
* Whether this step is complete.
* The flow engine advances past complete steps.
*/
isComplete?: (session: WizardSession) => boolean;

/**
* Workflow reference filename this step executes, if any.
* When set, the runner issues a continued query for this reference.
* e.g. "basic-integration-1.0-begin.md"
*/
workflowReference?: string;

/**
* Whether this step blocks downstream code via a gate promise.
* e.g. "setup" and "health-check" gate bin.ts before runWizard().
*/
gate?: 'setup' | 'health';

/**
* Hook called when the step becomes active.
*/
onEnter?: () => void;

/**
* Hook called when the step completes.
*/
onComplete?: () => void;
}

/**
* An ordered list of workflow steps that defines a wizard flow.
* The first flow is the current PostHog integration.
* Future flows register different step lists.
*/
export type Workflow = WorkflowStep[];
Loading
Loading