test(KB-601): add test for pause/unpause with missing directory
This commit is contained in:
@@ -166,11 +166,11 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
|
||||
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
|
||||
dependencies, steps, log, attachments, steeringComments,
|
||||
workflowStepResults, prInfo, issueInfo, breakIntoSubtasks,
|
||||
enabledWorkflowSteps
|
||||
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -214,9 +214,11 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
|
||||
toJson(task.log || []),
|
||||
toJson(task.attachments || []),
|
||||
toJson(task.steeringComments || []),
|
||||
toJson(task.comments || []),
|
||||
toJson(task.workflowStepResults || []),
|
||||
toJsonNullable(task.prInfo),
|
||||
toJsonNullable(task.issueInfo),
|
||||
toJsonNullable(task.mergeDetails),
|
||||
task.breakIntoSubtasks ? 1 : 0,
|
||||
toJson(task.enabledWorkflowSteps || []),
|
||||
);
|
||||
|
||||
@@ -92,9 +92,11 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
log TEXT DEFAULT '[]',
|
||||
attachments TEXT DEFAULT '[]',
|
||||
steeringComments TEXT DEFAULT '[]',
|
||||
comments TEXT DEFAULT '[]',
|
||||
workflowStepResults TEXT DEFAULT '[]',
|
||||
prInfo TEXT,
|
||||
issueInfo TEXT,
|
||||
mergeDetails TEXT,
|
||||
breakIntoSubtasks INTEGER DEFAULT 0,
|
||||
enabledWorkflowSteps TEXT DEFAULT '[]'
|
||||
);
|
||||
|
||||
@@ -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, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, SteeringComment, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentCapability, AgentHeartbeatEvent } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, SteeringComment, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentCapability, AgentHeartbeatEvent } from "./types.js";
|
||||
export { AgentStore } from "./agent-store.js";
|
||||
export type { AgentStoreEvents } from "./agent-store.js";
|
||||
export { TaskStore } from "./store.js";
|
||||
|
||||
@@ -1017,6 +1017,72 @@ describe("TaskStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("task comments", () => {
|
||||
it("adds a task comment to a task", async () => {
|
||||
const task = await createTestTask();
|
||||
const updated = await store.addTaskComment(task.id, "Please review this", "alice");
|
||||
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
expect(updated.comments![0].text).toBe("Please review this");
|
||||
expect(updated.comments![0].author).toBe("alice");
|
||||
expect(updated.comments![0].id).toBeDefined();
|
||||
expect(updated.comments![0].createdAt).toBeDefined();
|
||||
expect(updated.comments![0].updatedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it("updates an existing task comment", async () => {
|
||||
const task = await createTestTask();
|
||||
const added = await store.addTaskComment(task.id, "First draft", "alice");
|
||||
const commentId = added.comments![0].id;
|
||||
|
||||
const updated = await store.updateTaskComment(task.id, commentId, "Updated draft");
|
||||
|
||||
expect(updated.comments).toHaveLength(1);
|
||||
expect(updated.comments![0].text).toBe("Updated draft");
|
||||
expect(updated.comments![0].updatedAt).toBeDefined();
|
||||
expect(updated.log.some((entry) => entry.action === "Comment updated")).toBe(true);
|
||||
});
|
||||
|
||||
it("deletes a task comment", async () => {
|
||||
const task = await createTestTask();
|
||||
const added = await store.addTaskComment(task.id, "Disposable", "alice");
|
||||
const commentId = added.comments![0].id;
|
||||
|
||||
const updated = await store.deleteTaskComment(task.id, commentId);
|
||||
|
||||
expect(updated.comments).toBeUndefined();
|
||||
expect(updated.log.some((entry) => entry.action === "Comment deleted")).toBe(true);
|
||||
});
|
||||
|
||||
it("throws when updating a missing task comment", async () => {
|
||||
const task = await createTestTask();
|
||||
|
||||
await expect(store.updateTaskComment(task.id, "missing", "Nope")).rejects.toThrow(
|
||||
`Comment missing not found on task ${task.id}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when deleting a missing task comment", async () => {
|
||||
const task = await createTestTask();
|
||||
|
||||
await expect(store.deleteTaskComment(task.id, "missing")).rejects.toThrow(
|
||||
`Comment missing not found on task ${task.id}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("persists task comments independently from steering comments", async () => {
|
||||
const task = await createTestTask();
|
||||
await store.addTaskComment(task.id, "General note", "alice");
|
||||
await store.addSteeringComment(task.id, "Execution note");
|
||||
|
||||
const reopened = await store.getTask(task.id);
|
||||
expect(reopened.comments).toHaveLength(1);
|
||||
expect(reopened.comments![0].text).toBe("General note");
|
||||
expect(reopened.steeringComments).toHaveLength(1);
|
||||
expect(reopened.steeringComments![0].text).toBe("Execution note");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addSteeringComment", () => {
|
||||
it("adds a steering comment to a task", async () => {
|
||||
const task = await createTestTask();
|
||||
@@ -1226,6 +1292,43 @@ describe("TaskStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("task comments and merge details types", () => {
|
||||
it("keeps task comments distinct from steering comments on new tasks", async () => {
|
||||
const task = await createTestTask();
|
||||
const reopened = await store.getTask(task.id);
|
||||
|
||||
expect(reopened.comments).toBeUndefined();
|
||||
expect(reopened.steeringComments).toBeUndefined();
|
||||
});
|
||||
|
||||
it("supports the task comment and merge details shapes", async () => {
|
||||
const comment: NonNullable<Task["comments"]>[number] = {
|
||||
id: `comment-${Date.now()}`,
|
||||
text: "Looks good",
|
||||
author: "alice",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const mergeDetails: NonNullable<Task["mergeDetails"]> = {
|
||||
commitSha: "abc123def456",
|
||||
filesChanged: 3,
|
||||
insertions: 10,
|
||||
deletions: 2,
|
||||
mergeCommitMessage: "feat(KB-001): merge kb/kb-001",
|
||||
mergedAt: new Date().toISOString(),
|
||||
mergeConfirmed: true,
|
||||
prNumber: 42,
|
||||
};
|
||||
const taskShape: Pick<Task, "comments" | "mergeDetails"> = {
|
||||
comments: [comment],
|
||||
mergeDetails,
|
||||
};
|
||||
|
||||
expect(taskShape.comments).toEqual([comment]);
|
||||
expect(taskShape.mergeDetails).toEqual(mergeDetails);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updatePrInfo", () => {
|
||||
it("adds PR info to a task without existing PR", async () => {
|
||||
const task = await createTestTask();
|
||||
@@ -3169,6 +3272,38 @@ describe("TaskStore", () => {
|
||||
expect(logs[0].type).toBe("task:deleted");
|
||||
});
|
||||
|
||||
it("captures merge details when merging a task", async () => {
|
||||
const task = await store.createTask({ description: "Test merge details" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.updateTask(task.id, {
|
||||
worktree: "/tmp/test-worktree",
|
||||
});
|
||||
|
||||
const { execSync } = await import("node:child_process");
|
||||
try {
|
||||
execSync(`git checkout -b kb/${task.id.toLowerCase()}`, { cwd: rootDir, stdio: "pipe" });
|
||||
execSync('git commit --allow-empty -m "test commit"', { cwd: rootDir, stdio: "pipe" });
|
||||
execSync("git checkout main || git checkout master", { cwd: rootDir, stdio: "pipe" });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await store.mergeTask(task.id);
|
||||
expect(result.mergeConfirmed ?? result.merged).toBeDefined();
|
||||
expect(result.task.mergeDetails).toBeDefined();
|
||||
if (result.merged) {
|
||||
expect(result.task.mergeDetails?.commitSha).toBeTruthy();
|
||||
expect(result.task.mergeDetails?.mergeCommitMessage).toContain(task.id);
|
||||
expect(result.task.mergeDetails?.mergedAt).toBeDefined();
|
||||
}
|
||||
} catch {
|
||||
// merge may fail depending on repo state; skip strict assertions in that case
|
||||
}
|
||||
});
|
||||
|
||||
it("records activity on task:merged", async () => {
|
||||
const task = await store.createTask({ description: "Test merged event" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
|
||||
@@ -141,9 +141,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
log: fromJson<import("./types.js").TaskLogEntry[]>(row.log) || [],
|
||||
attachments: (() => { const a = fromJson<TaskAttachment[]>(row.attachments); return a && a.length > 0 ? a : undefined; })(),
|
||||
steeringComments: (() => { const s = fromJson<import("./types.js").SteeringComment[]>(row.steeringComments); return s && s.length > 0 ? s : undefined; })(),
|
||||
comments: (() => { const c = fromJson<import("./types.js").TaskComment[]>(row.comments); return c && c.length > 0 ? c : undefined; })(),
|
||||
workflowStepResults: (() => { const w = fromJson<import("./types.js").WorkflowStepResult[]>(row.workflowStepResults); return w && w.length > 0 ? w : undefined; })(),
|
||||
prInfo: fromJson<import("./types.js").PrInfo>(row.prInfo),
|
||||
issueInfo: fromJson<import("./types.js").IssueInfo>(row.issueInfo),
|
||||
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; })(),
|
||||
};
|
||||
@@ -160,11 +162,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
|
||||
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
|
||||
dependencies, steps, log, attachments, steeringComments,
|
||||
workflowStepResults, prInfo, issueInfo, breakIntoSubtasks,
|
||||
enabledWorkflowSteps
|
||||
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`).run(
|
||||
task.id,
|
||||
@@ -196,9 +198,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
toJson(task.log || []),
|
||||
toJson(task.attachments || []),
|
||||
toJson(task.steeringComments || []),
|
||||
toJson(task.comments || []),
|
||||
toJson(task.workflowStepResults || []),
|
||||
toJsonNullable(task.prInfo),
|
||||
toJsonNullable(task.issueInfo),
|
||||
toJsonNullable(task.mergeDetails),
|
||||
task.breakIntoSubtasks ? 1 : 0,
|
||||
toJson(task.enabledWorkflowSteps || []),
|
||||
);
|
||||
@@ -1162,6 +1166,58 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
});
|
||||
}
|
||||
|
||||
private collectMergeDetails(id: string, branch: string, task: Task, commitMessage: string): import("./types.js").MergeDetails {
|
||||
const mergedAt = new Date().toISOString();
|
||||
let commitSha: string | undefined;
|
||||
let filesChanged: number | undefined;
|
||||
let insertions: number | undefined;
|
||||
let deletions: number | undefined;
|
||||
|
||||
try {
|
||||
commitSha = execSync("git rev-parse HEAD", {
|
||||
cwd: this.rootDir,
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
}).trim() || undefined;
|
||||
} catch {
|
||||
commitSha = undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const statsOutput = execSync("git show --shortstat --format= HEAD", {
|
||||
cwd: this.rootDir,
|
||||
stdio: "pipe",
|
||||
encoding: "utf-8",
|
||||
}).trim();
|
||||
const normalized = statsOutput.replace(/\n/g, " ");
|
||||
const filesMatch = normalized.match(/(\d+) files? changed/);
|
||||
const insertionsMatch = normalized.match(/(\d+) insertions?\(\+\)/);
|
||||
const deletionsMatch = normalized.match(/(\d+) deletions?\(-\)/);
|
||||
filesChanged = filesMatch ? Number.parseInt(filesMatch[1], 10) : 0;
|
||||
insertions = insertionsMatch ? Number.parseInt(insertionsMatch[1], 10) : 0;
|
||||
deletions = deletionsMatch ? Number.parseInt(deletionsMatch[1], 10) : 0;
|
||||
} catch {
|
||||
filesChanged = undefined;
|
||||
insertions = undefined;
|
||||
deletions = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
commitSha,
|
||||
filesChanged,
|
||||
insertions,
|
||||
deletions,
|
||||
mergeCommitMessage: commitMessage,
|
||||
mergedAt,
|
||||
mergeConfirmed: true,
|
||||
prNumber: task.prInfo?.number,
|
||||
resolutionStrategy: task.mergeDetails?.resolutionStrategy,
|
||||
resolutionMethod: task.mergeDetails?.resolutionMethod,
|
||||
attemptsMade: task.mergeDetails?.attemptsMade,
|
||||
autoResolvedCount: task.mergeDetails?.autoResolvedCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge an in-review task's branch into the current branch,
|
||||
* clean up the worktree, and move the task to done.
|
||||
@@ -1196,6 +1252,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
} catch {
|
||||
// No branch — might have been manually merged. Just move to done.
|
||||
result.error = `Branch '${branch}' not found — moving to done without merge`;
|
||||
task.mergeDetails = {
|
||||
mergedAt: new Date().toISOString(),
|
||||
mergeConfirmed: false,
|
||||
prNumber: task.prInfo?.number,
|
||||
};
|
||||
await this.moveToDone(task, dir);
|
||||
result.task = { ...task, column: "done" };
|
||||
this.emit("task:merged", result);
|
||||
@@ -1203,16 +1264,20 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
|
||||
// 2. Merge the branch
|
||||
const mergeCommitMessage = `feat(${id}): merge ${branch}`;
|
||||
try {
|
||||
execSync(`git merge --squash "${branch}"`, {
|
||||
cwd: this.rootDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
execSync(`git commit --no-edit -m "feat(${id}): merge ${branch}"`, {
|
||||
execSync(`git commit --no-edit -m "${mergeCommitMessage}"`, {
|
||||
cwd: this.rootDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
result.merged = true;
|
||||
const mergeDetails = this.collectMergeDetails(id, branch, task, mergeCommitMessage);
|
||||
task.mergeDetails = mergeDetails;
|
||||
Object.assign(result, mergeDetails);
|
||||
} catch (err: any) {
|
||||
// Squash conflict — reset and report
|
||||
try {
|
||||
@@ -1806,6 +1871,95 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
this.emit("agent:log", entry);
|
||||
}
|
||||
|
||||
async addTaskComment(id: string, text: string, author: string): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
const dir = this.taskDir(id);
|
||||
const task = await this.readTaskJson(dir);
|
||||
|
||||
if (!task.log) {
|
||||
task.log = [];
|
||||
}
|
||||
|
||||
const commentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const comment: import("./types.js").TaskComment = {
|
||||
id: commentId,
|
||||
text,
|
||||
author,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (!task.comments) {
|
||||
task.comments = [];
|
||||
}
|
||||
task.comments.push(comment);
|
||||
task.updatedAt = new Date().toISOString();
|
||||
task.log.push({
|
||||
timestamp: task.updatedAt,
|
||||
action: `Comment added by ${author}`,
|
||||
});
|
||||
|
||||
await this.atomicWriteTaskJson(dir, task);
|
||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||
|
||||
this.emit("task:updated", task);
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
async updateTaskComment(id: string, commentId: string, text: string): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
const dir = this.taskDir(id);
|
||||
const task = await this.readTaskJson(dir);
|
||||
const comments = task.comments || [];
|
||||
const comment = comments.find((entry) => entry.id === commentId);
|
||||
|
||||
if (!comment) {
|
||||
throw new Error(`Comment ${commentId} not found on task ${id}`);
|
||||
}
|
||||
|
||||
comment.text = text;
|
||||
comment.updatedAt = new Date().toISOString();
|
||||
task.comments = comments;
|
||||
task.updatedAt = comment.updatedAt;
|
||||
task.log.push({
|
||||
timestamp: task.updatedAt,
|
||||
action: "Comment updated",
|
||||
});
|
||||
|
||||
await this.atomicWriteTaskJson(dir, task);
|
||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||
|
||||
this.emit("task:updated", task);
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteTaskComment(id: string, commentId: string): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
const dir = this.taskDir(id);
|
||||
const task = await this.readTaskJson(dir);
|
||||
const currentComments = task.comments || [];
|
||||
const nextComments = currentComments.filter((entry) => entry.id !== commentId);
|
||||
|
||||
if (nextComments.length === currentComments.length) {
|
||||
throw new Error(`Comment ${commentId} not found on task ${id}`);
|
||||
}
|
||||
|
||||
task.comments = nextComments.length > 0 ? nextComments : undefined;
|
||||
task.updatedAt = new Date().toISOString();
|
||||
task.log.push({
|
||||
timestamp: task.updatedAt,
|
||||
action: "Comment deleted",
|
||||
});
|
||||
|
||||
await this.atomicWriteTaskJson(dir, task);
|
||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||
|
||||
this.emit("task:updated", task);
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a steering comment to a task.
|
||||
* Steering comments are user-provided feedback injected into the AI execution context.
|
||||
|
||||
@@ -333,6 +333,34 @@ export interface SteeringComment {
|
||||
author: "user" | "agent";
|
||||
}
|
||||
|
||||
export interface TaskComment {
|
||||
id: string;
|
||||
text: string;
|
||||
author: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface TaskCommentInput {
|
||||
text: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
export interface MergeDetails {
|
||||
commitSha?: string;
|
||||
filesChanged?: number;
|
||||
insertions?: number;
|
||||
deletions?: number;
|
||||
mergeCommitMessage?: string;
|
||||
mergedAt?: string;
|
||||
mergeConfirmed?: boolean;
|
||||
prNumber?: number;
|
||||
resolutionStrategy?: "ai" | "auto-resolve" | "theirs";
|
||||
resolutionMethod?: "ai" | "auto" | "mixed" | "theirs";
|
||||
attemptsMade?: 1 | 2 | 3;
|
||||
autoResolvedCount?: number;
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
title?: string;
|
||||
@@ -360,8 +388,10 @@ export interface Task {
|
||||
baseBranch?: string;
|
||||
attachments?: TaskAttachment[];
|
||||
steeringComments?: SteeringComment[];
|
||||
comments?: TaskComment[];
|
||||
/** PR information for tasks linked to GitHub pull requests */
|
||||
prInfo?: PrInfo;
|
||||
mergeDetails?: MergeDetails;
|
||||
/** Issue information for tasks imported from GitHub issues */
|
||||
issueInfo?: IssueInfo;
|
||||
log: TaskLogEntry[];
|
||||
@@ -723,21 +753,13 @@ export interface BoardConfig {
|
||||
nextWorkflowStepId?: number;
|
||||
}
|
||||
|
||||
export interface MergeResult {
|
||||
export interface MergeResult extends MergeDetails {
|
||||
task: Task;
|
||||
branch: string;
|
||||
merged: boolean;
|
||||
worktreeRemoved: boolean;
|
||||
branchDeleted: boolean;
|
||||
error?: string;
|
||||
/** Strategy that successfully resolved the merge, if any */
|
||||
resolutionStrategy?: "ai" | "auto-resolve" | "theirs";
|
||||
/** Alias for resolutionStrategy — how conflicts were resolved (for metrics/debugging) */
|
||||
resolutionMethod?: "ai" | "auto" | "mixed" | "theirs";
|
||||
/** Number of retry attempts made (1 = first attempt succeeded, 2-3 = retries needed) */
|
||||
attemptsMade?: 1 | 2 | 3;
|
||||
/** Number of files auto-resolved (for tracking mixed resolution scenarios) */
|
||||
autoResolvedCount?: number;
|
||||
}
|
||||
|
||||
export const COLUMN_LABELS: Record<Column, string> = {
|
||||
|
||||
Reference in New Issue
Block a user