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
177 lines
7.3 KiB
TypeScript
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);
|
|
});
|