diff --git a/README.md b/README.md
index 13a377e6..3437440a 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/lib/__tests__/workflow-queue.test.ts b/src/lib/__tests__/workflow-queue.test.ts
index d2f1d1c1..547af0c7 100644
--- a/src/lib/__tests__/workflow-queue.test.ts
+++ b/src/lib/__tests__/workflow-queue.test.ts
@@ -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',
},
];
@@ -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' },
]);
});
@@ -80,10 +104,12 @@ 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();
});
@@ -91,20 +117,30 @@ describe('WizardWorkflowQueue', () => {
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);
});
@@ -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',
},
]);
});
diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts
index 4f0cb847..c6a4b9bc 100644
--- a/src/lib/agent-runner.ts
+++ b/src/lib/agent-runner.ts
@@ -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,
@@ -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
diff --git a/src/lib/stage.ts b/src/lib/stage.ts
new file mode 100644
index 00000000..69ceda65
--- /dev/null
+++ b/src/lib/stage.ts
@@ -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[];
diff --git a/src/lib/workflow-queue.ts b/src/lib/workflow-queue.ts
index c8ab586e..e3b47e29 100644
--- a/src/lib/workflow-queue.ts
+++ b/src/lib/workflow-queue.ts
@@ -2,30 +2,48 @@ export type WizardWorkflowQueueItem =
| {
id: 'bootstrap';
kind: 'bootstrap';
+ label: string;
}
| {
id: string;
kind: 'workflow';
referenceFilename: string;
+ label: string;
}
| {
id: 'env-vars';
kind: 'env-vars';
+ label: string;
};
export class WizardWorkflowQueue {
private items: WizardWorkflowQueueItem[];
+ private onChange?: () => void;
constructor(items: WizardWorkflowQueueItem[] = []) {
this.items = [...items];
}
+ /** Register a listener that fires on any queue mutation. */
+ setOnChange(fn: () => void): void {
+ this.onChange = fn;
+ }
+
enqueue(item: WizardWorkflowQueueItem): void {
this.items.push(item);
+ this.onChange?.();
+ }
+
+ /** Insert an item at the front of the queue (next to run). */
+ enqueueNext(item: WizardWorkflowQueueItem): void {
+ this.items.unshift(item);
+ this.onChange?.();
}
dequeue(): WizardWorkflowQueueItem | undefined {
- return this.items.shift();
+ const item = this.items.shift();
+ this.onChange?.();
+ return item;
}
peek(): WizardWorkflowQueueItem | undefined {
@@ -50,12 +68,14 @@ export interface WorkflowStepSeed {
stepId: string;
/** Filename inside the skill's references/ dir, e.g. "basic-integration-1.0-begin.md" */
referenceFilename: string;
+ /** Human-readable title from SKILL.md frontmatter, e.g. "PostHog Setup - Edit" */
+ title: string;
}
/**
* Parse workflow steps from SKILL.md content.
*
- * Extracts `step_id` and `reference` from the YAML frontmatter's
+ * Extracts `step_id`, `reference`, and `title` from the YAML frontmatter's
* `workflow` array. Uses simple regex — no YAML library needed
* since we control the output format in skill-generator.
*/
@@ -67,12 +87,13 @@ export function parseWorkflowStepsFromSkillMd(
const frontmatter = fmMatch[1];
const steps: WorkflowStepSeed[] = [];
- const entryRegex = /step_id:\s*(.+)\n\s*reference:\s*(.+)/g;
+ const entryRegex = /step_id:\s*(.+)\n\s*reference:\s*(.+)\n\s*title:\s*(.+)/g;
let match;
while ((match = entryRegex.exec(frontmatter)) !== null) {
steps.push({
stepId: match[1].trim(),
referenceFilename: match[2].trim(),
+ title: match[3].trim(),
});
}
return steps;
@@ -86,15 +107,16 @@ export function createInitialWizardWorkflowQueue(
steps: WorkflowStepSeed[],
): WizardWorkflowQueue {
const items: WizardWorkflowQueueItem[] = [
- { id: 'bootstrap', kind: 'bootstrap' },
+ { id: 'bootstrap', kind: 'bootstrap', label: 'Preparing integration' },
...steps.map(
(step): WizardWorkflowQueueItem => ({
id: `workflow:${step.stepId}`,
kind: 'workflow',
referenceFilename: step.referenceFilename,
+ label: step.title,
}),
),
- { id: 'env-vars', kind: 'env-vars' },
+ { id: 'env-vars', kind: 'env-vars', label: 'Environment variables' },
];
return new WizardWorkflowQueue(items);
}
@@ -112,9 +134,10 @@ export function createPostBootstrapQueue(
id: `workflow:${step.stepId}`,
kind: 'workflow',
referenceFilename: step.referenceFilename,
+ label: step.title,
}),
),
- { id: 'env-vars', kind: 'env-vars' },
+ { id: 'env-vars', kind: 'env-vars', label: 'Environment variables' },
];
return new WizardWorkflowQueue(items);
}
diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts
index b5a816f0..574c66f5 100644
--- a/src/ui/logging-ui.ts
+++ b/src/ui/logging-ui.ts
@@ -6,6 +6,7 @@
import { TaskStatus, type WizardUI, type SpinnerHandle } from './wizard-ui';
import type { SettingsConflict } from '../lib/agent-interface';
+import type { WizardWorkflowQueue } from '../lib/workflow-queue';
export class LoggingUI implements WizardUI {
intro(message: string): void {
@@ -154,4 +155,16 @@ export class LoggingUI implements WizardUI {
setEventPlan(_events: Array<{ name: string; description: string }>): void {
// No-op in CI mode
}
+
+ setWorkQueue(_queue: WizardWorkflowQueue): void {
+ // No-op in CI mode
+ }
+
+ setCurrentQueueItem(_item: { id: string; label: string } | null): void {
+ // No-op in CI mode
+ }
+
+ completeQueueItem(_item: { id: string; label: string }): void {
+ // No-op in CI mode
+ }
}
diff --git a/src/ui/tui/ink-ui.ts b/src/ui/tui/ink-ui.ts
index 305e5b33..33e51b06 100644
--- a/src/ui/tui/ink-ui.ts
+++ b/src/ui/tui/ink-ui.ts
@@ -9,6 +9,7 @@
import type { WizardUI, SpinnerHandle } from '../wizard-ui.js';
import type { WizardStore } from './store.js';
import type { SettingsConflict } from '../../lib/agent-interface.js';
+import type { WizardWorkflowQueue } from '../../lib/workflow-queue.js';
import { RunPhase, OutroKind } from '../../lib/wizard-session.js';
// Strip ANSI escape codes (chalk formatting) from strings
@@ -156,4 +157,16 @@ export class InkUI implements WizardUI {
setEventPlan(events: Array<{ name: string; description: string }>): void {
this.store.setEventPlan(events);
}
+
+ setWorkQueue(queue: WizardWorkflowQueue): void {
+ this.store.workQueue = queue;
+ }
+
+ setCurrentQueueItem(item: { id: string; label: string } | null): void {
+ this.store.setCurrentQueueItem(item);
+ }
+
+ completeQueueItem(item: { id: string; label: string }): void {
+ this.store.completeQueueItem(item);
+ }
}
diff --git a/src/ui/tui/playground/demos/ProgressDemo.tsx b/src/ui/tui/playground/demos/ProgressDemo.tsx
index f6513576..065450e0 100644
--- a/src/ui/tui/playground/demos/ProgressDemo.tsx
+++ b/src/ui/tui/playground/demos/ProgressDemo.tsx
@@ -7,30 +7,35 @@ import { Box, Text } from 'ink';
import { useState, useEffect } from 'react';
import { ProgressList, LoadingBox } from '../../primitives/index.js';
import type { ProgressItem } from '../../primitives/index.js';
+import { TaskStatus } from '../../../wizard-ui.js';
import { Colors } from '../../styles.js';
const INITIAL_ITEMS: ProgressItem[] = [
{
label: 'Detect framework',
activeForm: 'Detecting framework',
- status: 'pending',
+ status: TaskStatus.Pending,
},
{
label: 'Install dependencies',
activeForm: 'Installing dependencies',
- status: 'pending',
+ status: TaskStatus.Pending,
},
{
label: 'Configure PostHog',
activeForm: 'Configuring PostHog',
- status: 'pending',
+ status: TaskStatus.Pending,
},
{
label: 'Add analytics provider',
activeForm: 'Adding analytics provider',
- status: 'pending',
+ status: TaskStatus.Pending,
+ },
+ {
+ label: 'Verify setup',
+ activeForm: 'Verifying setup',
+ status: TaskStatus.Pending,
},
- { label: 'Verify setup', activeForm: 'Verifying setup', status: 'pending' },
];
export const ProgressDemo = () => {
@@ -52,9 +57,9 @@ export const ProgressDemo = () => {
setItems(
INITIAL_ITEMS.map((item, i) => {
- if (i < cycle) return { ...item, status: 'completed' as const };
- if (i === cycle) return { ...item, status: 'in_progress' as const };
- return { ...item, status: 'pending' as const };
+ if (i < cycle) return { ...item, status: TaskStatus.Completed };
+ if (i === cycle) return { ...item, status: TaskStatus.InProgress };
+ return { ...item, status: TaskStatus.Pending };
}),
);
}, [tick]);
diff --git a/src/ui/tui/primitives/ProgressList.tsx b/src/ui/tui/primitives/ProgressList.tsx
index f7d28d46..40355d2c 100644
--- a/src/ui/tui/primitives/ProgressList.tsx
+++ b/src/ui/tui/primitives/ProgressList.tsx
@@ -7,11 +7,14 @@ import { Box, Text } from 'ink';
import { Spinner } from '@inkjs/ui';
import { Colors, Icons } from '../styles.js';
import { LoadingBox } from './LoadingBox.js';
+import { TaskStatus } from '../../wizard-ui.js';
export interface ProgressItem {
label: string;
activeForm?: string;
- status: 'pending' | 'in_progress' | 'completed';
+ status: TaskStatus;
+ /** Nesting depth — 0 = top-level, 1 = nested under a stage, etc. */
+ indent?: number;
}
interface ProgressListProps {
@@ -34,26 +37,29 @@ export const ProgressList = ({ items, title }: ProgressListProps) => {
{items.length === 0 && }
{items.map((item, i) => {
const icon =
- item.status === 'completed'
+ item.status === TaskStatus.Completed
? Icons.squareFilled
- : item.status === 'in_progress'
+ : item.status === TaskStatus.InProgress
? Icons.triangleRight
: Icons.squareOpen;
const color =
- item.status === 'completed'
+ item.status === TaskStatus.Completed
? Colors.success
- : item.status === 'in_progress'
+ : item.status === TaskStatus.InProgress
? Colors.primary
: Colors.muted;
const label =
- item.status === 'in_progress' && item.activeForm
+ item.status === TaskStatus.InProgress && item.activeForm
? item.activeForm
: item.label;
+ const pad = item.indent ? ' '.repeat(item.indent) : '';
+
return (
+ {pad}
{icon}
- {label}
+ {label}
);
})}
diff --git a/src/ui/tui/screens/RunScreen.tsx b/src/ui/tui/screens/RunScreen.tsx
index 7ba6ff5c..b0811dfd 100644
--- a/src/ui/tui/screens/RunScreen.tsx
+++ b/src/ui/tui/screens/RunScreen.tsx
@@ -21,6 +21,7 @@ import {
HNViewer,
} from '../primitives/index.js';
import type { ProgressItem } from '../primitives/index.js';
+import { TaskStatus } from '../../wizard-ui.js';
import { ADDITIONAL_FEATURE_LABELS } from '../../../lib/wizard-session.js';
import { LearnCard } from '../components/LearnCard.js';
import { TipsCard } from '../components/TipsCard.js';
@@ -40,23 +41,53 @@ export const RunScreen = ({ store }: RunScreenProps) => {
const [columns] = useStdoutDimensions();
- const progressItems: ProgressItem[] = store.tasks.map((t) => ({
- label: t.label,
- activeForm: t.activeForm,
- status: t.status,
- }));
+ // Build stage-grouped progress items
+ const progressItems: ProgressItem[] = [];
+ const current = store.currentQueueItem;
+ const completed = store.completedQueueItems;
+ const pendingQueue = store.workQueue?.toArray() ?? [];
- // When all tasks are done but the queue has features, show a transitional item
- const queue = store.session.additionalFeatureQueue;
- const allDone =
- progressItems.length > 0 &&
- progressItems.every((t) => t.status === 'completed');
- if (allDone && queue.length > 0) {
- const nextLabel = ADDITIONAL_FEATURE_LABELS[queue[0]];
+ // Completed stages
+ for (const item of completed) {
+ progressItems.push({
+ label: item.label,
+ status: TaskStatus.Completed,
+ });
+ }
+
+ // Current stage header + nested agent tasks
+ if (current) {
+ progressItems.push({
+ label: current.label,
+ activeForm: current.label,
+ status: TaskStatus.InProgress,
+ });
+ // Nest agent tasks under current stage
+ for (const t of store.tasks) {
+ progressItems.push({
+ label: t.label,
+ activeForm: t.activeForm,
+ status: t.status,
+ indent: 1,
+ });
+ }
+ }
+
+ // Pending queue items
+ for (const item of pendingQueue) {
+ progressItems.push({
+ label: item.label,
+ status: TaskStatus.Pending,
+ });
+ }
+
+ // Additional features waiting
+ const featureQueue = store.session.additionalFeatureQueue;
+ for (const feature of featureQueue) {
+ const nextLabel = ADDITIONAL_FEATURE_LABELS[feature];
progressItems.push({
label: `Set up ${nextLabel}`,
- activeForm: `Setting up ${nextLabel}...`,
- status: 'in_progress',
+ status: TaskStatus.Pending,
});
}
diff --git a/src/ui/tui/store.ts b/src/ui/tui/store.ts
index 26c9fd75..d5d95584 100644
--- a/src/ui/tui/store.ts
+++ b/src/ui/tui/store.ts
@@ -34,6 +34,7 @@ import {
evaluateWizardReadiness,
WizardReadiness,
} from '../../lib/health-checks/readiness.js';
+import type { WizardWorkflowQueue } from '../../lib/workflow-queue.js';
export { TaskStatus, Screen, Overlay, Flow, RunPhase, McpOutcome };
export type { ScreenName, OutroData, WizardSession };
@@ -58,6 +59,8 @@ export class WizardStore {
private $statusExpanded = atom(false);
private $tasks = atom([]);
private $eventPlan = atom([]);
+ private $currentQueueItem = atom<{ id: string; label: string } | null>(null);
+ private $completedQueueItems = atom<{ id: string; label: string }[]>([]);
private $learnCardBlockIdx = atom(0);
private $learnCardComplete = atom(false);
private $version = atom(0);
@@ -70,6 +73,22 @@ export class WizardStore {
version = '';
+ /**
+ * Live reference to the workflow queue.
+ * Set by the agent runner so the UI can dynamically enqueue work.
+ */
+ private _workQueue: WizardWorkflowQueue | null = null;
+
+ get workQueue() {
+ return this._workQueue;
+ }
+
+ set workQueue(queue: WizardWorkflowQueue | null) {
+ this._workQueue = queue;
+ // Re-render when the queue changes (enqueue/dequeue)
+ queue?.setOnChange(() => this.emitChange());
+ }
+
/** Navigation router — resolves active screen from session state. */
readonly router: WizardRouter;
@@ -154,6 +173,30 @@ export class WizardStore {
return this.$eventPlan.get();
}
+ get currentQueueItem(): { id: string; label: string } | null {
+ return this.$currentQueueItem.get();
+ }
+
+ get completedQueueItems(): { id: string; label: string }[] {
+ return this.$completedQueueItems.get();
+ }
+
+ setCurrentQueueItem(item: { id: string; label: string } | null): void {
+ this.$currentQueueItem.set(item);
+ // Clear agent tasks when transitioning to a new stage —
+ // each stage gets a fresh task list from TodoWrite
+ this.$tasks.set([]);
+ this.emitChange();
+ }
+
+ completeQueueItem(item: { id: string; label: string }): void {
+ const completed = this.$completedQueueItems.get();
+ if (!completed.some((c) => c.id === item.id)) {
+ this.$completedQueueItems.set([...completed, item]);
+ }
+ this.emitChange();
+ }
+
get statusExpanded(): boolean {
return this.$statusExpanded.get();
}
@@ -520,6 +563,7 @@ export class WizardStore {
const incomingLabels = new Set(incoming.map((t) => t.label));
+ // Keep completed tasks that aren't being updated by the incoming list
const retained = this.$tasks
.get()
.filter((t) => t.done && !incomingLabels.has(t.label));
diff --git a/src/ui/wizard-ui.ts b/src/ui/wizard-ui.ts
index 09bfb564..8a8beafc 100644
--- a/src/ui/wizard-ui.ts
+++ b/src/ui/wizard-ui.ts
@@ -9,6 +9,7 @@
*/
import type { SettingsConflict } from '../lib/agent-interface';
+import type { WizardWorkflowQueue } from '../lib/workflow-queue';
export enum TaskStatus {
Pending = 'pending',
@@ -96,4 +97,11 @@ export interface WizardUI {
// ── Event plan from .posthog-events.json ────────────────────
setEventPlan(events: Array<{ name: string; description: string }>): void;
+
+ // ── Work queue (for dynamic task enqueue from UI) ──────────
+ setWorkQueue(queue: WizardWorkflowQueue): void;
+
+ // ── Queue stage tracking ──────────────────────────────────
+ setCurrentQueueItem(item: { id: string; label: string } | null): void;
+ completeQueueItem(item: { id: string; label: string }): void;
}