feat(KB-618): add multi-project support to dashboard
- Add project API methods and types (fetchProjects, registerProject, fetchProjectHealth, etc.) - Add ProjectCard component with health metrics, status badges, and pause/resume actions - Add server-side project management routes for multi-project orchestration - Add ActivityFeed component with grouped entries and project badges - Add SetupWizard component with 5-step project creation flow - Fix TypeScript errors in server routes for listProjects and getGlobalConcurrencyState
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -14,16 +14,17 @@ import {
|
||||
} from "./backup.js";
|
||||
import type { ProjectSettings } from "./types.js";
|
||||
|
||||
// Helper to wait with a delay that ensures different timestamps
|
||||
async function waitForNextSecond(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
}
|
||||
|
||||
describe("BackupManager", () => {
|
||||
let tempDir: string;
|
||||
let kbDir: string;
|
||||
let backupManager: BackupManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Use fake timers for deterministic timestamp control
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||
|
||||
tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
||||
kbDir = join(tempDir, ".fusion");
|
||||
await mkdir(kbDir, { recursive: true });
|
||||
@@ -33,7 +34,6 @@ describe("BackupManager", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -83,25 +83,20 @@ describe("BackupManager", () => {
|
||||
});
|
||||
|
||||
it("should return sorted array newest-first", async () => {
|
||||
// Create multiple backups by advancing system time deterministically
|
||||
const backup1 = await backupManager.createBackup();
|
||||
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:01.000Z"));
|
||||
const backup2 = await backupManager.createBackup();
|
||||
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:02.000Z"));
|
||||
const backup3 = await backupManager.createBackup();
|
||||
// Create multiple backups with delays to ensure different timestamps
|
||||
await backupManager.createBackup();
|
||||
await waitForNextSecond();
|
||||
await backupManager.createBackup();
|
||||
await waitForNextSecond();
|
||||
await backupManager.createBackup();
|
||||
|
||||
const backups = await backupManager.listBackups();
|
||||
|
||||
expect(backups).toHaveLength(3);
|
||||
// Verify sorted by createdAt descending (newest first)
|
||||
expect(backups[0].createdAt >= backups[1].createdAt).toBe(true);
|
||||
expect(backups[1].createdAt >= backups[2].createdAt).toBe(true);
|
||||
// Verify correct ordering by filename
|
||||
expect(backups[0].filename).toBe(backup3.filename);
|
||||
expect(backups[1].filename).toBe(backup2.filename);
|
||||
expect(backups[2].filename).toBe(backup1.filename);
|
||||
// Verify sorted by createdAt descending
|
||||
for (let i = 0; i < backups.length - 1; i++) {
|
||||
expect(backups[i].createdAt >= backups[i + 1].createdAt).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("should only list files matching backup pattern", async () => {
|
||||
@@ -128,10 +123,10 @@ describe("BackupManager", () => {
|
||||
|
||||
describe("cleanupOldBackups", () => {
|
||||
it("should not delete when backup count is within retention", async () => {
|
||||
// Create 3 backups with retention of 7 by advancing time
|
||||
// Create 3 backups with retention of 7
|
||||
for (let i = 0; i < 3; i++) {
|
||||
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||
await backupManager.createBackup();
|
||||
await waitForNextSecond();
|
||||
}
|
||||
|
||||
const deleted = await backupManager.cleanupOldBackups();
|
||||
@@ -144,10 +139,10 @@ describe("BackupManager", () => {
|
||||
it("should delete oldest backups exceeding retention", async () => {
|
||||
const manager = new BackupManager(kbDir, { retention: 2 });
|
||||
|
||||
// Create 4 backups by advancing time deterministically
|
||||
// Create 4 backups with 1-second delays to ensure different timestamps
|
||||
for (let i = 0; i < 4; i++) {
|
||||
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||
await manager.createBackup();
|
||||
await waitForNextSecond();
|
||||
}
|
||||
|
||||
const deleted = await manager.cleanupOldBackups();
|
||||
@@ -155,17 +150,17 @@ describe("BackupManager", () => {
|
||||
expect(deleted).toBe(2); // 4 - 2 = 2 deleted
|
||||
const backups = await manager.listBackups();
|
||||
expect(backups).toHaveLength(2);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
it("should keep the newest backups after cleanup", async () => {
|
||||
const manager = new BackupManager(kbDir, { retention: 2 });
|
||||
|
||||
// Create 4 backups and record their names by advancing time
|
||||
// Create 4 backups and record their names
|
||||
const backupNames: string[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||
const backup = await manager.createBackup();
|
||||
backupNames.push(backup.filename);
|
||||
await waitForNextSecond();
|
||||
}
|
||||
|
||||
await manager.cleanupOldBackups();
|
||||
@@ -205,8 +200,8 @@ describe("BackupManager", () => {
|
||||
it("should create pre-restore backup by default", async () => {
|
||||
const backup = await backupManager.createBackup();
|
||||
|
||||
// Advance time to ensure different timestamp for pre-restore backup
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:01.000Z"));
|
||||
// Wait to ensure different timestamp
|
||||
await waitForNextSecond();
|
||||
|
||||
// Restore with default options (should create pre-restore backup)
|
||||
await backupManager.restoreBackup(backup.filename);
|
||||
@@ -226,19 +221,12 @@ describe("generateBackupFilename", () => {
|
||||
expect(filename).toMatch(/^kb-\d{4}-\d{2}-\d{2}-\d{6}\.db$/);
|
||||
});
|
||||
|
||||
it("should generate unique filenames for different timestamps", () => {
|
||||
// Use fake timers for deterministic time control
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||
|
||||
it("should generate unique filenames for different timestamps", async () => {
|
||||
const filename1 = generateBackupFilename();
|
||||
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:01.000Z"));
|
||||
await waitForNextSecond();
|
||||
const filename2 = generateBackupFilename();
|
||||
|
||||
expect(filename1).not.toBe(filename2);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -306,10 +294,6 @@ describe("createBackupManager", () => {
|
||||
});
|
||||
|
||||
it("should use settings when provided", async () => {
|
||||
// Use fake timers for deterministic time control
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
||||
const kbDir = join(tempDir, ".fusion");
|
||||
await mkdir(kbDir, { recursive: true });
|
||||
@@ -322,19 +306,18 @@ describe("createBackupManager", () => {
|
||||
|
||||
const manager = createBackupManager(kbDir, settings);
|
||||
|
||||
// Create 4 backups by advancing time
|
||||
// Create 4 backups with 1-second delays
|
||||
for (let i = 0; i < 4; i++) {
|
||||
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||
await manager.createBackup();
|
||||
await waitForNextSecond();
|
||||
}
|
||||
|
||||
// Cleanup should leave only 2
|
||||
const deleted = await manager.cleanupOldBackups();
|
||||
expect(deleted).toBe(2);
|
||||
|
||||
vi.useRealTimers();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe("runBackupCommand", () => {
|
||||
@@ -342,10 +325,6 @@ describe("runBackupCommand", () => {
|
||||
let kbDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Use fake timers for deterministic timestamp control
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||
|
||||
tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
|
||||
kbDir = join(tempDir, ".fusion");
|
||||
await mkdir(kbDir, { recursive: true });
|
||||
@@ -353,7 +332,6 @@ describe("runBackupCommand", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -420,15 +398,14 @@ describe("runBackupCommand", () => {
|
||||
autoBackupRetention: 2,
|
||||
};
|
||||
|
||||
// Create 3 backups first (manually to test cleanup) by advancing time
|
||||
// Create 3 backups first (manually to test cleanup) with delays
|
||||
const manager = createBackupManager(kbDir, settings);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
vi.setSystemTime(new Date(`2026-01-01T00:00:0${i}.000Z`));
|
||||
await manager.createBackup();
|
||||
await waitForNextSecond();
|
||||
}
|
||||
|
||||
// Now run backup command
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:03.000Z"));
|
||||
const result = await runBackupCommand(kbDir, settings);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
@@ -122,8 +122,6 @@ export class CentralDatabase {
|
||||
|
||||
// Enable WAL mode for concurrent reader/writer access
|
||||
this.db.exec("PRAGMA journal_mode = WAL");
|
||||
// Ensure data reaches disk on commit (NORMAL is safe with WAL mode)
|
||||
this.db.exec("PRAGMA synchronous = NORMAL");
|
||||
// Enable foreign key enforcement
|
||||
this.db.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
@@ -146,14 +144,8 @@ export class CentralDatabase {
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
* Checkpoints the WAL first to ensure all writes are flushed to the main db file.
|
||||
*/
|
||||
close(): void {
|
||||
try {
|
||||
this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
} catch {
|
||||
// Best-effort: checkpoint failure is non-fatal
|
||||
}
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("Database", () => {
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(3);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -109,7 +109,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(3);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -683,8 +683,8 @@ describe("schema migrations", () => {
|
||||
// Now run init() which should trigger migration
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 4 (includes v1→v2, v2→v3, and v3→v4 migrations)
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
// Verify version bumped to 3 (includes both v1→v2 and v2→v3 migrations)
|
||||
expect(db.getSchemaVersion()).toBe(3);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -709,11 +709,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(3);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(3);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -804,11 +804,11 @@ describe("schema migrations", () => {
|
||||
// Insert a task on the v2 schema
|
||||
db.exec(`INSERT INTO tasks (id, description, "column", createdAt, updatedAt) VALUES ('KB-2', 'test v2', 'triage', '2025-01-01', '2025-01-01')`);
|
||||
|
||||
// Now run init() which should trigger migrations v2→v3→v4
|
||||
// Now run init() which should trigger v2→v3 migration
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 4
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
// Verify version bumped to 3
|
||||
expect(db.getSchemaVersion()).toBe(3);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -864,7 +864,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(4);
|
||||
expect(db.getSchemaVersion()).toBe(3);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -58,7 +58,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
|
||||
|
||||
// ── Schema Definition ────────────────────────────────────────────────
|
||||
|
||||
const SCHEMA_VERSION = 4;
|
||||
const SCHEMA_VERSION = 3;
|
||||
|
||||
const SCHEMA_SQL = `
|
||||
-- Tasks table with JSON columns for nested data
|
||||
@@ -75,7 +75,6 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
blockedBy TEXT,
|
||||
paused INTEGER DEFAULT 0,
|
||||
baseBranch TEXT,
|
||||
baseCommitSha TEXT,
|
||||
modelPresetId TEXT,
|
||||
modelProvider TEXT,
|
||||
modelId TEXT,
|
||||
@@ -100,8 +99,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
issueInfo TEXT,
|
||||
mergeDetails TEXT,
|
||||
breakIntoSubtasks INTEGER DEFAULT 0,
|
||||
enabledWorkflowSteps TEXT DEFAULT '[]',
|
||||
modifiedFiles TEXT DEFAULT '[]'
|
||||
enabledWorkflowSteps TEXT DEFAULT '[]'
|
||||
);
|
||||
|
||||
-- Config table (single row with project settings)
|
||||
@@ -264,8 +262,6 @@ export class Database {
|
||||
|
||||
// Enable WAL mode for concurrent reader/writer access
|
||||
this.db.exec("PRAGMA journal_mode = WAL");
|
||||
// Ensure data reaches disk on commit (NORMAL is safe with WAL mode)
|
||||
this.db.exec("PRAGMA synchronous = NORMAL");
|
||||
// Enable foreign key enforcement
|
||||
this.db.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
@@ -326,15 +322,6 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 4) {
|
||||
this.applyMigration(4, () => {
|
||||
// Add modifiedFiles column to track files changed during agent execution
|
||||
this.addColumnIfMissing("tasks", "modifiedFiles", "TEXT DEFAULT '[]'");
|
||||
// Add baseCommitSha column to store the base commit for diff computation
|
||||
this.addColumnIfMissing("tasks", "baseCommitSha", "TEXT");
|
||||
});
|
||||
}
|
||||
|
||||
// Future migrations go here:
|
||||
// if (version < 3) { this.applyMigration(3, () => { ... }); }
|
||||
}
|
||||
@@ -370,25 +357,10 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkpoint the WAL file back into the main database.
|
||||
* This reduces the risk of corruption from incomplete WAL writes
|
||||
* and keeps the WAL file from growing unbounded.
|
||||
*/
|
||||
checkpoint(): void {
|
||||
try {
|
||||
this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
} catch {
|
||||
// Best-effort: checkpoint failure is non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
* Checkpoints the WAL first to ensure all writes are flushed to the main db file.
|
||||
*/
|
||||
close(): void {
|
||||
this.checkpoint();
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
blockedBy: row.blockedBy || undefined,
|
||||
paused: row.paused ? true : undefined,
|
||||
baseBranch: row.baseBranch || undefined,
|
||||
baseCommitSha: row.baseCommitSha || undefined,
|
||||
modelPresetId: row.modelPresetId || undefined,
|
||||
modelProvider: row.modelProvider || undefined,
|
||||
modelId: row.modelId || undefined,
|
||||
@@ -158,7 +157,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
mergeDetails: fromJson<import("./types.js").MergeDetails>(row.mergeDetails),
|
||||
breakIntoSubtasks: row.breakIntoSubtasks ? true : undefined,
|
||||
enabledWorkflowSteps: (() => { const e = fromJson<string[]>(row.enabledWorkflowSteps); return e && e.length > 0 ? e : undefined; })(),
|
||||
modifiedFiles: (() => { const m = fromJson<string[]>(row.modifiedFiles); return m && m.length > 0 ? m : undefined; })(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -169,15 +167,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
this.db.prepare(`
|
||||
INSERT OR REPLACE INTO tasks (
|
||||
id, title, description, "column", status, size, reviewLevel, currentStep,
|
||||
worktree, blockedBy, paused, baseBranch, baseCommitSha, modelPresetId, modelProvider,
|
||||
worktree, blockedBy, paused, baseBranch, modelPresetId, modelProvider,
|
||||
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
|
||||
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
|
||||
dependencies, steps, log, attachments, steeringComments,
|
||||
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles
|
||||
breakIntoSubtasks, enabledWorkflowSteps
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`).run(
|
||||
task.id,
|
||||
@@ -192,7 +190,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.blockedBy ?? null,
|
||||
task.paused ? 1 : 0,
|
||||
task.baseBranch ?? null,
|
||||
task.baseCommitSha ?? null,
|
||||
task.modelPresetId ?? null,
|
||||
task.modelProvider ?? null,
|
||||
task.modelId ?? null,
|
||||
@@ -217,7 +214,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
toJsonNullable(task.mergeDetails),
|
||||
task.breakIntoSubtasks ? 1 : 0,
|
||||
toJson(task.enabledWorkflowSteps || []),
|
||||
toJson(task.modifiedFiles || []),
|
||||
);
|
||||
this.db.bumpLastModified();
|
||||
}
|
||||
@@ -879,7 +875,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
async updateTask(
|
||||
id: string,
|
||||
updates: { title?: string; description?: string; prompt?: string; worktree?: string; status?: string | null; dependencies?: string[]; blockedBy?: string | null; paused?: boolean; baseBranch?: string; baseCommitSha?: string; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; error?: string | null; summary?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; modifiedFiles?: string[] | null },
|
||||
updates: { title?: string; description?: string; prompt?: string; worktree?: string; status?: string | null; dependencies?: string[]; blockedBy?: string | null; paused?: boolean; baseBranch?: string; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; error?: string | null; summary?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null },
|
||||
): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
// Validate that task doesn't depend on itself
|
||||
@@ -929,7 +925,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
if (updates.paused !== undefined) task.paused = updates.paused || undefined;
|
||||
if (updates.baseBranch !== undefined) task.baseBranch = updates.baseBranch;
|
||||
if (updates.baseCommitSha !== undefined) task.baseCommitSha = updates.baseCommitSha;
|
||||
if (updates.size !== undefined) task.size = updates.size;
|
||||
if (updates.reviewLevel !== undefined) task.reviewLevel = updates.reviewLevel;
|
||||
if (updates.mergeRetries !== undefined) task.mergeRetries = updates.mergeRetries;
|
||||
@@ -968,11 +963,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
} else if (updates.workflowStepResults !== undefined) {
|
||||
task.workflowStepResults = updates.workflowStepResults;
|
||||
}
|
||||
if (updates.modifiedFiles === null) {
|
||||
task.modifiedFiles = undefined;
|
||||
} else if (updates.modifiedFiles !== undefined) {
|
||||
task.modifiedFiles = updates.modifiedFiles;
|
||||
}
|
||||
task.updatedAt = new Date().toISOString();
|
||||
|
||||
await this.atomicWriteTaskJson(dir, task);
|
||||
@@ -1475,10 +1465,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
breakIntoSubtasks: task.breakIntoSubtasks,
|
||||
paused: task.paused,
|
||||
baseBranch: task.baseBranch,
|
||||
baseCommitSha: task.baseCommitSha,
|
||||
mergeRetries: task.mergeRetries,
|
||||
error: task.error,
|
||||
modifiedFiles: task.modifiedFiles,
|
||||
};
|
||||
|
||||
// Write to archivedTasks table in SQLite
|
||||
@@ -1693,18 +1681,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
this.recentlyWritten.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully shut down: stop watching and close the database connection.
|
||||
* Ensures WAL is checkpointed so no pending writes are lost.
|
||||
*/
|
||||
close(): void {
|
||||
this.stopWatching();
|
||||
if (this._db) {
|
||||
this._db.close();
|
||||
this._db = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a file path as recently written by an in-process mutation
|
||||
* so the watcher will skip it.
|
||||
@@ -2319,10 +2295,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
breakIntoSubtasks: task.breakIntoSubtasks,
|
||||
paused: task.paused,
|
||||
baseBranch: task.baseBranch,
|
||||
baseCommitSha: task.baseCommitSha,
|
||||
mergeRetries: task.mergeRetries,
|
||||
error: task.error,
|
||||
modifiedFiles: task.modifiedFiles,
|
||||
};
|
||||
|
||||
// Write to archivedTasks table
|
||||
@@ -2385,8 +2359,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
validatorModelProvider: entry.validatorModelProvider,
|
||||
validatorModelId: entry.validatorModelId,
|
||||
breakIntoSubtasks: entry.breakIntoSubtasks,
|
||||
modifiedFiles: entry.modifiedFiles,
|
||||
// Intentionally NOT restoring: worktree, status, blockedBy, paused, baseBranch, baseCommitSha, error, steeringComments
|
||||
// Intentionally NOT restoring: worktree, status, blockedBy, paused, baseBranch, error, steeringComments
|
||||
};
|
||||
|
||||
// Write task.json
|
||||
|
||||
@@ -387,10 +387,6 @@ export interface Task {
|
||||
* unmerged branch. The executor reads this to branch from the
|
||||
* dependency's branch instead of HEAD. Cleared after worktree creation. */
|
||||
baseBranch?: string;
|
||||
/** Commit SHA of the base branch at worktree creation time.
|
||||
* Used for computing file diffs when reviewing task changes.
|
||||
* Set by the executor when creating the worktree. */
|
||||
baseCommitSha?: string;
|
||||
attachments?: TaskAttachment[];
|
||||
steeringComments?: SteeringComment[];
|
||||
comments?: TaskComment[];
|
||||
@@ -432,8 +428,6 @@ export interface Task {
|
||||
error?: string;
|
||||
/** Optional summary of what was changed/fixed when task is completed */
|
||||
summary?: string;
|
||||
/** Files modified during agent execution, captured at task completion time */
|
||||
modifiedFiles?: string[];
|
||||
/** ISO-8601 timestamp of when the task last entered its current column.
|
||||
* Used to sort cards within a column so that recently-moved cards appear at the top. */
|
||||
columnMovedAt?: string;
|
||||
@@ -867,11 +861,8 @@ export interface ArchivedTaskEntry {
|
||||
breakIntoSubtasks?: boolean;
|
||||
paused?: boolean;
|
||||
baseBranch?: string;
|
||||
baseCommitSha?: string;
|
||||
mergeRetries?: number;
|
||||
error?: string;
|
||||
/** Files modified during agent execution, captured at task completion time */
|
||||
modifiedFiles?: string[];
|
||||
}
|
||||
|
||||
/** Type of planning question presented to the user */
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
test: {
|
||||
include: ["src/**/*.test.ts"],
|
||||
maxWorkers,
|
||||
fileParallelism: true,
|
||||
fileParallelism: false,
|
||||
coverage: {
|
||||
enabled: false,
|
||||
reporter: ["text", "html", "json"],
|
||||
|
||||
Reference in New Issue
Block a user