feat(KB-297): auto-create refinement tasks from steering comments on done tasks
- Add logic to auto-create refinement task when steering comment is added to done task - Add 117 lines of unit tests covering auto-refinement trigger conditions - Include changeset for minor version bump
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
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, WorkflowStepTemplate } 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, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_STATES, AGENT_VALID_TRANSITIONS } 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, AgentState, AgentCapability, Agent, AgentDetail, AgentHeartbeatEvent, AgentHeartbeatRun, AgentCreateInput, AgentUpdateInput } from "./types.js";
|
||||
export { TaskStore } from "./store.js";
|
||||
export { GlobalSettingsStore } from "./global-settings.js";
|
||||
export { AgentStore } from "./agent-store.js";
|
||||
export type { AgentStoreEvents } from "./agent-store.js";
|
||||
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";
|
||||
export {
|
||||
isGhAvailable,
|
||||
|
||||
@@ -1107,6 +1107,123 @@ describe("TaskStore", () => {
|
||||
const updated = await store.addSteeringComment(task.id, "Timestamp test");
|
||||
expect(updated.updatedAt).not.toBe(before);
|
||||
});
|
||||
|
||||
it("creates refinement task when steering comment added to done task", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Need to fix edge case");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length + 1);
|
||||
|
||||
const refinement = allTasksAfter.find((t) => t.id !== task.id && t.title?.includes("Refinement"));
|
||||
expect(refinement).toBeDefined();
|
||||
expect(refinement?.column).toBe("triage");
|
||||
expect(refinement?.dependencies).toContain(task.id);
|
||||
});
|
||||
|
||||
it("does not create refinement when steering comment added to non-done task (triage)", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
// Task starts in triage
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Some feedback");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
});
|
||||
|
||||
it("does not create refinement when steering comment added to non-done task (in-progress)", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Some feedback");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
});
|
||||
|
||||
it("does not create refinement when steering comment added to non-done task (in-review)", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Some feedback");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
});
|
||||
|
||||
it("steering comment is still added to original task even when refinement is created", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
const updated = await store.addSteeringComment(task.id, "Need to fix edge case");
|
||||
|
||||
expect(updated.steeringComments).toHaveLength(1);
|
||||
expect(updated.steeringComments![0].text).toBe("Need to fix edge case");
|
||||
});
|
||||
|
||||
it("refinement task has correct dependency on original done task", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
await store.addSteeringComment(task.id, "Need to fix edge case");
|
||||
|
||||
const allTasks = await store.listTasks();
|
||||
const refinement = allTasks.find((t) => t.id !== task.id && t.dependencies?.includes(task.id));
|
||||
|
||||
expect(refinement).toBeDefined();
|
||||
expect(refinement?.dependencies).toEqual([task.id]);
|
||||
});
|
||||
|
||||
it("does not create refinement for agent-authored comments", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
const allTasksBefore = await store.listTasks();
|
||||
|
||||
await store.addSteeringComment(task.id, "Agent feedback", "agent");
|
||||
|
||||
const allTasksAfter = await store.listTasks();
|
||||
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
|
||||
});
|
||||
|
||||
it("does not fail when steering comment is empty or whitespace on done task", async () => {
|
||||
const task = await store.createTask({ description: "Original task" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.moveTask(task.id, "done");
|
||||
|
||||
// Should not throw - refineTask will reject empty feedback but we catch it
|
||||
const updated = await store.addSteeringComment(task.id, " ");
|
||||
|
||||
expect(updated.steeringComments).toHaveLength(1);
|
||||
expect(updated.steeringComments![0].text).toBe(" ");
|
||||
});
|
||||
});
|
||||
|
||||
describe("updatePrInfo", () => {
|
||||
|
||||
@@ -1557,13 +1557,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
/**
|
||||
* Add a steering comment to a task.
|
||||
* Steering comments are user-provided feedback injected into the AI execution context.
|
||||
* When a steering comment is added to a task in the "done" column by a user,
|
||||
* automatically creates a refinement task with the comment text as feedback.
|
||||
*/
|
||||
async addSteeringComment(
|
||||
id: string,
|
||||
text: string,
|
||||
author: "user" | "agent" = "user",
|
||||
): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
// Phase 1: Add steering comment under lock
|
||||
const task = await this.withTaskLock(id, async () => {
|
||||
const dir = this.taskDir(id);
|
||||
const task = await this.readTaskJson(dir);
|
||||
|
||||
@@ -1599,6 +1602,20 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
this.emit("task:updated", task);
|
||||
return task;
|
||||
});
|
||||
|
||||
// Phase 2: Auto-refinement OUTSIDE the lock (to avoid lock contention)
|
||||
// Only create refinement for user comments on done tasks
|
||||
if (task.column === "done" && author === "user") {
|
||||
try {
|
||||
await this.refineTask(id, text);
|
||||
} catch {
|
||||
// Silently ignore - refinement is best-effort and shouldn't fail
|
||||
// the steering comment addition. refineTask already validates
|
||||
// feedback text, so empty/whitespace comments won't create refinements.
|
||||
}
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user