fix(FN-710): fix addSteeringComment to persist comments and correct type usage

- Fix addSteeringComment to skip auto-refinement and write directly to steeringComments array
- Update formatCommentForInjection to accept SteeringComment type instead of raw string
- Export SteeringComment type from @fusion/core for executor usage
- Add tests verifying steering comments are persisted to task store
- Fix executor import to use exported SteeringComment type
This commit is contained in:
gsxdsm
2026-04-02 14:17:19 -07:00
parent 0258c21ff6
commit bf12c22efb
4 changed files with 100 additions and 6 deletions

View File

@@ -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, TaskComment, TaskCommentInput, TaskCreateInput, TaskDetail, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentCapability, AgentHeartbeatEvent, AgentHeartbeatRun, NtfyNotificationEvent } 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, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentCapability, AgentHeartbeatEvent, AgentHeartbeatRun, NtfyNotificationEvent, SteeringComment } from "./types.js";
export { AgentStore } from "./agent-store.js";
export type { AgentStoreEvents } from "./agent-store.js";
export { TaskStore } from "./store.js";

View File

@@ -1934,6 +1934,65 @@ Task with acceptance criteria
expect(updated.comments).toHaveLength(1);
expect(updated.comments![0].text).toBe(" ");
});
it("addSteeringComment on done task does NOT create a refinement 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, "Please handle the edge case");
const allTasksAfter = await store.listTasks();
// No refinement task should be created
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
});
it("addSteeringComment writes to both comments and steeringComments", async () => {
const task = await createTestTask();
const updated = await store.addSteeringComment(task.id, "Focus on error handling");
// Should appear in unified comments (for UI display)
expect(updated.comments).toBeDefined();
expect(updated.comments!.some(c => c.text === "Focus on error handling")).toBe(true);
// Should appear in steeringComments (for executor injection)
expect(updated.steeringComments).toBeDefined();
expect(updated.steeringComments!.some(c => c.text === "Focus on error handling")).toBe(true);
});
it("addSteeringComment steeringComments persist through round-trip", async () => {
const task = await createTestTask();
await store.addSteeringComment(task.id, "Focus on error handling");
const fetched = await store.getTask(task.id);
expect(fetched.steeringComments).toBeDefined();
expect(fetched.steeringComments!).toHaveLength(1);
expect(fetched.steeringComments![0].text).toBe("Focus on error handling");
});
it("regular addComment on done task still creates refinement", 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.addComment(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();
});
});
describe("task comments and merge details types", () => {

View File

@@ -183,6 +183,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
steps: fromJson<import("./types.js").TaskStep[]>(row.steps) || [],
log: fromJson<import("./types.js").TaskLogEntry[]>(row.log) || [],
attachments: (() => { const a = fromJson<TaskAttachment[]>(row.attachments); return a && a.length > 0 ? a : undefined; })(),
steeringComments: (() => {
const sc = fromJson<import("./types.js").SteeringComment[]>(row.steeringComments);
return sc && sc.length > 0 ? sc : undefined;
})(),
comments: (() => {
// Merge legacy steeringComments and comments into unified comments field
const legacySteering = fromJson<Array<{ id: string; text: string; createdAt: string; author: string }>>(row.steeringComments) || [];
@@ -2023,11 +2027,40 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/**
* Add a steering comment to a task.
* Steering comments are injected into the AI execution context.
* @deprecated Use addComment instead - comments are now unified
* They are stored in BOTH `comments` (for unified UI display) and
* `steeringComments` (for executor real-time injection).
* Unlike regular comments, steering comments never trigger auto-refinement.
*/
async addSteeringComment(id: string, text: string, author: "user" | "agent" = "user"): Promise<Task> {
// Delegates to addComment for unified comment storage
return this.addComment(id, text, author);
// Write to unified comments (skip refinement steering is for agent injection, not follow-up tasks)
const task = await this.addComment(id, text, author, { skipRefinement: true });
// Also write to steeringComments so the executor's real-time injection listener can detect new entries
const updated = await this.withTaskLock(id, async () => {
const dir = this.taskDir(id);
const currentTask = await this.readTaskJson(dir);
const steeringComment: import("./types.js").SteeringComment = {
id: task.comments![task.comments!.length - 1].id,
text,
createdAt: new Date().toISOString(),
author,
};
if (!currentTask.steeringComments) {
currentTask.steeringComments = [];
}
currentTask.steeringComments.push(steeringComment);
currentTask.updatedAt = new Date().toISOString();
await this.atomicWriteTaskJson(dir, currentTask);
if (this.watcher) this.taskCache.set(id, { ...currentTask });
this.emit("task:updated", currentTask);
return currentTask;
});
return updated;
}
async updateTaskComment(id: string, commentId: string, text: string): Promise<Task> {
@@ -2096,6 +2129,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
id: string,
text: string,
author: string = "user",
options?: { skipRefinement?: boolean },
): Promise<Task> {
// Phase 1: Add comment under lock
const task = await this.withTaskLock(id, async () => {
@@ -2137,7 +2171,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// 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") {
// Steering comments skip refinement — they are injected into the agent stream instead
if (task.column === "done" && author === "user" && !options?.skipRefinement) {
try {
await this.refineTask(id, text);
} catch {

View File

@@ -1941,7 +1941,7 @@ Treat a non-zero exit code as a blocking failure. Do not claim success without a
* Format a comment for injection into a running agent session.
* Used for real-time steering during task execution.
*/
function formatCommentForInjection(comment: import("@fusion/core").TaskComment): string {
function formatCommentForInjection(comment: import("@fusion/core").SteeringComment): string {
const timestamp = formatTimestamp(comment.createdAt);
return `📣 **New feedback** — ${timestamp} (${comment.author}):\n\n${comment.text}\n\nPlease adjust your approach based on this feedback.`;
}