[taskboard] add dispatch control plane

This commit is contained in:
2026-03-06 15:21:19 -08:00
parent 1699f0f2b7
commit be1cf8ca8d
25 changed files with 1594 additions and 292 deletions

View File

@@ -7,6 +7,41 @@ const DB_PATH = process.env.DB_PATH || path.join(process.cwd(), "data", "tasks.d
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
let database: sqlite3.Database | null = null;
let databaseReady: Promise<void> | null = null;
function getColumnNames(db: sqlite3.Database, tableName: string) {
return new Promise<string[]>((resolve, reject) => {
db.all<{ name: string }>(`PRAGMA table_info(${tableName})`, [], (error, rows) => {
if (error) {
reject(error);
return;
}
resolve(rows.map((row) => row.name));
});
});
}
async function ensureColumn(
db: sqlite3.Database,
tableName: string,
columnName: string,
definition: string,
) {
const columnNames = await getColumnNames(db, tableName);
if (columnNames.includes(columnName)) {
return;
}
await new Promise<void>((resolve, reject) => {
db.run(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`, (error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
function getDatabase() {
if (database) {
@@ -41,12 +76,63 @@ function getDatabase() {
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
database?.run(`
CREATE TABLE IF NOT EXISTS task_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
assignee TEXT NOT NULL DEFAULT '',
family TEXT,
host TEXT NOT NULL DEFAULT '',
event_type TEXT NOT NULL,
state TEXT,
summary TEXT NOT NULL,
detail TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
database?.run(`
CREATE INDEX IF NOT EXISTS idx_task_events_task_time
ON task_events(task_id, created_at DESC)
`);
database?.run(`
CREATE INDEX IF NOT EXISTS idx_task_events_assignee_time
ON task_events(assignee, created_at DESC)
`);
});
databaseReady = (async () => {
if (!database) {
return;
}
await ensureColumn(database, "tasks", "family", "TEXT");
await ensureColumn(database, "tasks", "target_host", "TEXT NOT NULL DEFAULT ''");
await ensureColumn(database, "tasks", "target_channel", "TEXT NOT NULL DEFAULT ''");
await ensureColumn(database, "tasks", "dispatch_method", "TEXT NOT NULL DEFAULT 'manual'");
await ensureColumn(database, "tasks", "dispatch_state", "TEXT NOT NULL DEFAULT 'planned'");
await ensureColumn(database, "tasks", "template_key", "TEXT");
await ensureColumn(database, "tasks", "repo_slug", "TEXT");
await ensureColumn(database, "tasks", "base_branch", "TEXT");
await ensureColumn(database, "tasks", "preferred_agent", "TEXT");
await ensureColumn(database, "tasks", "reasoning_effort", "TEXT");
await ensureColumn(database, "tasks", "model_hint", "TEXT");
await ensureColumn(database, "tasks", "last_dispatch_at", "TEXT");
await ensureColumn(database, "tasks", "acknowledged_at", "TEXT");
await ensureColumn(database, "tasks", "last_error", "TEXT");
})();
return database;
}
export function all<T>(sql: string, params: unknown[] = []) {
async function ensureReady() {
getDatabase();
if (databaseReady) {
await databaseReady;
}
}
export async function all<T>(sql: string, params: unknown[] = []) {
await ensureReady();
const db = getDatabase();
return new Promise<T[]>((resolve, reject) => {
db.all(sql, params, (error, rows) => {
@@ -59,7 +145,8 @@ export function all<T>(sql: string, params: unknown[] = []) {
});
}
export function get<T>(sql: string, params: unknown[] = []) {
export async function get<T>(sql: string, params: unknown[] = []) {
await ensureReady();
const db = getDatabase();
return new Promise<T | undefined>((resolve, reject) => {
db.get(sql, params, (error, row) => {
@@ -72,7 +159,8 @@ export function get<T>(sql: string, params: unknown[] = []) {
});
}
export function run(sql: string, params: unknown[] = []) {
export async function run(sql: string, params: unknown[] = []) {
await ensureReady();
const db = getDatabase();
return new Promise<{ lastID: number; changes: number }>((resolve, reject) => {
db.run(sql, params, function onRun(error) {