feat(FN-965): add per-task planning model override

- Add planningModelProvider and planningModelId fields to core Task type
- Update backend API validation to accept planning model fields on create/update
- Update frontend API client and TaskForm to handle planning model
- Add planning model selector row to ModelSelectorTab component
- Wire up planning model in TaskDetailModal edit mode with save support
- Update AGENTS.md with planning model override documentation
- Add comprehensive tests for ModelSelectorTab and API routes
- Bump schema version from 10 to 11
This commit is contained in:
gsxdsm
2026-04-06 22:55:25 -07:00
parent 05a00c2877
commit ffbe8cf59b
12 changed files with 473 additions and 66 deletions

View File

@@ -174,6 +174,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
modelId: row.modelId || undefined,
validatorModelProvider: row.validatorModelProvider || undefined,
validatorModelId: row.validatorModelId || undefined,
planningModelProvider: row.planningModelProvider || undefined,
planningModelId: row.planningModelId || undefined,
mergeRetries: row.mergeRetries ?? undefined,
stuckKillCount: row.stuckKillCount ?? undefined,
recoveryRetryCount: row.recoveryRetryCount ?? undefined,
@@ -225,15 +227,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
INSERT OR REPLACE INTO tasks (
id, title, description, "column", status, size, reviewLevel, currentStep,
worktree, blockedBy, paused, baseBranch, branch, baseCommitSha, modelPresetId, modelProvider,
modelId, validatorModelProvider, validatorModelId, mergeRetries,
modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
stuckKillCount, recoveryRetryCount, nextRecoveryAt, error,
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
dependencies, steps, log, attachments, steeringComments,
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
`).run(
task.id,
@@ -255,6 +257,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.modelId ?? null,
task.validatorModelProvider ?? null,
task.validatorModelId ?? null,
task.planningModelProvider ?? null,
task.planningModelId ?? null,
task.mergeRetries ?? null,
task.stuckKillCount ?? 0,
task.recoveryRetryCount ?? null,
@@ -808,6 +812,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
modelId: input.modelId,
validatorModelProvider: input.validatorModelProvider,
validatorModelId: input.validatorModelId,
planningModelProvider: input.planningModelProvider,
planningModelId: input.planningModelId,
steps: [],
currentStep: 0,
log: [{ timestamp: now, action: "Task created" }],
@@ -1095,7 +1101,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[]; blockedBy?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; stuckKillCount?: number | null; recoveryRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: 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; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; blockedBy?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; stuckKillCount?: number | null; recoveryRetryCount?: 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; 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 },
): Promise<Task> {
return this.withTaskLock(id, async () => {
// Validate that task doesn't depend on itself
@@ -1203,6 +1209,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
} else if (updates.validatorModelId !== undefined) {
task.validatorModelId = updates.validatorModelId;
}
if (updates.planningModelProvider === null) {
task.planningModelProvider = undefined;
} else if (updates.planningModelProvider !== undefined) {
task.planningModelProvider = updates.planningModelProvider;
}
if (updates.planningModelId === null) {
task.planningModelId = undefined;
} else if (updates.planningModelId !== undefined) {
task.planningModelId = updates.planningModelId;
}
if (updates.error === null) {
task.error = undefined;
} else if (updates.error !== undefined) {
@@ -1821,6 +1837,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
modelId: task.modelId,
validatorModelProvider: task.validatorModelProvider,
validatorModelId: task.validatorModelId,
planningModelProvider: task.planningModelProvider,
planningModelId: task.planningModelId,
breakIntoSubtasks: task.breakIntoSubtasks,
paused: task.paused,
baseBranch: task.baseBranch,
@@ -2709,6 +2727,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
modelId: entry.modelId,
validatorModelProvider: entry.validatorModelProvider,
validatorModelId: entry.validatorModelId,
planningModelProvider: entry.planningModelProvider,
planningModelId: entry.planningModelId,
breakIntoSubtasks: entry.breakIntoSubtasks,
modifiedFiles: entry.modifiedFiles,
// Intentionally NOT restoring: worktree, status, blockedBy, paused, baseBranch, baseCommitSha, error, comments