Files
fusion/demo/simulate.ts
gsxdsm ffce26ab1d FN-5986: add browser demo lifecycle board flow
Add a browser demo workflow and supporting fixtures for board walkthroughs.

- add a demo README plus seeded Browser Demo Lifecycle workflow with Todo, In Progress, In Review, QA, and Publish columns
- seed representative browser demo tasks and lifecycle log entries across the custom workflow columns
- update the live simulator to advance workflow tasks through QA and Publish while preserving the legacy merge path
- add a core lifecycle test covering workflow selection and the Todo → In Progress → In Review → QA → Publish transitions

Files changed:
 demo/README.md                                     |  25 ++++
 demo/seed.ts                                       | 131 ++++++++++++++++++++-
 demo/simulate.ts                                   |  35 ++++--
 packages/core/src/__tests__/browser-demo-lifecycle.test.ts   |  71 +++++++++++
 4 files changed, 248 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-5986

Fusion-Task-Lineage: 9638f7da-9b50-4de1-8ffa-d861dbf7aff2
2026-06-07 20:25:00 -07:00

177 lines
7.3 KiB
TypeScript

/**
* Simulates live board activity — tasks progressing, new ideas landing,
* planning completing, reviews finishing. Run alongside `kb dashboard`.
*
* Usage: `npx tsx demo/simulate.ts [dir]`
*
* Expects a seeded board (run `demo/seed.ts` first).
*/
import { TaskStore } from "../packages/core/src/index.js";
const root = process.argv[2] || process.cwd();
const NEW_TASK_IDEAS = [
"Add two-factor authentication with TOTP",
"Implement webhook delivery for task events with retry and exponential backoff",
"Add a CLI tool for bulk task import from CSV",
"Support custom fields on tasks — text, number, date, dropdown",
"Add a Gantt chart view for project timelines",
"Implement role-based access control with custom roles",
"Add Slack integration for task notifications",
"Support task templates for recurring work",
"Add time tracking per task with weekly summaries",
"Implement API key rotation without downtime",
"Add a public changelog page generated from done tasks",
"Support file versioning on attachments",
];
async function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
function pick<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
async function main() {
const store = new TaskStore(root);
await store.init();
let ideaIndex = 0;
console.log("Simulating board activity... (Ctrl+C to stop)\n");
while (true) {
const tasks = await store.listTasks();
const planning = tasks.filter((t) => t.column === "triage" && !t.paused);
const inProgress = tasks.filter((t) => t.column === "in-progress" && !t.paused);
const inReview = tasks.filter((t) => t.column === "in-review" && !t.paused);
const qa = tasks.filter((t) => t.column === "qa" && !t.paused);
const todo = tasks.filter((t) => t.column === "todo" && !t.paused);
// Roll dice for what happens this tick
const roll = Math.random();
if (roll < 0.2 && ideaIndex < NEW_TASK_IDEAS.length) {
// New task lands in planning
const desc = NEW_TASK_IDEAS[ideaIndex++];
const task = await store.createTask({ description: desc });
console.log(` + New task: ${task.id} — "${desc.slice(0, 50)}..."`);
await sleep(2000 + Math.random() * 3000);
} else if (roll < 0.4 && planning.length > 0) {
// Planning completes — task gets planned and moves to todo
const task = pick(planning);
const title = task.description.slice(0, 60).replace(/\.$/, "");
await store.updateTask(task.id, {
title,
size: pick(["S", "M", "L"] as const),
reviewLevel: pick([0, 1, 1, 2, 2, 3]),
});
await store.logEntry(task.id, "Planning complete — plan written", "approved");
await store.moveTask(task.id, "todo");
console.log(` ✓ Planned: ${task.id} → todo`);
await sleep(3000 + Math.random() * 4000);
} else if (roll < 0.6 && todo.length > 0 && inProgress.length < 3) {
// Scheduler picks up a todo task
const task = pick(todo);
await store.moveTask(task.id, "in-progress");
await store.updateTask(task.id, { status: null });
await store.logEntry(task.id, "Scheduled for execution", "worktree created");
console.log(` ▸ Started: ${task.id} — "${task.title || task.description.slice(0, 40)}"`);
await sleep(2000 + Math.random() * 3000);
} else if (roll < 0.85 && inProgress.length > 0) {
// Step progress on an in-progress task
const task = pick(inProgress);
const detail = await store.getTask(task.id);
// Initialize steps if needed
if (detail.steps.length === 0) {
const steps = [
"Analyze codebase and plan approach",
"Implement core logic",
"Add tests and edge case handling",
"Integration testing and documentation",
];
// Write a prompt with steps so parseStepsFromPrompt works
const stepsSection = steps
.map((s, i) => `### Step ${i + 1}: ${s}\n\n- [ ] Complete`)
.join("\n\n");
await store.updateTask(task.id, {
prompt: `# ${task.id}: ${task.title || "Task"}\n\n## Steps\n\n${stepsSection}\n`,
});
}
// Re-read after potential prompt update
const fresh = await store.getTask(task.id);
if (fresh.steps.length > 0) {
const currentIdx = fresh.currentStep;
if (currentIdx < fresh.steps.length) {
const step = fresh.steps[currentIdx];
if (step.status === "pending") {
await store.updateStep(task.id, currentIdx, "in-progress");
await store.updateTask(task.id, { status: null });
await store.logEntry(task.id, `Step ${currentIdx} started`, "in-progress");
console.log(` ▸ ${task.id} step ${currentIdx}: ${step.name}`);
} else if (step.status === "in-progress") {
// Review and complete step
const verdict = Math.random() < 0.85 ? "approved" : "revise";
if (verdict === "approved") {
await store.updateStep(task.id, currentIdx, "done");
await store.logEntry(task.id, `Review: step ${currentIdx}`, "approved");
console.log(` ✓ ${task.id} step ${currentIdx} approved`);
// Check if all steps done
const updated = await store.getTask(task.id);
if (updated.currentStep >= updated.steps.length) {
await store.moveTask(task.id, "in-review");
await store.updateTask(task.id, { status: null });
await store.logEntry(task.id, "All steps complete — moved to review");
console.log(` ★ ${task.id} → in-review`);
}
} else {
await store.logEntry(task.id, `Review: step ${currentIdx}`, "revise — needs fixes");
await store.updateTask(task.id, { status: null });
console.log(` ↻ ${task.id} step ${currentIdx} needs revision`);
}
}
}
}
await sleep(3000 + Math.random() * 5000);
} else if (roll < 0.9 && inReview.length > 0) {
const task = pick(inReview);
let movedToQa = false;
try {
await store.moveTask(task.id, "qa", { moveSource: "user", allowDirectInReviewMove: true });
await store.logEntry(task.id, "Review complete — moved to QA", "awaiting browser smoke test");
console.log(` ✓ Review complete: ${task.id} → qa`);
movedToQa = true;
} catch {
// Not every workflow defines QA; fall back to the legacy merge path.
}
if (!movedToQa) {
await store.logEntry(task.id, "Auto-merged into main");
await store.updateTask(task.id, { status: undefined, worktree: undefined });
await store.moveTask(task.id, "done");
console.log(` ✓ Merged: ${task.id} → done`);
}
await sleep(4000 + Math.random() * 3000);
} else if (roll < 0.97 && qa.length > 0) {
const task = pick(qa);
await store.moveTask(task.id, "publish", { moveSource: "user" });
await store.logEntry(task.id, "QA verification complete", "ready to publish");
console.log(` ✓ QA passed: ${task.id} → publish`);
await sleep(3000 + Math.random() * 3000);
} else {
// Quiet tick
await sleep(2000 + Math.random() * 2000);
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});