feat(FN-2383): persist task priority across core storage
- Add task-priority contract, normalization helpers, and exports in @fusion/core types/index - Store task priority in SQLite and migrate existing databases with default values - Update task store behavior and sorting tests to preserve and order by persisted priority - Add migration/regression coverage for archived tasks and refresh storage/task-management docs
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { TaskStore } from "../store.js";
|
||||
import { sortTasksByPriorityThenAgeAndId } from "../task-priority.js";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
@@ -52,4 +53,16 @@ describe("TaskStore.listTasks() sort order", () => {
|
||||
expect(nums[i]).toBeGreaterThan(nums[i - 1]);
|
||||
}
|
||||
});
|
||||
|
||||
it("provides deterministic helper ordering by priority then age then id", () => {
|
||||
const sorted = sortTasksByPriorityThenAgeAndId([
|
||||
{ id: "FN-010", createdAt: "2026-01-02T00:00:00Z", priority: "normal" },
|
||||
{ id: "FN-001", createdAt: "2026-01-01T00:00:00Z", priority: "high" },
|
||||
{ id: "FN-002", createdAt: "2026-01-01T00:00:00Z", priority: "high" },
|
||||
{ id: "FN-003", createdAt: "2026-01-01T00:00:00Z" },
|
||||
{ id: "FN-004", createdAt: "2026-01-01T00:00:00Z", priority: "urgent" },
|
||||
]);
|
||||
|
||||
expect(sorted.map((task) => task.id)).toEqual(["FN-004", "FN-001", "FN-002", "FN-003", "FN-010"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
|
||||
|
||||
expect(tableNames.has("task_documents")).toBe(true);
|
||||
expect(tableNames.has("task_document_revisions")).toBe(true);
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
|
||||
const index = db
|
||||
.prepare(
|
||||
|
||||
@@ -184,6 +184,7 @@ describe("migrateFromLegacy", () => {
|
||||
id: "FN-001",
|
||||
title: "Test task",
|
||||
description: "A test task",
|
||||
priority: "urgent",
|
||||
column: "todo",
|
||||
dependencies: ["FN-000"],
|
||||
steps: [{ name: "Step 1", status: "done" }],
|
||||
@@ -205,6 +206,7 @@ describe("migrateFromLegacy", () => {
|
||||
expect(row).toBeDefined();
|
||||
expect(row.title).toBe("Test task");
|
||||
expect(row.column).toBe("todo");
|
||||
expect(row.priority).toBe("urgent");
|
||||
expect(row.size).toBe("M");
|
||||
expect(row.reviewLevel).toBe(2);
|
||||
expect(JSON.parse(row.dependencies)).toEqual(["FN-000"]);
|
||||
@@ -212,6 +214,32 @@ describe("migrateFromLegacy", () => {
|
||||
expect(JSON.parse(row.prInfo).number).toBe(1);
|
||||
});
|
||||
|
||||
it("defaults migrated tasks to normal priority when legacy task.json omits priority", async () => {
|
||||
const tasksDir = join(fusionDir, "tasks");
|
||||
const taskDir = join(tasksDir, "FN-001");
|
||||
await mkdir(taskDir, { recursive: true });
|
||||
|
||||
await writeFile(
|
||||
join(taskDir, "task.json"),
|
||||
JSON.stringify({
|
||||
id: "FN-001",
|
||||
description: "Legacy priorityless task",
|
||||
column: "triage",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
|
||||
await migrateFromLegacy(fusionDir, db);
|
||||
|
||||
const row = db.prepare("SELECT priority FROM tasks WHERE id = 'FN-001'").get() as { priority: string };
|
||||
expect(row.priority).toBe("normal");
|
||||
});
|
||||
|
||||
it("skips invalid task.json files", async () => {
|
||||
const tasksDir = join(fusionDir, "tasks");
|
||||
const validDir = join(tasksDir, "FN-001");
|
||||
|
||||
@@ -13,6 +13,7 @@ import { readFile, readdir, rename } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Database } from "./db.js";
|
||||
import { toJson, toJsonNullable, normalizeTaskComments } from "./db.js";
|
||||
import { normalizeTaskPriority } from "./task-priority.js";
|
||||
import type { Task, BoardConfig, ActivityLogEntry, ArchivedTaskEntry, WorkflowStep } from "./types.js";
|
||||
import type { ScheduledTask } from "./automation.js";
|
||||
|
||||
@@ -215,7 +216,7 @@ async function migrateTasks(fusionDir: string, db: Database): Promise<void> {
|
||||
|
||||
const insertStmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO tasks (
|
||||
id, title, description, "column", status, size, reviewLevel, currentStep,
|
||||
id, title, description, priority, "column", status, size, reviewLevel, currentStep,
|
||||
worktree, blockedBy, paused, baseBranch, baseCommitSha, modelPresetId,
|
||||
modelProvider, modelId, validatorModelProvider, validatorModelId,
|
||||
mergeRetries, recoveryRetryCount, nextRecoveryAt,
|
||||
@@ -224,7 +225,7 @@ async function migrateTasks(fusionDir: string, db: Database): Promise<void> {
|
||||
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, sliceId
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`);
|
||||
@@ -248,6 +249,7 @@ async function migrateTasks(fusionDir: string, db: Database): Promise<void> {
|
||||
task.id,
|
||||
task.title ?? null,
|
||||
task.description,
|
||||
normalizeTaskPriority(task.priority),
|
||||
task.column,
|
||||
task.status ?? null,
|
||||
task.size ?? null,
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("Database", () => {
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -154,7 +154,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -761,7 +761,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -786,11 +786,52 @@ describe("schema migrations", () => {
|
||||
const db = new Database(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("migrates v42 databases by adding task priority with normal default", () => {
|
||||
tmpDir = makeTmpDir();
|
||||
const fusionDir = join(tmpDir, ".fusion");
|
||||
const db = new Database(fusionDir);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value TEXT);
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
description TEXT NOT NULL,
|
||||
"column" TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
executionMode TEXT DEFAULT 'standard'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
nextId INTEGER DEFAULT 1,
|
||||
nextWorkflowStepId INTEGER DEFAULT 1,
|
||||
settings TEXT DEFAULT '{}',
|
||||
workflowSteps TEXT DEFAULT '[]',
|
||||
updatedAt TEXT
|
||||
);
|
||||
`);
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '42')");
|
||||
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
|
||||
db.exec(`INSERT INTO tasks (id, description, "column", createdAt, updatedAt) VALUES ('FN-1', 'legacy', 'triage', '2026-01-01', '2026-01-01')`);
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("priority");
|
||||
|
||||
const task = db.prepare("SELECT priority FROM tasks WHERE id = 'FN-1'").get() as { priority: string };
|
||||
expect(task.priority).toBe("normal");
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -806,7 +847,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "agentRatings" }]);
|
||||
@@ -830,7 +871,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "mission_events" }]);
|
||||
@@ -934,7 +975,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1303,7 +1344,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -86,7 +86,7 @@ export function probeFts5(db: DatabaseSync): boolean {
|
||||
|
||||
// ── Schema Definition ────────────────────────────────────────────────
|
||||
|
||||
const SCHEMA_VERSION = 42;
|
||||
const SCHEMA_VERSION = 43;
|
||||
|
||||
function normalizeTaskComments(
|
||||
steeringComments: SteeringComment[] | undefined,
|
||||
@@ -152,6 +152,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
description TEXT NOT NULL,
|
||||
priority TEXT DEFAULT 'normal',
|
||||
"column" TEXT NOT NULL,
|
||||
status TEXT,
|
||||
size TEXT,
|
||||
@@ -1700,6 +1701,19 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
// Task priority contract (FN-2383)
|
||||
// Adds priority column and normalizes legacy/missing values to 'normal'.
|
||||
if (version < 43) {
|
||||
this.applyMigration(43, () => {
|
||||
this.addColumnIfMissing("tasks", "priority", "TEXT DEFAULT 'normal'");
|
||||
this.db.exec(`
|
||||
UPDATE tasks
|
||||
SET priority = 'normal'
|
||||
WHERE priority IS NULL OR priority = '' OR priority NOT IN ('low', 'normal', 'high', 'urgent')
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, validateMessageMetadata } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskDetail, InboxTask, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskDetail, InboxTask, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
||||
export {
|
||||
BUILTIN_AGENT_PROMPTS,
|
||||
@@ -223,6 +223,15 @@ export {
|
||||
applyRoadmapFeatureReorder,
|
||||
moveRoadmapFeature,
|
||||
} from "./roadmap-ordering.js";
|
||||
export {
|
||||
isTaskPriority,
|
||||
normalizeTaskPriority,
|
||||
getTaskPriorityRank,
|
||||
compareTaskPriority,
|
||||
compareTasksByPriorityThenAgeAndId,
|
||||
sortTasksByPriorityThenAgeAndId,
|
||||
} from "./task-priority.js";
|
||||
export type { TaskPrioritySortable } from "./task-priority.js";
|
||||
export {
|
||||
mapFeatureToTaskHandoff,
|
||||
mapRoadmapToMissionHandoff,
|
||||
|
||||
@@ -776,7 +776,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
|
||||
const db1 = createDatabase(legacyDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(42);
|
||||
expect(db1.getSchemaVersion()).toBe(43);
|
||||
db1.close();
|
||||
|
||||
// Step 2: Manually downgrade to version 32 and drop insight tables
|
||||
@@ -811,7 +811,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
expect(tableNamesBefore).not.toContain("project_insight_runs");
|
||||
// Now run init — this triggers the v32→v33 migration
|
||||
db3.init();
|
||||
expect(db3.getSchemaVersion()).toBe(42);
|
||||
expect(db3.getSchemaVersion()).toBe(43);
|
||||
|
||||
// Step 4: Verify insight tables exist after migration
|
||||
const tablesAfter = db3.prepare(
|
||||
@@ -842,12 +842,12 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
try {
|
||||
const db1 = createDatabase(testDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(42);
|
||||
expect(db1.getSchemaVersion()).toBe(43);
|
||||
db1.close();
|
||||
|
||||
const db2 = createDatabase(testDir);
|
||||
expect(() => db2.init()).not.toThrow();
|
||||
expect(db2.getSchemaVersion()).toBe(42);
|
||||
expect(db2.getSchemaVersion()).toBe(43);
|
||||
db2.close();
|
||||
} finally {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
|
||||
@@ -2626,7 +2626,7 @@ describe("MissionStore", () => {
|
||||
|
||||
describe("Loop State & Validator Run Schema (v31)", () => {
|
||||
it("schema version is 40 after migration", () => {
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
});
|
||||
|
||||
it("mission_features table has loop state columns", () => {
|
||||
|
||||
@@ -739,7 +739,7 @@ describe("RoadmapStore", () => {
|
||||
|
||||
describe("schema version", () => {
|
||||
it("schema version is 40 after init", () => {
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 40", () => {
|
||||
expect(db.getSchemaVersion()).toBe(42);
|
||||
expect(db.getSchemaVersion()).toBe(43);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,6 +155,74 @@ describe("TaskStore", () => {
|
||||
|
||||
});
|
||||
|
||||
describe("task priority", () => {
|
||||
it("defaults to normal priority when omitted", async () => {
|
||||
const task = await store.createTask({
|
||||
description: "Priority default task",
|
||||
});
|
||||
|
||||
expect(task.priority).toBe("normal");
|
||||
|
||||
const detail = await store.getTask(task.id);
|
||||
expect(detail.priority).toBe("normal");
|
||||
});
|
||||
|
||||
it("persists explicit priority on create and update, and normalizes null update to default", async () => {
|
||||
const task = await store.createTask({
|
||||
description: "Priority explicit task",
|
||||
priority: "urgent",
|
||||
});
|
||||
expect(task.priority).toBe("urgent");
|
||||
|
||||
const lowered = await store.updateTask(task.id, { priority: "low" });
|
||||
expect(lowered.priority).toBe("low");
|
||||
|
||||
const reset = await store.updateTask(task.id, { priority: null });
|
||||
expect(reset.priority).toBe("normal");
|
||||
|
||||
const detail = await store.getTask(task.id);
|
||||
expect(detail.priority).toBe("normal");
|
||||
});
|
||||
|
||||
it("preserves explicit priority through archive and unarchive", async () => {
|
||||
const task = await store.createTask({
|
||||
description: "Archive priority task",
|
||||
column: "done",
|
||||
priority: "high",
|
||||
});
|
||||
|
||||
await store.archiveTask(task.id, false);
|
||||
const archived = await store.getTask(task.id);
|
||||
expect(archived.priority).toBe("high");
|
||||
|
||||
const unarchived = await store.unarchiveTask(task.id);
|
||||
expect(unarchived.priority).toBe("high");
|
||||
});
|
||||
|
||||
it("restores legacy archive entries missing priority as normal", async () => {
|
||||
const now = new Date().toISOString();
|
||||
const legacyEntry = {
|
||||
id: "FN-999",
|
||||
title: "Legacy archive task",
|
||||
description: "Legacy task without explicit priority",
|
||||
column: "archived" as const,
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
archivedAt: now,
|
||||
};
|
||||
|
||||
const restored = await (store as any).restoreFromArchive(legacyEntry);
|
||||
expect(restored.priority).toBe("normal");
|
||||
|
||||
const unarchived = await store.unarchiveTask(legacyEntry.id);
|
||||
expect(unarchived.priority).toBe("normal");
|
||||
});
|
||||
});
|
||||
|
||||
describe("breakIntoSubtasks task creation flag", () => {
|
||||
it("persists breakIntoSubtasks=true when explicitly requested", async () => {
|
||||
const task = await store.createTask({
|
||||
|
||||
@@ -3,8 +3,9 @@ import { randomUUID } from "node:crypto";
|
||||
import { mkdir, readdir, readFile, writeFile, rename, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, watch, type FSWatcher } from "node:fs";
|
||||
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode } from "./types.js";
|
||||
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority } from "./types.js";
|
||||
import { VALID_TRANSITIONS, DEFAULT_SETTINGS, isGlobalSettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey } from "./types.js";
|
||||
import { normalizeTaskPriority } from "./task-priority.js";
|
||||
import { GlobalSettingsStore } from "./global-settings.js";
|
||||
import { Database, toJson, toJsonNullable, fromJson } from "./db.js";
|
||||
import { ArchiveDatabase } from "./archive-db.js";
|
||||
@@ -25,6 +26,7 @@ interface TaskRow {
|
||||
id: string;
|
||||
title: string | null;
|
||||
description: string;
|
||||
priority: string | null;
|
||||
column: string;
|
||||
status: string | null;
|
||||
size: string | null;
|
||||
@@ -445,6 +447,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
id: row.id,
|
||||
title: row.title || undefined,
|
||||
description: row.description,
|
||||
priority: normalizeTaskPriority(row.priority),
|
||||
column: row.column as Column,
|
||||
status: row.status || undefined,
|
||||
size: (row.size || undefined) as Task["size"],
|
||||
@@ -519,6 +522,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
description: entry.description,
|
||||
priority: normalizeTaskPriority(entry.priority),
|
||||
column: "archived",
|
||||
dependencies: entry.dependencies ?? [],
|
||||
steps: entry.steps ?? [],
|
||||
@@ -637,6 +641,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
priority: normalizeTaskPriority(task.priority),
|
||||
column: "archived",
|
||||
dependencies: task.dependencies,
|
||||
steps: task.steps,
|
||||
@@ -714,7 +719,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
const prefix = tableAlias ? `${tableAlias}.` : "";
|
||||
return [
|
||||
"id", "title", "description", "\"column\"", "status", "size", "reviewLevel", "currentStep",
|
||||
"id", "title", "description", "priority", "\"column\"", "status", "size", "reviewLevel", "currentStep",
|
||||
"worktree", "blockedBy", "paused", "baseBranch", "branch", "baseCommitSha",
|
||||
"modelPresetId", "modelProvider", "modelId",
|
||||
"validatorModelProvider", "validatorModelId",
|
||||
@@ -732,7 +737,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
private getTaskSelectClauseWithActivityLogLimit(limit: number): string {
|
||||
const columns = [
|
||||
"id", "title", "description", "\"column\"", "status", "size", "reviewLevel", "currentStep",
|
||||
"id", "title", "description", "priority", "\"column\"", "status", "size", "reviewLevel", "currentStep",
|
||||
"worktree", "blockedBy", "paused", "baseBranch", "branch", "baseCommitSha",
|
||||
"modelPresetId", "modelProvider", "modelId",
|
||||
"validatorModelProvider", "validatorModelId",
|
||||
@@ -775,7 +780,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
private upsertTask(task: Task): void {
|
||||
this.db.prepare(`
|
||||
INSERT INTO tasks (
|
||||
id, title, description, "column", status, size, reviewLevel, currentStep,
|
||||
id, title, description, priority, "column", status, size, reviewLevel, currentStep,
|
||||
worktree, blockedBy, paused, baseBranch, branch, baseCommitSha, modelPresetId, modelProvider,
|
||||
modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
|
||||
workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, nextRecoveryAt, error,
|
||||
@@ -784,12 +789,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, checkedOutBy, checkedOutAt
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
description = excluded.description,
|
||||
priority = excluded.priority,
|
||||
"column" = excluded."column",
|
||||
status = excluded.status,
|
||||
size = excluded.size,
|
||||
@@ -845,6 +851,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.id,
|
||||
task.title ?? null,
|
||||
task.description,
|
||||
normalizeTaskPriority(task.priority),
|
||||
task.column,
|
||||
task.status ?? null,
|
||||
task.size ?? null,
|
||||
@@ -1121,6 +1128,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
if (!Array.isArray(fileTask.log)) fileTask.log = [];
|
||||
if (!Array.isArray(fileTask.dependencies)) fileTask.dependencies = [];
|
||||
if (!Array.isArray(fileTask.steps)) fileTask.steps = [];
|
||||
fileTask.priority = normalizeTaskPriority(fileTask.priority);
|
||||
return fileTask;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
@@ -1832,6 +1840,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
id,
|
||||
title,
|
||||
description: input.description,
|
||||
priority: normalizeTaskPriority(input.priority),
|
||||
column: input.column || "triage",
|
||||
dependencies: input.dependencies || [],
|
||||
breakIntoSubtasks: input.breakIntoSubtasks === true ? true : undefined,
|
||||
@@ -1894,6 +1903,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
id: newId,
|
||||
title: sourceTask.title,
|
||||
description: `${sourceTask.description}\n\n(Duplicated from ${id})`,
|
||||
priority: normalizeTaskPriority(sourceTask.priority),
|
||||
column: "triage",
|
||||
modelPresetId: sourceTask.modelPresetId,
|
||||
dependencies: [], // Fresh task should have no dependencies
|
||||
@@ -1970,6 +1980,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
id: newId,
|
||||
title: `Refinement: ${sourceLabel}`,
|
||||
description: `${feedback.trim()}\n\nRefines: ${id}`,
|
||||
priority: normalizeTaskPriority(sourceTask.priority),
|
||||
column: "triage",
|
||||
dependencies: [id], // Refinement depends on the original being complete
|
||||
steps: [], // Reset execution state
|
||||
@@ -2398,7 +2409,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
async updateTask(
|
||||
id: string,
|
||||
updates: { title?: string; description?: string; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
runContext?: RunMutationContext,
|
||||
): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
@@ -2417,6 +2428,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
if (updates.title !== undefined) task.title = updates.title;
|
||||
if (updates.description !== undefined) task.description = updates.description;
|
||||
if (updates.priority === null) {
|
||||
task.priority = normalizeTaskPriority(undefined);
|
||||
} else if (updates.priority !== undefined) {
|
||||
task.priority = normalizeTaskPriority(updates.priority);
|
||||
}
|
||||
if (updates.worktree === null) {
|
||||
task.worktree = undefined;
|
||||
} else if (updates.worktree !== undefined) {
|
||||
@@ -4812,6 +4828,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
description: entry.description,
|
||||
priority: normalizeTaskPriority(entry.priority),
|
||||
column: "archived", // Will be changed to "done" by unarchiveTask
|
||||
dependencies: entry.dependencies,
|
||||
steps: entry.steps,
|
||||
|
||||
63
packages/core/src/task-priority.test.ts
Normal file
63
packages/core/src/task-priority.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
compareTaskPriority,
|
||||
compareTasksByPriorityThenAgeAndId,
|
||||
getTaskPriorityRank,
|
||||
isTaskPriority,
|
||||
normalizeTaskPriority,
|
||||
sortTasksByPriorityThenAgeAndId,
|
||||
} from "./task-priority.js";
|
||||
import {
|
||||
DEFAULT_TASK_PRIORITY,
|
||||
TASK_PRIORITIES,
|
||||
type TaskPriority,
|
||||
} from "./types.js";
|
||||
import * as core from "./index.js";
|
||||
|
||||
describe("task-priority", () => {
|
||||
it("defines the bounded priority contract in order", () => {
|
||||
expect(TASK_PRIORITIES).toEqual(["low", "normal", "high", "urgent"]);
|
||||
expect(DEFAULT_TASK_PRIORITY).toBe("normal");
|
||||
});
|
||||
|
||||
it("normalizes missing or invalid values to default", () => {
|
||||
expect(normalizeTaskPriority(undefined)).toBe("normal");
|
||||
expect(normalizeTaskPriority(null)).toBe("normal");
|
||||
expect(normalizeTaskPriority("")).toBe("normal");
|
||||
});
|
||||
|
||||
it("identifies valid task priorities", () => {
|
||||
for (const value of TASK_PRIORITIES) {
|
||||
expect(isTaskPriority(value)).toBe(true);
|
||||
}
|
||||
expect(isTaskPriority("in_progress")).toBe(false);
|
||||
});
|
||||
|
||||
it("provides deterministic ranks and priority comparator", () => {
|
||||
const orderedByRank: TaskPriority[] = ["low", "normal", "high", "urgent"];
|
||||
expect(orderedByRank.map((priority) => getTaskPriorityRank(priority))).toEqual([0, 1, 2, 3]);
|
||||
expect(compareTaskPriority("urgent", "low")).toBeLessThan(0);
|
||||
expect(compareTaskPriority(undefined, "normal")).toBe(0);
|
||||
});
|
||||
|
||||
it("sorts tasks by priority desc then createdAt asc then id asc", () => {
|
||||
const tasks = [
|
||||
{ id: "FN-002", createdAt: "2026-01-01T00:00:00.000Z", priority: "high" as TaskPriority },
|
||||
{ id: "FN-001", createdAt: "2026-01-01T00:00:00.000Z", priority: "high" as TaskPriority },
|
||||
{ id: "FN-009", createdAt: "2026-01-02T00:00:00.000Z", priority: "urgent" as TaskPriority },
|
||||
{ id: "FN-003", createdAt: "2026-01-01T00:00:00.000Z", priority: undefined },
|
||||
];
|
||||
|
||||
const sorted = sortTasksByPriorityThenAgeAndId(tasks);
|
||||
expect(sorted.map((task) => task.id)).toEqual(["FN-009", "FN-001", "FN-002", "FN-003"]);
|
||||
|
||||
// comparator function should match sorted behavior
|
||||
expect(compareTasksByPriorityThenAgeAndId(tasks[0], tasks[1])).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("re-exports priority helpers from the core index", () => {
|
||||
expect(core.TASK_PRIORITIES).toEqual(TASK_PRIORITIES);
|
||||
expect(core.DEFAULT_TASK_PRIORITY).toBe("normal");
|
||||
expect(core.normalizeTaskPriority("bogus")).toBe(DEFAULT_TASK_PRIORITY);
|
||||
});
|
||||
});
|
||||
78
packages/core/src/task-priority.ts
Normal file
78
packages/core/src/task-priority.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES } from "./types.js";
|
||||
import type { TaskPriority } from "./types.js";
|
||||
|
||||
export interface TaskPrioritySortable {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
priority?: TaskPriority | null;
|
||||
}
|
||||
|
||||
const PRIORITY_RANK: Record<TaskPriority, number> = {
|
||||
low: 0,
|
||||
normal: 1,
|
||||
high: 2,
|
||||
urgent: 3,
|
||||
};
|
||||
|
||||
export function isTaskPriority(value: unknown): value is TaskPriority {
|
||||
return typeof value === "string" && (TASK_PRIORITIES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an optional/legacy task priority value to the bounded core contract.
|
||||
* Missing or invalid values map to DEFAULT_TASK_PRIORITY (`normal`).
|
||||
*/
|
||||
export function normalizeTaskPriority(priority: unknown): TaskPriority {
|
||||
return isTaskPriority(priority) ? priority : DEFAULT_TASK_PRIORITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a numeric rank where higher values indicate higher priority.
|
||||
*/
|
||||
export function getTaskPriorityRank(priority: unknown): number {
|
||||
return PRIORITY_RANK[normalizeTaskPriority(priority)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare priorities so higher-priority tasks sort first.
|
||||
*/
|
||||
export function compareTaskPriority(a: unknown, b: unknown): number {
|
||||
return getTaskPriorityRank(b) - getTaskPriorityRank(a);
|
||||
}
|
||||
|
||||
function compareTaskId(a: string, b: string): number {
|
||||
const aNum = Number.parseInt(a.slice(a.lastIndexOf("-") + 1), 10);
|
||||
const bNum = Number.parseInt(b.slice(b.lastIndexOf("-") + 1), 10);
|
||||
|
||||
if (Number.isFinite(aNum) && Number.isFinite(bNum) && aNum !== bNum) {
|
||||
return aNum - bNum;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic comparator for priority-aware task ordering:
|
||||
* 1) priority (urgent → low), 2) createdAt ASC, 3) id ASC.
|
||||
*/
|
||||
export function compareTasksByPriorityThenAgeAndId<T extends TaskPrioritySortable>(a: T, b: T): number {
|
||||
const priorityCmp = compareTaskPriority(a.priority, b.priority);
|
||||
if (priorityCmp !== 0) {
|
||||
return priorityCmp;
|
||||
}
|
||||
|
||||
if (a.createdAt !== b.createdAt) {
|
||||
return a.createdAt.localeCompare(b.createdAt);
|
||||
}
|
||||
|
||||
return compareTaskId(a.id, b.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sorted copy (input remains unchanged).
|
||||
*/
|
||||
export function sortTasksByPriorityThenAgeAndId<T extends TaskPrioritySortable>(
|
||||
tasks: readonly T[],
|
||||
): T[] {
|
||||
return [...tasks].sort(compareTasksByPriorityThenAgeAndId);
|
||||
}
|
||||
@@ -5,6 +5,16 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
||||
export const COLUMNS = ["triage", "todo", "in-progress", "in-review", "done", "archived"] as const;
|
||||
export type Column = (typeof COLUMNS)[number];
|
||||
|
||||
/** Ordered task-priority levels for the core task domain contract. */
|
||||
export const TASK_PRIORITIES = ["low", "normal", "high", "urgent"] as const;
|
||||
export type TaskPriority = (typeof TASK_PRIORITIES)[number];
|
||||
|
||||
/**
|
||||
* Default task priority used for legacy rows/entries and create flows when
|
||||
* callers omit the priority field.
|
||||
*/
|
||||
export const DEFAULT_TASK_PRIORITY: TaskPriority = "normal";
|
||||
|
||||
/**
|
||||
* Execution mode for task implementation.
|
||||
* Controls how the executor agent approaches the task:
|
||||
@@ -651,6 +661,11 @@ export interface Task {
|
||||
id: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
/**
|
||||
* Task importance level. Missing legacy values normalize to `normal` when
|
||||
* tasks are hydrated from persistence.
|
||||
*/
|
||||
priority?: TaskPriority;
|
||||
column: Column;
|
||||
dependencies: string[];
|
||||
/** User-requested hint for triage: prefer splitting into child tasks when appropriate. */
|
||||
@@ -804,6 +819,10 @@ export interface InboxTask {
|
||||
export interface TaskCreateInput {
|
||||
title?: string;
|
||||
description: string;
|
||||
/**
|
||||
* Optional task importance level. Omitted values default to `normal`.
|
||||
*/
|
||||
priority?: TaskPriority;
|
||||
column?: Column;
|
||||
dependencies?: string[];
|
||||
breakIntoSubtasks?: boolean;
|
||||
@@ -1554,6 +1573,11 @@ export interface ArchivedTaskEntry {
|
||||
id: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
/**
|
||||
* Task importance level at archive time. Missing legacy values should be
|
||||
* interpreted as `normal` during restore/read flows.
|
||||
*/
|
||||
priority?: TaskPriority;
|
||||
column: "archived"; // Always archived when in the log
|
||||
dependencies: string[];
|
||||
steps: TaskStep[];
|
||||
|
||||
Reference in New Issue
Block a user