Files
fusion/demo/simulate.ts
Fusion 69aa1a550a feat(FN-2605): merge fusion/fn-2605 (auto-resolved)
- test(FN-2605): complete Step 4 — align tests with planning labels
- docs(FN-2605): complete Step 3 — update demo and script terminology
- docs(FN-2605): complete Step 2 — update docs terminology
- docs(FN-2605): complete Step 1 — update README terminology
2026-04-26 12:34:02 -07:00

164 lines
6.7 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 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.95 && inReview.length > 0) {
// Auto-merge a reviewed task
const task = pick(inReview);
await store.logEntry(task.id, "Auto-merged into main");
// Can't use mergeTask (no real branch), so just move directly
const dir = `${root}/.fusion/tasks/${task.id}`;
// Read, update, write manually to move to done
const detail = await store.getTask(task.id);
await store.updateTask(task.id, { status: undefined, worktree: undefined });
// Use moveTask for the column transition
await store.moveTask(task.id, "done");
console.log(` ✓ Merged: ${task.id} → done`);
await sleep(4000 + Math.random() * 3000);
} else {
// Quiet tick
await sleep(2000 + Math.random() * 2000);
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});