/** * 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(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); });