feat(KB-045): add scheduled tasks automation system
- Add AutomationStore and core automation types for cron-based scheduling - Implement CronRunner engine for executing scheduled automations - Add REST API routes for CRUD operations on automations - Create UI components: ScheduleCard, ScheduleForm, and ScheduledTasksModal - Integrate scheduled tasks into dashboard App.tsx and CLI dashboard command - Add comprehensive tests for store, runner, API, and UI components - Include changeset for the new scheduled tasks feature
This commit is contained in:
@@ -28,6 +28,11 @@ function makeMockStore() {
|
||||
|
||||
vi.mock("@kb/core", () => ({
|
||||
TaskStore: vi.fn().mockImplementation(() => makeMockStore()),
|
||||
AutomationStore: vi.fn().mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
listSchedules: vi.fn().mockResolvedValue([]),
|
||||
getDueSchedules: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}));
|
||||
|
||||
// ── Mock @kb/dashboard ─────────────────────────────────────────────
|
||||
@@ -84,6 +89,10 @@ vi.mock("@kb/engine", async (importOriginal) => {
|
||||
stop: vi.fn(),
|
||||
})),
|
||||
aiMergeTask: vi.fn().mockResolvedValue({ merged: true }),
|
||||
CronRunner: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
})),
|
||||
scanIdleWorktrees: vi.fn().mockResolvedValue([]),
|
||||
cleanupOrphanedWorktrees: vi.fn().mockResolvedValue(0),
|
||||
};
|
||||
|
||||
@@ -37,6 +37,16 @@ function makeMockStore() {
|
||||
|
||||
vi.mock("@kb/core", () => ({
|
||||
TaskStore: vi.fn().mockImplementation(() => makeMockStore()),
|
||||
AutomationStore: vi.fn().mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
listSchedules: vi.fn().mockResolvedValue([]),
|
||||
getSchedule: vi.fn().mockResolvedValue(null),
|
||||
createSchedule: vi.fn().mockResolvedValue({}),
|
||||
updateSchedule: vi.fn().mockResolvedValue({}),
|
||||
deleteSchedule: vi.fn().mockResolvedValue({}),
|
||||
recordRun: vi.fn().mockResolvedValue({}),
|
||||
getDueSchedules: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}));
|
||||
|
||||
// ── Hoisted shared mocks ───────────────────────────────────────────
|
||||
@@ -145,6 +155,10 @@ vi.mock("@kb/engine", async (importOriginal) => {
|
||||
handleNewComments: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
aiMergeTask: vi.fn().mockImplementation(() => Promise.resolve({ merged: true })),
|
||||
CronRunner: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
})),
|
||||
scanIdleWorktrees: vi.fn().mockResolvedValue([]),
|
||||
cleanupOrphanedWorktrees: vi.fn().mockResolvedValue(0),
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { createInterface } from "node:readline";
|
||||
import { TaskStore } from "@kb/core";
|
||||
import { TaskStore, AutomationStore } from "@kb/core";
|
||||
import type { Settings, TaskDetail, PrInfo } from "@kb/core";
|
||||
import { createServer, GitHubClient } from "@kb/dashboard";
|
||||
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask, UsageLimitPauser, PRIORITY_MERGE, scanIdleWorktrees, cleanupOrphanedWorktrees, NtfyNotifier, PrMonitor, PrCommentHandler } from "@kb/engine";
|
||||
import { TriageProcessor, TaskExecutor, Scheduler, AgentSemaphore, WorktreePool, aiMergeTask, UsageLimitPauser, PRIORITY_MERGE, scanIdleWorktrees, cleanupOrphanedWorktrees, NtfyNotifier, PrMonitor, PrCommentHandler, CronRunner } from "@kb/engine";
|
||||
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
/**
|
||||
@@ -203,6 +203,10 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
await store.init();
|
||||
await store.watch();
|
||||
|
||||
// ── AutomationStore: scheduled task persistence ──────────────────────
|
||||
const automationStore = new AutomationStore(cwd);
|
||||
await automationStore.init();
|
||||
|
||||
// ── NtfyNotifier: push notifications for task completion and failures ─
|
||||
const notifier = new NtfyNotifier(store);
|
||||
notifier.start();
|
||||
@@ -450,7 +454,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
// Start the web server with AI merge, auth, and model registry wired in
|
||||
const app = createServer(store, { onMerge, authStorage, modelRegistry });
|
||||
const app = createServer(store, { onMerge, authStorage, modelRegistry, automationStore });
|
||||
|
||||
// Start the AI engine (unless in dev mode)
|
||||
if (!opts.dev) {
|
||||
@@ -485,6 +489,10 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
onBlocked: (t, deps) => console.log(`[engine] ${t.id} blocked by ${deps.join(", ")}`),
|
||||
});
|
||||
|
||||
// ── CronRunner: scheduled task execution ──────────────────────────
|
||||
const cronRunner = new CronRunner(store, automationStore);
|
||||
cronRunner.start();
|
||||
|
||||
triage.start();
|
||||
scheduler.start();
|
||||
|
||||
@@ -589,6 +597,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
process.on("SIGINT", () => {
|
||||
triage.stop();
|
||||
scheduler.stop();
|
||||
cronRunner.stop();
|
||||
notifier.stop();
|
||||
if (mergeRetryTimer) clearTimeout(mergeRetryTimer);
|
||||
store.stopWatching();
|
||||
@@ -636,6 +645,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
console.log(` AI engine: ✓ active`);
|
||||
console.log(` • triage: auto-specifying tasks`);
|
||||
console.log(` • scheduler: dependency-aware execution`);
|
||||
console.log(` • cron: scheduled task execution`);
|
||||
}
|
||||
console.log(` File watcher: ✓ active`);
|
||||
console.log(` Press Ctrl+C to stop`);
|
||||
|
||||
Reference in New Issue
Block a user