feat(FN-4371): complete Step 1 — schema and persistence

Fusion-Task-Id: FN-4371
Fusion-Task-Lineage: 4175ba4c-b7cb-4f3a-87c5-75e51f06c0ff
This commit is contained in:
Fusion
2026-05-14 09:17:53 -07:00
committed by gsxdsm
parent 4bafbafe73
commit bf6d3544da
6 changed files with 65 additions and 9 deletions

View File

@@ -767,6 +767,39 @@ describe("TaskStore", () => {
}); });
describe("noCommitsExpected persistence", () => {
it("round-trips noCommitsExpected=true through create and reload", async () => {
const created = await store.createTask({
description: "Decision-only task",
noCommitsExpected: true,
});
expect(created.noCommitsExpected).toBe(true);
const reloaded = await store.getTask(created.id);
expect(reloaded.noCommitsExpected).toBe(true);
});
it("keeps noCommitsExpected undefined when omitted", async () => {
const created = await store.createTask({ description: "Regular task" });
expect(created.noCommitsExpected).toBeUndefined();
const reloaded = await store.getTask(created.id);
expect(reloaded.noCommitsExpected).toBeUndefined();
});
it("updates noCommitsExpected via updateTask", async () => {
const created = await store.createTask({ description: "Toggle decision-only" });
const updated = await store.updateTask(created.id, { noCommitsExpected: true });
expect(updated.noCommitsExpected).toBe(true);
const reloaded = await store.getTask(created.id);
expect(reloaded.noCommitsExpected).toBe(true);
});
});
describe("executionMode persistence", () => { describe("executionMode persistence", () => {
it("sets executionMode to 'fast' via createTask and persists", async () => { it("sets executionMode to 'fast' via createTask and persists", async () => {
const created = await store.createTask({ const created = await store.createTask({

View File

@@ -226,10 +226,10 @@ async function migrateTasks(fusionDir: string, db: Database): Promise<void> {
columnMovedAt, dependencies, steps, log, attachments, steeringComments, columnMovedAt, dependencies, steps, log, attachments, steeringComments,
comments, workflowStepResults, prInfo, issueInfo, comments, workflowStepResults, prInfo, issueInfo,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl, sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, sliceId mergeDetails, breakIntoSubtasks, noCommitsExpected, enabledWorkflowSteps, modifiedFiles, sliceId
) VALUES ( ) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) )
`); `);
@@ -295,6 +295,7 @@ async function migrateTasks(fusionDir: string, db: Database): Promise<void> {
task.sourceIssue?.url ?? null, task.sourceIssue?.url ?? null,
toJsonNullable(task.mergeDetails), toJsonNullable(task.mergeDetails),
task.breakIntoSubtasks ? 1 : 0, task.breakIntoSubtasks ? 1 : 0,
task.noCommitsExpected ? 1 : 0,
toJson(task.enabledWorkflowSteps || []), toJson(task.enabledWorkflowSteps || []),
toJson(task.modifiedFiles || []), toJson(task.modifiedFiles || []),
task.sliceId ?? null, task.sliceId ?? null,

View File

@@ -252,6 +252,7 @@ CREATE TABLE IF NOT EXISTS tasks (
sourceIssueUrl TEXT, sourceIssueUrl TEXT,
mergeDetails TEXT, mergeDetails TEXT,
breakIntoSubtasks INTEGER DEFAULT 0, breakIntoSubtasks INTEGER DEFAULT 0,
noCommitsExpected INTEGER DEFAULT 0,
enabledWorkflowSteps TEXT DEFAULT '[]', enabledWorkflowSteps TEXT DEFAULT '[]',
modifiedFiles TEXT DEFAULT '[]', modifiedFiles TEXT DEFAULT '[]',
missionId TEXT, missionId TEXT,

View File

@@ -119,6 +119,7 @@ interface TaskRow {
sourceIssueUrl: string | null; sourceIssueUrl: string | null;
mergeDetails: string | null; mergeDetails: string | null;
breakIntoSubtasks: number | null; breakIntoSubtasks: number | null;
noCommitsExpected: number | null;
enabledWorkflowSteps: string | null; enabledWorkflowSteps: string | null;
modifiedFiles: string | null; modifiedFiles: string | null;
missionId: string | null; missionId: string | null;
@@ -1087,6 +1088,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
})(), })(),
mergeDetails: fromJson<import("./types.js").MergeDetails>(row.mergeDetails), mergeDetails: fromJson<import("./types.js").MergeDetails>(row.mergeDetails),
breakIntoSubtasks: row.breakIntoSubtasks ? true : undefined, breakIntoSubtasks: row.breakIntoSubtasks ? true : undefined,
noCommitsExpected: row.noCommitsExpected ? true : undefined,
enabledWorkflowSteps: (() => { const e = fromJson<string[]>(row.enabledWorkflowSteps); return e && e.length > 0 ? e : 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; })(), modifiedFiles: (() => { const m = fromJson<string[]>(row.modifiedFiles); return m && m.length > 0 ? m : undefined; })(),
missionId: row.missionId || undefined, missionId: row.missionId || undefined,
@@ -1155,6 +1157,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
planningModelProvider: entry.planningModelProvider, planningModelProvider: entry.planningModelProvider,
planningModelId: entry.planningModelId, planningModelId: entry.planningModelId,
breakIntoSubtasks: entry.breakIntoSubtasks, breakIntoSubtasks: entry.breakIntoSubtasks,
noCommitsExpected: entry.noCommitsExpected,
modifiedFiles: slim ? undefined : entry.modifiedFiles, modifiedFiles: slim ? undefined : entry.modifiedFiles,
missionId: entry.missionId, missionId: entry.missionId,
sliceId: entry.sliceId, sliceId: entry.sliceId,
@@ -1284,6 +1287,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
planningModelProvider: task.planningModelProvider, planningModelProvider: task.planningModelProvider,
planningModelId: task.planningModelId, planningModelId: task.planningModelId,
breakIntoSubtasks: task.breakIntoSubtasks, breakIntoSubtasks: task.breakIntoSubtasks,
noCommitsExpected: task.noCommitsExpected,
baseBranch: task.baseBranch, baseBranch: task.baseBranch,
branch: task.branch, branch: task.branch,
baseCommitSha: task.baseCommitSha, baseCommitSha: task.baseCommitSha,
@@ -1347,7 +1351,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt", "createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
"dependencies", "steps", "comments", "review", "reviewState", "workflowStepResults", "steeringComments", "dependencies", "steps", "comments", "review", "reviewState", "workflowStepResults", "steeringComments",
"attachments", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails", "attachments", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles", "breakIntoSubtasks", "noCommitsExpected", "enabledWorkflowSteps", "modifiedFiles",
"missionId", "sliceId", "scopeOverride", "scopeOverrideReason", "assignedAgentId", "pausedByAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource", "missionId", "sliceId", "scopeOverride", "scopeOverrideReason", "assignedAgentId", "pausedByAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
"sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata", "sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata",
"checkedOutBy", "checkedOutAt", "checkoutNodeId", "checkoutRunId", "checkoutLeaseRenewedAt", "checkoutLeaseEpoch", "checkedOutBy", "checkedOutAt", "checkoutNodeId", "checkoutRunId", "checkoutLeaseRenewedAt", "checkoutLeaseEpoch",
@@ -1396,7 +1400,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt", "createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
"dependencies", "steps", "attachments", "steeringComments", "dependencies", "steps", "attachments", "steeringComments",
"comments", "review", "reviewState", "workflowStepResults", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails", "comments", "review", "reviewState", "workflowStepResults", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles", "breakIntoSubtasks", "noCommitsExpected", "enabledWorkflowSteps", "modifiedFiles",
"missionId", "sliceId", "scopeOverride", "scopeOverrideReason", "assignedAgentId", "pausedByAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource", "missionId", "sliceId", "scopeOverride", "scopeOverrideReason", "assignedAgentId", "pausedByAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
"sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata", "sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata",
"checkedOutBy", "checkedOutAt", "checkoutNodeId", "checkoutRunId", "checkoutLeaseRenewedAt", "checkoutLeaseEpoch", "checkedOutBy", "checkedOutAt", "checkoutNodeId", "checkoutRunId", "checkoutLeaseRenewedAt", "checkoutLeaseEpoch",
@@ -1498,6 +1502,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.sourceIssue?.url ?? null, task.sourceIssue?.url ?? null,
toJsonNullable(task.mergeDetails), toJsonNullable(task.mergeDetails),
task.breakIntoSubtasks ? 1 : 0, task.breakIntoSubtasks ? 1 : 0,
task.noCommitsExpected ? 1 : 0,
toJson(task.enabledWorkflowSteps || []), toJson(task.enabledWorkflowSteps || []),
toJson(task.modifiedFiles || []), toJson(task.modifiedFiles || []),
task.missionId ?? null, task.missionId ?? null,
@@ -1545,7 +1550,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
dependencies, steps, log, attachments, steeringComments, dependencies, steps, log, attachments, steeringComments,
comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking, comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl, sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch mergeDetails, breakIntoSubtasks, noCommitsExpected, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
) VALUES (${placeholders}) ) VALUES (${placeholders})
`).run(...values); `).run(...values);
this.db.bumpLastModified(); this.db.bumpLastModified();
@@ -1570,7 +1575,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
dependencies, steps, log, attachments, steeringComments, dependencies, steps, log, attachments, steeringComments,
comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking, comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl, sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch mergeDetails, breakIntoSubtasks, noCommitsExpected, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
) VALUES (${placeholders}) ) VALUES (${placeholders})
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
lineageId = excluded.lineageId, lineageId = excluded.lineageId,
@@ -1644,6 +1649,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
sourceIssueUrl = excluded.sourceIssueUrl, sourceIssueUrl = excluded.sourceIssueUrl,
mergeDetails = excluded.mergeDetails, mergeDetails = excluded.mergeDetails,
breakIntoSubtasks = excluded.breakIntoSubtasks, breakIntoSubtasks = excluded.breakIntoSubtasks,
noCommitsExpected = excluded.noCommitsExpected,
enabledWorkflowSteps = excluded.enabledWorkflowSteps, enabledWorkflowSteps = excluded.enabledWorkflowSteps,
modifiedFiles = excluded.modifiedFiles, modifiedFiles = excluded.modifiedFiles,
missionId = excluded.missionId, missionId = excluded.missionId,
@@ -2945,6 +2951,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
column: input.column || "triage", column: input.column || "triage",
dependencies: input.dependencies || [], dependencies: input.dependencies || [],
breakIntoSubtasks: input.breakIntoSubtasks === true ? true : undefined, breakIntoSubtasks: input.breakIntoSubtasks === true ? true : undefined,
noCommitsExpected: input.noCommitsExpected === true ? true : undefined,
enabledWorkflowSteps: resolvedWorkflowSteps, enabledWorkflowSteps: resolvedWorkflowSteps,
modelPresetId: input.modelPresetId, modelPresetId: input.modelPresetId,
assignedAgentId: input.assignedAgentId, assignedAgentId: input.assignedAgentId,
@@ -3906,7 +3913,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
async updateTask( async updateTask(
id: string, id: string,
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; pausedByAgentId?: string | null; pausedReason?: string | null; tokenBudgetSoftAlertedAt?: string | null; tokenBudgetHardAlertedAt?: string | null; tokenBudgetOverride?: import("./types.js").TaskTokenBudgetOverride | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: 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; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; mergeAuditBounceCount?: 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; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; githubTracking?: import("./types.js").TaskGithubTracking | null; tokenUsage?: import("./types.js").TaskTokenUsage | 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; pausedByAgentId?: string | null; pausedReason?: string | null; tokenBudgetSoftAlertedAt?: string | null; tokenBudgetHardAlertedAt?: string | null; tokenBudgetOverride?: import("./types.js").TaskTokenBudgetOverride | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: 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; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; mergeAuditBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; noCommitsExpected?: boolean | null; 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; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; githubTracking?: import("./types.js").TaskGithubTracking | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
runContext?: RunMutationContext, runContext?: RunMutationContext,
): Promise<Task> { ): Promise<Task> {
return this.withTaskLock(id, async () => { return this.withTaskLock(id, async () => {
@@ -4195,6 +4202,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
if (updates.enabledWorkflowSteps !== undefined) { if (updates.enabledWorkflowSteps !== undefined) {
task.enabledWorkflowSteps = await this.resolveEnabledWorkflowSteps(updates.enabledWorkflowSteps); task.enabledWorkflowSteps = await this.resolveEnabledWorkflowSteps(updates.enabledWorkflowSteps);
} }
if (updates.noCommitsExpected === null) {
task.noCommitsExpected = undefined;
} else if (updates.noCommitsExpected !== undefined) {
task.noCommitsExpected = updates.noCommitsExpected || undefined;
}
if (updates.modelProvider === null) { if (updates.modelProvider === null) {
task.modelProvider = undefined; task.modelProvider = undefined;
} else if (updates.modelProvider !== undefined) { } else if (updates.modelProvider !== undefined) {
@@ -7102,6 +7114,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
planningModelProvider: entry.planningModelProvider, planningModelProvider: entry.planningModelProvider,
planningModelId: entry.planningModelId, planningModelId: entry.planningModelId,
breakIntoSubtasks: entry.breakIntoSubtasks, breakIntoSubtasks: entry.breakIntoSubtasks,
noCommitsExpected: entry.noCommitsExpected,
modifiedFiles: entry.modifiedFiles, modifiedFiles: entry.modifiedFiles,
// Intentionally NOT restoring: worktree, status, blockedBy, paused, executionStartBranch, baseCommitSha, error // Intentionally NOT restoring: worktree, status, blockedBy, paused, executionStartBranch, baseCommitSha, error
}; };

View File

@@ -1209,6 +1209,8 @@ export interface Task {
dependencies: string[]; dependencies: string[];
/** User-requested hint for triage: prefer splitting into child tasks when appropriate. */ /** User-requested hint for triage: prefer splitting into child tasks when appropriate. */
breakIntoSubtasks?: boolean; breakIntoSubtasks?: boolean;
/** When true, this decision-only task is expected to complete without creating git commits. */
noCommitsExpected?: boolean;
worktree?: string; worktree?: string;
steps: TaskStep[]; steps: TaskStep[];
currentStep: number; currentStep: number;
@@ -2989,6 +2991,7 @@ export interface ArchivedTaskEntry {
planningModelId?: string; planningModelId?: string;
/** Optional: other metadata to preserve */ /** Optional: other metadata to preserve */
breakIntoSubtasks?: boolean; breakIntoSubtasks?: boolean;
noCommitsExpected?: boolean;
paused?: boolean; paused?: boolean;
baseBranch?: string; baseBranch?: string;
/** Actual git branch name used for this task's worktree */ /** Actual git branch name used for this task's worktree */

View File

@@ -1552,7 +1552,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
router.patch("/tasks/:id", async (req, res) => { router.patch("/tasks/:id", async (req, res) => {
try { try {
const { store: scopedStore } = await getProjectContext(req); const { store: scopedStore } = await getProjectContext(req);
const { title, description, prompt, priority, dependencies, enabledWorkflowSteps, modelProvider, modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, thinkingLevel, assigneeUserId, reviewLevel, executionMode, sourceIssue, nodeId, branch, baseBranch, githubTracking } = req.body; const { title, description, prompt, priority, dependencies, enabledWorkflowSteps, modelProvider, modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, thinkingLevel, assigneeUserId, reviewLevel, executionMode, sourceIssue, nodeId, branch, baseBranch, githubTracking, noCommitsExpected } = req.body;
const hasBodyField = (field: string) => Object.prototype.hasOwnProperty.call(req.body, field); const hasBodyField = (field: string) => Object.prototype.hasOwnProperty.call(req.body, field);
// Validate model fields are strings or undefined/null // Validate model fields are strings or undefined/null
@@ -1604,6 +1604,10 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
} }
} }
if (hasBodyField("noCommitsExpected") && noCommitsExpected !== undefined && typeof noCommitsExpected !== "boolean") {
throw new Error("noCommitsExpected must be a boolean");
}
let validatedSourceIssue: import("@fusion/core").TaskSourceIssue | null | undefined; let validatedSourceIssue: import("@fusion/core").TaskSourceIssue | null | undefined;
if (hasBodyField("sourceIssue")) { if (hasBodyField("sourceIssue")) {
if (sourceIssue === null) { if (sourceIssue === null) {
@@ -1717,6 +1721,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
if (hasBodyField("priority")) updates.priority = priority; if (hasBodyField("priority")) updates.priority = priority;
if (dependencies !== undefined) updates.dependencies = dependencies; if (dependencies !== undefined) updates.dependencies = dependencies;
if (enabledWorkflowSteps !== undefined) updates.enabledWorkflowSteps = enabledWorkflowSteps; if (enabledWorkflowSteps !== undefined) updates.enabledWorkflowSteps = enabledWorkflowSteps;
if (hasBodyField("noCommitsExpected")) updates.noCommitsExpected = noCommitsExpected;
if (hasBodyField("modelProvider")) updates.modelProvider = validatedModelProvider; if (hasBodyField("modelProvider")) updates.modelProvider = validatedModelProvider;
if (hasBodyField("modelId")) updates.modelId = validatedModelId; if (hasBodyField("modelId")) updates.modelId = validatedModelId;
if (hasBodyField("validatorModelProvider")) updates.validatorModelProvider = validatedValidatorModelProvider; if (hasBodyField("validatorModelProvider")) updates.validatorModelProvider = validatedValidatorModelProvider;
@@ -1772,7 +1777,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
if (err instanceof ApiError) { if (err instanceof ApiError) {
throw err; throw err;
} }
const status = (err instanceof Error ? err.message : String(err)).includes("must be a string") || (err instanceof Error ? err.message : String(err)).includes("must be a non-empty string") || (err instanceof Error ? err.message : String(err)).includes("must be a string or null") || (err instanceof Error ? err.message : String(err)).includes("must be an array of strings") || (err instanceof Error ? err.message : String(err)).includes("thinkingLevel must be one of") || (err instanceof Error ? err.message : String(err)).includes("reviewLevel must be an integer") || (err instanceof Error ? err.message : String(err)).includes("executionMode must be one of") || (err instanceof Error ? err.message : String(err)).includes("priority must be one of") || (err instanceof Error ? err.message : String(err)).includes("sourceIssue") ? 400 : 500; const status = (err instanceof Error ? err.message : String(err)).includes("must be a string") || (err instanceof Error ? err.message : String(err)).includes("must be a non-empty string") || (err instanceof Error ? err.message : String(err)).includes("must be a string or null") || (err instanceof Error ? err.message : String(err)).includes("must be an array of strings") || (err instanceof Error ? err.message : String(err)).includes("must be a boolean") || (err instanceof Error ? err.message : String(err)).includes("thinkingLevel must be one of") || (err instanceof Error ? err.message : String(err)).includes("reviewLevel must be an integer") || (err instanceof Error ? err.message : String(err)).includes("executionMode must be one of") || (err instanceof Error ? err.message : String(err)).includes("priority must be one of") || (err instanceof Error ? err.message : String(err)).includes("sourceIssue") ? 400 : 500;
throw new ApiError(status, err instanceof Error ? err.message : String(err)); throw new ApiError(status, err instanceof Error ? err.message : String(err));
} }
}); });