feat(KB-021): add --dev mode to dashboard command

- Add --dev argument parsing in bin.ts for development mode flag
- Implement dev mode in dashboard command with hot reloading support
- Add comprehensive test coverage for --dev mode functionality
- Create changeset documenting the new dashboard dev mode feature
- Update STANDALONE.md with dev mode documentation
This commit is contained in:
gsxdsm
2026-03-29 18:30:40 -07:00
parent f6d978ccf5
commit af2d577e5f
5 changed files with 144 additions and 8 deletions

View File

@@ -47,6 +47,7 @@ kb — AI-orchestrated task board
Usage:
kb dashboard Start the board web UI
kb dashboard --paused Start with automation paused
kb dashboard --dev Start web UI only (no AI engine)
kb task create [desc] [opts] Create a new task (goes to triage)
kb task list List all tasks
kb task show <id> Show task details, steps, log
@@ -63,6 +64,7 @@ Usage:
Options:
--port, -p <port> Dashboard port (default: 4040)
--paused Start with engine paused (automation disabled)
--dev Start dashboard only (no AI engine)
--attach <file> Attach file(s) on task create (repeatable)
--depends <id> Declare dependency on task create (repeatable)
--limit, -l <n> Max issues to import (default: 30, max: 100)
@@ -96,7 +98,8 @@ async function main() {
const port = pi !== -1 ? parseInt(args[pi + 1], 10) : 4040;
const open = !args.includes("--no-open");
const paused = args.includes("--paused");
await runDashboard(port, { open, paused });
const dev = args.includes("--dev");
await runDashboard(port, { open, paused, dev });
break;
}

View File

@@ -729,7 +729,118 @@ describe("runDashboard — --paused flag", () => {
});
});
// ── Merge conflict retry logic tests ────────────────────────────────────
describe("runDashboard — --dev mode", () => {
let mockStore: ReturnType<typeof makeMockStore>;
let consoleSpy: ReturnType<typeof vi.spyOn>;
beforeEach(async () => {
capturedExecutorOpts = undefined;
vi.clearAllMocks();
mockStore = makeMockStore();
const { TaskStore } = await import("@kb/core");
(TaskStore as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
const engine = await import("@kb/engine");
(engine.aiMergeTask as ReturnType<typeof vi.fn>).mockImplementation(() =>
Promise.resolve({ merged: true }),
);
(engine.TaskExecutor as unknown as ReturnType<typeof vi.fn>).mockImplementation(
(_store: unknown, _cwd: unknown, opts: unknown) => {
capturedExecutorOpts = opts as Record<string, unknown>;
return { resumeOrphaned: vi.fn().mockResolvedValue(undefined) };
},
);
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
});
afterEach(() => {
consoleSpy.mockRestore();
});
it("does NOT start TriageProcessor in dev mode", async () => {
const { TriageProcessor } = await import("@kb/engine");
await runDashboard(0, { open: false, dev: true });
expect(TriageProcessor).not.toHaveBeenCalled();
});
it("does NOT start TaskExecutor in dev mode", async () => {
const { TaskExecutor } = await import("@kb/engine");
await runDashboard(0, { open: false, dev: true });
expect(TaskExecutor).not.toHaveBeenCalled();
});
it("does NOT start Scheduler in dev mode", async () => {
const { Scheduler } = await import("@kb/engine");
await runDashboard(0, { open: false, dev: true });
expect(Scheduler).not.toHaveBeenCalled();
});
it("starts the server correctly in dev mode", async () => {
const { createServer } = await import("@kb/dashboard");
await runDashboard(4040, { open: false, dev: true });
// Wait for async 'listening' event
await new Promise((r) => setTimeout(r, 50));
// Server should have been created and listen called
expect(createServer).toHaveBeenCalled();
expect(mockListen).toHaveBeenCalledWith(4040);
// Banner should show the port
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("http://localhost:4040"),
);
});
it("shows 'AI engine: disabled (dev mode)' in dev mode", async () => {
await runDashboard(0, { open: false, dev: true });
// Wait for async 'listening' event
await new Promise((r) => setTimeout(r, 50));
// Should show disabled message
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("✗ disabled (dev mode)"),
);
});
it("does NOT show triage/scheduler details in dev mode", async () => {
await runDashboard(0, { open: false, dev: true });
// Wait for async 'listening' event
await new Promise((r) => setTimeout(r, 50));
// Should NOT show triage/scheduler details
const triageCall = consoleSpy.mock.calls.find(
(call) => typeof call[0] === "string" && call[0].includes("• triage"),
);
const schedulerCall = consoleSpy.mock.calls.find(
(call) => typeof call[0] === "string" && call[0].includes("• scheduler"),
);
expect(triageCall).toBeUndefined();
expect(schedulerCall).toBeUndefined();
});
it("starts all engine components when dev is false (default)", async () => {
const { TriageProcessor, TaskExecutor, Scheduler } = await import("@kb/engine");
await runDashboard(0, { open: false });
expect(TriageProcessor).toHaveBeenCalled();
expect(TaskExecutor).toHaveBeenCalled();
expect(Scheduler).toHaveBeenCalled();
});
it("shows 'AI engine: ✓ active' when not in dev mode", async () => {
await runDashboard(0, { open: false });
// Wait for async 'listening' event
await new Promise((r) => setTimeout(r, 50));
// Should show active message
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("✓ active"),
);
});
});
describe("runDashboard — merge conflict retry logic", () => {
let mockStore: ReturnType<typeof makeMockStore>;

View File

@@ -13,7 +13,7 @@ function openBrowser(url: string): void {
exec(cmd, () => {});
}
export async function runDashboard(port: number, opts: { open?: boolean; paused?: boolean } = {}) {
export async function runDashboard(port: number, opts: { open?: boolean; paused?: boolean; dev?: boolean } = {}) {
const cwd = process.cwd();
const store = new TaskStore(cwd);
await store.init();
@@ -244,8 +244,8 @@ export async function runDashboard(port: number, opts: { open?: boolean; paused?
// Start the web server with AI merge, auth, and model registry wired in
const app = createServer(store, { onMerge, authStorage, modelRegistry });
// Start the AI engine
{
// Start the AI engine (unless in dev mode)
if (!opts.dev) {
const triage = new TriageProcessor(store, cwd, {
semaphore,
usageLimitPauser,
@@ -381,6 +381,14 @@ export async function runDashboard(port: number, opts: { open?: boolean; paused?
});
}
// Dev mode: simplified SIGINT handler (no engine components)
if (opts.dev) {
process.on("SIGINT", () => {
store.stopWatching();
process.exit(0);
});
}
const server = app.listen(port);
server.on("error", (err: NodeJS.ErrnoException) => {
@@ -406,9 +414,13 @@ export async function runDashboard(port: number, opts: { open?: boolean; paused?
console.log();
console.log(` Tasks stored in .kb/tasks/`);
console.log(` Merge: AI-assisted (conflict resolution + commit messages)`);
console.log(` AI engine: ✓ active`);
console.log(` • triage: auto-specifying tasks`);
console.log(` • scheduler: dependency-aware execution`);
if (opts.dev) {
console.log(` AI engine: ✗ disabled (dev mode)`);
} else {
console.log(` AI engine: ✓ active`);
console.log(` • triage: auto-specifying tasks`);
console.log(` • scheduler: dependency-aware execution`);
}
console.log(` File watcher: ✓ active`);
console.log(` Press Ctrl+C to stop`);
console.log();