chore: add demo / screenshot

This commit is contained in:
Dustin Byrne
2026-03-28 23:31:05 -04:00
parent dbbd3c20b7
commit 2ad581d829
7 changed files with 562 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
"@dustinbyrne/kb": patch
---
Add dashboard screenshot to pi package gallery listing and README.

View File

@@ -2,6 +2,8 @@
AI-orchestrated task board. Like Trello, but your tasks get specified, executed, and delivered by AI — powered by [pi](https://github.com/badlogic/pi-mono). AI-orchestrated task board. Like Trello, but your tasks get specified, executed, and delivered by AI — powered by [pi](https://github.com/badlogic/pi-mono).
![kb dashboard](demo/screenshot.png)
## Workflow ## Workflow
```mermaid ```mermaid

BIN
demo/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

388
demo/seed.ts Normal file
View File

@@ -0,0 +1,388 @@
/**
* Seeds a .kb board with realistic tasks across all columns.
* Run from a git repo: `npx tsx demo/seed.ts [dir]`
*
* Creates a board that looks like an active project mid-flight:
* - Done: shipped features
* - In Review: finished work waiting for merge
* - In Progress: agents actively executing (with steps partially done)
* - Todo: specified and queued
* - Triage: raw ideas just landing
*/
import { TaskStore } from "../packages/core/src/index.js";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
const root = process.argv[2] || process.cwd();
async function main() {
const store = new TaskStore(root);
await store.init();
// ── Done ──────────────────────────────────────────────────────────
const done = [
{
title: "Project scaffolding and CI setup",
desc: "Set up monorepo structure, TypeScript config, CI pipeline, and initial package layout.",
size: "S" as const,
reviewLevel: 0,
},
{
title: "User authentication with JWT",
desc: "Implement signup, login, and token refresh endpoints with bcrypt password hashing and JWT access/refresh tokens.",
size: "M" as const,
reviewLevel: 1,
},
{
title: "Database schema and migrations",
desc: "Design and implement the core PostgreSQL schema — users, workspaces, projects, tasks. Include migration tooling with up/down support.",
size: "M" as const,
reviewLevel: 1,
},
{
title: "REST API endpoints",
desc: "Implement CRUD endpoints for workspaces, projects, and tasks with input validation and proper error responses.",
size: "L" as const,
reviewLevel: 2,
},
{
title: "Rate limiting middleware",
desc: "Add token-bucket rate limiting per API key with configurable limits and proper 429 responses.",
size: "S" as const,
reviewLevel: 0,
},
{
title: "Add pagination to list endpoints",
desc: "Implement cursor-based pagination for all list endpoints. Return next/prev links in response headers.",
size: "S" as const,
reviewLevel: 1,
},
];
const doneIds: string[] = [];
for (const t of done) {
const task = await store.createTask({ description: t.desc, title: t.title });
doneIds.push(task.id);
await store.updateTask(task.id, { size: t.size, reviewLevel: t.reviewLevel });
// Move through the pipeline: triage → todo → in-progress → in-review → done
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
// Add some steps and mark them done
const steps = generateSteps(t.title);
await writePrompt(store, task.id, t.title, t.desc, steps);
for (let i = 0; i < steps.length; i++) {
await store.updateStep(task.id, i, "done");
}
await store.moveTask(task.id, "in-review");
await store.moveTask(task.id, "done");
await addLogs(store, task.id, "done");
}
// ── In Review ─────────────────────────────────────────────────────
const inReview = [
{
title: "WebSocket real-time notifications",
desc: "Add WebSocket support for pushing live task updates, mentions, and status changes to connected clients.",
size: "M" as const,
reviewLevel: 2,
deps: [doneIds[1]], // depends on auth
},
{
title: "Full-text search with PostgreSQL tsvector",
desc: "Implement search across tasks and comments using PostgreSQL full-text indexing. Support phrase queries and ranking.",
size: "L" as const,
reviewLevel: 2,
deps: [doneIds[2]], // depends on db schema
},
];
const reviewIds: string[] = [];
for (const t of inReview) {
const task = await store.createTask({ description: t.desc, title: t.title, dependencies: t.deps });
reviewIds.push(task.id);
await store.updateTask(task.id, { size: t.size, reviewLevel: t.reviewLevel });
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
const steps = generateSteps(t.title);
await writePrompt(store, task.id, t.title, t.desc, steps, t.deps);
for (let i = 0; i < steps.length; i++) {
await store.updateStep(task.id, i, "done");
}
await store.moveTask(task.id, "in-review");
await addLogs(store, task.id, "in-review");
}
// ── In Progress ───────────────────────────────────────────────────
const inProgress = [
{
title: "Dark mode with system preference detection",
desc: "Add dark mode theme with CSS custom properties. Detect system preference via prefers-color-scheme and allow manual toggle. Persist preference in localStorage.",
size: "M" as const,
reviewLevel: 1,
currentStep: 1,
totalSteps: 4,
},
{
title: "File upload with S3 storage",
desc: "Implement file upload endpoints with presigned S3 URLs. Support drag-and-drop in the UI, progress tracking, and file type validation.",
size: "M" as const,
reviewLevel: 2,
deps: [doneIds[3]], // depends on REST API
currentStep: 0,
totalSteps: 3,
},
{
title: "Export to CSV and PDF",
desc: "Add export functionality for task lists and project reports. CSV for data, PDF for formatted reports with charts.",
size: "S" as const,
reviewLevel: 1,
currentStep: 2,
totalSteps: 4,
},
];
const inProgressIds: string[] = [];
for (const t of inProgress) {
const task = await store.createTask({ description: t.desc, title: t.title, dependencies: t.deps });
inProgressIds.push(task.id);
await store.updateTask(task.id, { size: t.size, reviewLevel: t.reviewLevel });
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
const steps = generateStepsDetailed(t.title, t.totalSteps);
await writePrompt(store, task.id, t.title, t.desc, steps, t.deps);
// Mark steps up to currentStep as done, currentStep as in-progress
for (let i = 0; i < t.currentStep; i++) {
await store.updateStep(task.id, i, "done");
}
await store.updateStep(task.id, t.currentStep, "in-progress");
await addLogs(store, task.id, "in-progress");
}
// ── Todo ──────────────────────────────────────────────────────────
const todo = [
{
title: "OAuth2 social login (Google, GitHub)",
desc: "Add OAuth2 login flow for Google and GitHub. Link social accounts to existing users by email. Support account unlinking.",
size: "M" as const,
reviewLevel: 2,
deps: [doneIds[1]], // depends on auth
},
{
title: "Audit logging for admin actions",
desc: "Log all admin actions (user management, settings changes, permission grants) to an append-only audit table with actor, action, target, and timestamp.",
size: "S" as const,
reviewLevel: 1,
blockedBy: () => inProgressIds[2], // implicit dep — file scope overlap with Export to CSV
},
{
title: "Email notification preferences",
desc: "Add per-user notification preferences — which events trigger emails, digest frequency, quiet hours. Integrate with the WebSocket notification system.",
size: "S" as const,
reviewLevel: 1,
deps: [reviewIds[0]], // depends on WebSocket notifications
},
{
title: "Multi-tenant workspace isolation",
desc: "Implement row-level security for workspace isolation. All queries scoped to the active workspace. Cross-workspace data sharing via explicit grants.",
size: "L" as const,
reviewLevel: 3,
deps: [doneIds[2]], // depends on db schema
},
];
for (const t of todo) {
const task = await store.createTask({ description: t.desc, title: t.title, dependencies: t.deps });
await store.updateTask(task.id, { size: t.size, reviewLevel: t.reviewLevel });
await store.moveTask(task.id, "todo");
const blockedBy = "blockedBy" in t && typeof t.blockedBy === "function" ? t.blockedBy() : undefined;
if (blockedBy) {
await store.updateTask(task.id, { status: "queued", blockedBy });
} else if (t.deps?.length) {
await store.updateTask(task.id, { status: "queued" });
}
const steps = generateSteps(t.title);
await writePrompt(store, task.id, t.title, t.desc, steps, t.deps);
}
// ── Triage ────────────────────────────────────────────────────────
const triage = [
{
desc: "Users are reporting slow page loads on the dashboard when they have more than 200 tasks. Probably need virtual scrolling or pagination in the UI.",
},
{
desc: "Add ability to invite team members via email link with a 72-hour expiry. Should work even if they don't have an account yet.",
},
{
desc: "Support markdown rendering in task descriptions and comments. Need to handle XSS — sanitize on render.",
},
{
desc: "Mobile responsive layout for the main views. At minimum: task list, task detail, and the board view.",
},
{
desc: "Add keyboard shortcuts for power users — j/k navigation, enter to open, x to close, / to search.",
},
];
for (const t of triage) {
await store.createTask({ description: t.desc });
}
const tasks = await store.listTasks();
const byColumn: Record<string, number> = {};
for (const t of tasks) {
byColumn[t.column] = (byColumn[t.column] || 0) + 1;
}
console.log(`\nSeeded ${tasks.length} tasks:`);
console.log(` Triage: ${byColumn["triage"] || 0}`);
console.log(` Todo: ${byColumn["todo"] || 0}`);
console.log(` In Progress: ${byColumn["in-progress"] || 0}`);
console.log(` In Review: ${byColumn["in-review"] || 0}`);
console.log(` Done: ${byColumn["done"] || 0}`);
console.log(`\nRun "kb dashboard" to see the board.`);
}
// ── Helpers ───────────────────────────────────────────────────────────
function generateSteps(title: string): string[] {
return [
"Analyze requirements and plan implementation",
"Implement core logic",
"Add tests",
"Integration testing and cleanup",
];
}
function generateStepsDetailed(title: string, count: number): string[] {
const pools: Record<number, string[]> = {
3: [
"Set up infrastructure and dependencies",
"Implement core functionality",
"Tests, error handling, and cleanup",
],
4: [
"Analyze codebase and plan approach",
"Implement core logic",
"Add tests and edge case handling",
"Integration testing and documentation",
],
};
return pools[count] || pools[4];
}
async function writePrompt(
store: TaskStore,
id: string,
title: string,
desc: string,
steps: string[],
deps?: string[],
) {
const depsSection = deps?.length
? deps.map((d) => `- **Task:** ${d}`).join("\n")
: "- **None**";
const stepsSection = steps
.map(
(s, i) => `### Step ${i + 1}: ${s}\n\n- [ ] Complete implementation\n- [ ] Verify correctness`,
)
.join("\n\n");
const prompt = `# ${id}: ${title}
**Created:** ${new Date().toISOString().split("T")[0]}
**Size:** M
## Mission
${desc}
## Dependencies
${depsSection}
## File Scope
- \`src/\`
## Steps
${stepsSection}
## Acceptance Criteria
- [ ] All steps complete
- [ ] All tests passing
- [ ] No regressions
`;
const dir = join(store.getRootDir(), ".kb", "tasks", id);
await writeFile(join(dir, "PROMPT.md"), prompt);
}
async function addLogs(store: TaskStore, id: string, targetColumn: string) {
const actions: Record<string, string[][]> = {
done: [
["Triage complete — spec written", "approved"],
["Scheduled for execution", "worktree created"],
["Step 0 started", "in-progress"],
["Review: step 0", "approved"],
["Step 1 started", "in-progress"],
["Review: step 1", "approved"],
["Step 2 started", "in-progress"],
["Review: step 2", "approved"],
["Step 3 started", "in-progress"],
["Review: step 3", "approved"],
["All steps complete — moved to review"],
["Auto-merged into main"],
],
"in-review": [
["Triage complete — spec written", "approved"],
["Scheduled for execution", "worktree created"],
["Step 0 started", "in-progress"],
["Review: step 0", "approved"],
["Step 1 started", "in-progress"],
["Review: step 1", "approved"],
["Step 2 started", "in-progress"],
["Review: step 2", "approved — minor nits addressed"],
["Step 3 started", "in-progress"],
["Review: step 3", "approved"],
["All steps complete — moved to review"],
],
"in-progress": [
["Triage complete — spec written", "approved"],
["Scheduled for execution", "worktree created"],
["Step 0 started", "in-progress"],
["Review: step 0", "approved"],
["Step 1 started", "in-progress"],
],
};
const entries = actions[targetColumn] || [];
for (const [action, outcome] of entries) {
await store.logEntry(id, action, outcome);
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

163
demo/simulate.ts Normal file
View File

@@ -0,0 +1,163 @@
/**
* Simulates live board activity — tasks progressing, new ideas landing,
* triage 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");
// eslint-disable-next-line no-constant-condition
while (true) {
const tasks = await store.listTasks();
const triage = 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 triage
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 && triage.length > 0) {
// Triage completes — task gets spec'd and moves to todo
const task = pick(triage);
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, "Triage complete — spec written", "approved");
await store.moveTask(task.id, "todo");
console.log(` ✓ Triaged: ${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}/.kb/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);
});

View File

@@ -2,6 +2,8 @@
An automated Kanban board for [pi](https://github.com/badlogic/pi-mono). You (or an agent) add high level ideas to your task list, and a team of agents execute them using worktrees. An automated Kanban board for [pi](https://github.com/badlogic/pi-mono). You (or an agent) add high level ideas to your task list, and a team of agents execute them using worktrees.
![kb dashboard](https://raw.githubusercontent.com/dustinbyrne/kb/main/demo/screenshot.png)
## Installation ## Installation
```bash ```bash

View File

@@ -11,7 +11,8 @@
"pi": { "pi": {
"extensions": [ "extensions": [
"./dist/extension.js" "./dist/extension.js"
] ],
"image": "https://raw.githubusercontent.com/dustinbyrne/kb/main/demo/screenshot.png"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"