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:
gsxdsm
2026-03-31 10:35:44 -07:00
parent be3f4e6745
commit c57d169003
4 changed files with 146 additions and 3 deletions

View File

@@ -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,

View File

@@ -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", () => {

View File

@@ -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;
}
/**