feat(KB-622): unify steeringComments and comments into single comments field

- Merge steeringComments and comments into unified comments field in Task type
- Update TaskStore to use single comments array instead of separate steeringComments
- Add database migration to convert existing steeringComments to comments
- Update executor to inject all comments into AI execution context
- Update dashboard SteeringTab to use unified comments API
- Update CLI task steer command to use comments field
- Update PR comment handler to add comments via unified API
This commit is contained in:
gsxdsm
2026-04-01 07:05:23 -07:00
parent 8f00302fb6
commit 02a30a9e96
22 changed files with 294 additions and 260 deletions

View File

@@ -541,7 +541,8 @@ describe("migrateFromLegacy", () => {
expect(JSON.parse(row.steps)).toHaveLength(2);
expect(JSON.parse(row.log)).toHaveLength(1);
expect(JSON.parse(row.attachments)).toHaveLength(1);
expect(JSON.parse(row.steeringComments)).toHaveLength(1);
// steeringComments should be merged into comments
expect(JSON.parse(row.comments)).toHaveLength(1); // 1 steering comment migrated
expect(JSON.parse(row.workflowStepResults)).toHaveLength(1);
expect(JSON.parse(row.prInfo).number).toBe(1);
expect(JSON.parse(row.issueInfo).number).toBe(10);

View File

@@ -165,12 +165,12 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
worktree, blockedBy, paused, baseBranch, modelPresetId, modelProvider,
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
dependencies, steps, log, attachments, steeringComments,
dependencies, steps, log, attachments,
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
breakIntoSubtasks, enabledWorkflowSteps
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
`);
@@ -184,6 +184,17 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
const raw = await readFile(taskJsonPath, "utf-8");
const task: Task = JSON.parse(raw);
// Merge steeringComments into comments (unified comments field)
const existingComments = task.comments || [];
const steeringComments = (task as any).steeringComments || [];
const mergedComments = [...existingComments, ...steeringComments.map((sc: any) => ({
id: sc.id,
text: sc.text,
author: sc.author,
createdAt: sc.createdAt,
updatedAt: sc.createdAt, // Steering comments didn't have updatedAt
}))];
insertStmt.run(
task.id,
task.title ?? null,
@@ -213,8 +224,7 @@ async function migrateTasks(kbDir: string, db: Database): Promise<void> {
toJson(task.steps || []),
toJson(task.log || []),
toJson(task.attachments || []),
toJson(task.steeringComments || []),
toJson(task.comments || []),
toJson(mergedComments),
toJson(task.workflowStepResults || []),
toJsonNullable(task.prInfo),
toJsonNullable(task.issueInfo),

View File

@@ -86,7 +86,7 @@ describe("Database", () => {
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(4);
expect(db.getSchemaVersion()).toBe(5);
});
it("seeds lastModified", () => {
@@ -109,7 +109,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(4);
expect(db.getSchemaVersion()).toBe(5);
});
it("does not overwrite existing config on re-init", () => {
@@ -411,7 +411,7 @@ describe("Database", () => {
steps: JSON.stringify([{ name: "Step 1", status: "done" }, { name: "Step 2", status: "in-progress" }]),
log: JSON.stringify([{ timestamp: now, action: "Created" }]),
attachments: JSON.stringify([{ filename: "test.png", originalName: "test.png", mimeType: "image/png", size: 1024, createdAt: now }]),
steeringComments: JSON.stringify([{ id: "c1", text: "Do this", createdAt: now, author: "user" }]),
comments: JSON.stringify([{ id: "c1", text: "Do this", createdAt: now, author: "user" }]),
workflowStepResults: JSON.stringify([{ workflowStepId: "WS-001", workflowStepName: "QA", status: "passed" }]),
prInfo: JSON.stringify({ url: "https://github.com/test/pr/1", number: 1, status: "open", title: "PR", headBranch: "feature", baseBranch: "main", commentCount: 0 }),
issueInfo: JSON.stringify({ url: "https://github.com/test/issues/1", number: 1, state: "open", title: "Issue" }),
@@ -425,7 +425,7 @@ describe("Database", () => {
worktree, blockedBy, paused, baseBranch, modelPresetId, modelProvider,
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
dependencies, steps, log, attachments, steeringComments,
dependencies, steps, log, attachments, comments,
workflowStepResults, prInfo, issueInfo, breakIntoSubtasks,
enabledWorkflowSteps
) VALUES (
@@ -440,7 +440,7 @@ describe("Database", () => {
task.validatorModelId, task.mergeRetries, task.error, task.summary,
task.thinkingLevel, task.createdAt, task.updatedAt, task.columnMovedAt,
task.dependencies, task.steps, task.log, task.attachments,
task.steeringComments, task.workflowStepResults, task.prInfo,
task.comments, task.workflowStepResults, task.prInfo,
task.issueInfo, task.breakIntoSubtasks, task.enabledWorkflowSteps,
);
@@ -458,7 +458,7 @@ describe("Database", () => {
expect(JSON.parse(row.steps)).toHaveLength(2);
expect(JSON.parse(row.log)).toHaveLength(1);
expect(JSON.parse(row.attachments)).toHaveLength(1);
expect(JSON.parse(row.steeringComments)).toHaveLength(1);
expect(JSON.parse(row.comments)).toHaveLength(1);
expect(JSON.parse(row.workflowStepResults)).toHaveLength(1);
expect(JSON.parse(row.prInfo).number).toBe(1);
expect(JSON.parse(row.issueInfo).state).toBe("open");
@@ -684,7 +684,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 4 (includes v1→v2, v2→v3, and v3→v4 migrations)
expect(db.getSchemaVersion()).toBe(4);
expect(db.getSchemaVersion()).toBe(5);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -709,11 +709,11 @@ describe("schema migrations", () => {
const db = new Database(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(4);
expect(db.getSchemaVersion()).toBe(5);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(4);
expect(db.getSchemaVersion()).toBe(5);
db.close();
});
@@ -808,7 +808,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 4
expect(db.getSchemaVersion()).toBe(4);
expect(db.getSchemaVersion()).toBe(5);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -864,7 +864,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(4);
expect(db.getSchemaVersion()).toBe(5);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -58,7 +58,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
// ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 4;
const SCHEMA_VERSION = 5;
const SCHEMA_SQL = `
-- Tasks table with JSON columns for nested data
@@ -333,8 +333,15 @@ export class Database {
});
}
if (version < 5) {
this.applyMigration(5, () => {
// Migrate steeringComments to comments (unified comments field)
this.migrateSteeringCommentsToComments();
});
}
// Future migrations go here:
// if (version < 3) { this.applyMigration(3, () => { ... }); }
// if (version < 6) { this.applyMigration(6, () => { ... }); }
}
/**
@@ -368,6 +375,60 @@ export class Database {
}
}
/**
* Migrate steeringComments data to the unified comments field.
* This is a one-way migration from schema version 4 to 5.
*/
private migrateSteeringCommentsToComments(): void {
// Only run if steeringComments column exists
if (!this.hasColumn("tasks", "steeringComments")) {
return;
}
// Get all tasks that have steering comments
const tasksWithSteering = this.db
.prepare("SELECT id, steeringComments, comments FROM tasks WHERE steeringComments != '[]'")
.all() as Array<{ id: string; steeringComments: string; comments: string }>;
for (const task of tasksWithSteering) {
try {
const steeringComments = JSON.parse(task.steeringComments) as Array<{
id: string;
text: string;
createdAt: string;
author: "user" | "agent";
}>;
const existingComments = JSON.parse(task.comments || "[]") as Array<{
id: string;
text: string;
author: string;
createdAt: string;
updatedAt?: string;
}>;
// Convert steering comments to the unified format
const migratedComments = steeringComments.map((sc) => ({
id: sc.id,
text: sc.text,
author: sc.author,
createdAt: sc.createdAt,
updatedAt: sc.createdAt, // Steering comments didn't have updatedAt
}));
// Merge: existing comments first, then migrated steering comments
const mergedComments = [...existingComments, ...migratedComments];
// Update the task with merged comments
this.db
.prepare("UPDATE tasks SET comments = ? WHERE id = ?")
.run(JSON.stringify(mergedComments), task.id);
} catch {
// Skip tasks with invalid JSON in steeringComments
continue;
}
}
}
/**
* Close the database connection.
*/

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, 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, 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";

View File

@@ -856,18 +856,18 @@ describe("TaskStore", () => {
expect(fetched.steps[0].status).toBe("in-progress");
});
it("addSteeringComment recreates missing task directory before persisting metadata", async () => {
it("addComment recreates missing task directory before persisting metadata", async () => {
const task = await createTestTask();
const dir = await deleteTaskDir(task.id);
const updated = await store.addSteeringComment(task.id, "Please recover from missing directory");
const updated = await store.addComment(task.id, "Please recover from missing directory");
expect(updated.steeringComments).toHaveLength(1);
expect(updated.comments).toHaveLength(1);
expect(existsSync(dir)).toBe(true);
expect(existsSync(join(dir, "task.json"))).toBe(true);
const fetched = await store.getTask(task.id);
expect(fetched.steeringComments).toHaveLength(1);
expect(fetched.comments).toHaveLength(1);
});
it("appendAgentLog recreates missing task directory before writing agent.log", async () => {
@@ -1233,7 +1233,7 @@ describe("TaskStore", () => {
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();
expect(updated.comments![0].updatedAt).toBeDefined();
});
it("updates an existing task comment", async () => {
@@ -1276,68 +1276,68 @@ describe("TaskStore", () => {
);
});
it("persists task comments independently from steering comments", async () => {
it("unifies task comments and steering comments into single comments field", async () => {
const task = await createTestTask();
await store.addTaskComment(task.id, "General note", "alice");
await store.addSteeringComment(task.id, "Execution note");
await store.addComment(task.id, "Execution note");
const reopened = await store.getTask(task.id);
expect(reopened.comments).toHaveLength(1);
// Both comments should now be in the unified comments field
expect(reopened.comments).toHaveLength(2);
expect(reopened.comments![0].text).toBe("General note");
expect(reopened.steeringComments).toHaveLength(1);
expect(reopened.steeringComments![0].text).toBe("Execution note");
expect(reopened.comments![1].text).toBe("Execution note");
});
});
describe("addSteeringComment", () => {
describe("addComment", () => {
it("adds a steering comment to a task", async () => {
const task = await createTestTask();
const updated = await store.addSteeringComment(task.id, "Please handle the edge case");
const updated = await store.addComment(task.id, "Please handle the edge case");
expect(updated.steeringComments).toHaveLength(1);
expect(updated.steeringComments![0].text).toBe("Please handle the edge case");
expect(updated.steeringComments![0].author).toBe("user");
expect(updated.steeringComments![0].id).toBeDefined();
expect(updated.steeringComments![0].createdAt).toBeDefined();
expect(updated.comments).toHaveLength(1);
expect(updated.comments![0].text).toBe("Please handle the edge case");
expect(updated.comments![0].author).toBe("user");
expect(updated.comments![0].id).toBeDefined();
expect(updated.comments![0].createdAt).toBeDefined();
});
it("accepts agent as author", async () => {
const task = await createTestTask();
const updated = await store.addSteeringComment(task.id, "Note from agent", "agent");
const updated = await store.addComment(task.id, "Note from agent", "agent");
expect(updated.steeringComments).toHaveLength(1);
expect(updated.steeringComments![0].author).toBe("agent");
expect(updated.comments).toHaveLength(1);
expect(updated.comments![0].author).toBe("agent");
});
it("initializes steeringComments array if undefined", async () => {
it("initializes comments array if undefined", async () => {
const task = await createTestTask();
expect(task.steeringComments).toBeUndefined();
expect(task.comments).toBeUndefined();
const updated = await store.addSteeringComment(task.id, "First comment");
expect(updated.steeringComments).toBeDefined();
expect(updated.steeringComments).toHaveLength(1);
const updated = await store.addComment(task.id, "First comment");
expect(updated.comments).toBeDefined();
expect(updated.comments).toHaveLength(1);
});
it("appends multiple comments in order", async () => {
const task = await createTestTask();
await store.addSteeringComment(task.id, "First comment");
await store.addSteeringComment(task.id, "Second comment");
await store.addSteeringComment(task.id, "Third comment");
await store.addComment(task.id, "First comment");
await store.addComment(task.id, "Second comment");
await store.addComment(task.id, "Third comment");
const fetched = await store.getTask(task.id);
expect(fetched.steeringComments).toHaveLength(3);
expect(fetched.steeringComments![0].text).toBe("First comment");
expect(fetched.steeringComments![1].text).toBe("Second comment");
expect(fetched.steeringComments![2].text).toBe("Third comment");
expect(fetched.comments).toHaveLength(3);
expect(fetched.comments![0].text).toBe("First comment");
expect(fetched.comments![1].text).toBe("Second comment");
expect(fetched.comments![2].text).toBe("Third comment");
});
it("generates unique IDs for each comment", async () => {
const task = await createTestTask();
const updated1 = await store.addSteeringComment(task.id, "Comment 1");
const updated2 = await store.addSteeringComment(task.id, "Comment 2");
const updated1 = await store.addComment(task.id, "Comment 1");
const updated2 = await store.addComment(task.id, "Comment 2");
const id1 = updated1.steeringComments![0].id;
const id2 = updated2.steeringComments![1].id;
const id1 = updated1.comments![0].id;
const id2 = updated2.comments![1].id;
expect(id1).not.toBe(id2);
});
@@ -1346,28 +1346,28 @@ describe("TaskStore", () => {
const events: any[] = [];
store.on("task:updated", (t) => events.push(t));
await store.addSteeringComment(task.id, "Test comment");
await store.addComment(task.id, "Test comment");
expect(events).toHaveLength(1);
expect(events[0].steeringComments).toHaveLength(1);
expect(events[0].steeringComments![0].text).toBe("Test comment");
expect(events[0].comments).toHaveLength(1);
expect(events[0].comments![0].text).toBe("Test comment");
});
it("persists to disk and round-trips correctly", async () => {
const task = await createTestTask();
await store.addSteeringComment(task.id, "Persisted comment");
await store.addComment(task.id, "Persisted comment");
const fetched = await store.getTask(task.id);
expect(fetched.steeringComments).toHaveLength(1);
expect(fetched.steeringComments![0].text).toBe("Persisted comment");
expect(fetched.steeringComments![0].author).toBe("user");
expect(fetched.comments).toHaveLength(1);
expect(fetched.comments![0].text).toBe("Persisted comment");
expect(fetched.comments![0].author).toBe("user");
});
it("adds log entry for the action", async () => {
const task = await createTestTask();
const updated = await store.addSteeringComment(task.id, "Comment with log");
const updated = await store.addComment(task.id, "Comment with log");
expect(updated.log.some((l) => l.action === "Steering comment added")).toBe(true);
expect(updated.log.some((l) => l.action === "Comment added")).toBe(true);
expect(updated.log.some((l) => l.outcome === "by user")).toBe(true);
});
@@ -1376,7 +1376,7 @@ describe("TaskStore", () => {
const before = task.updatedAt;
await new Promise((r) => setTimeout(r, 10)); // Ensure time passes
const updated = await store.addSteeringComment(task.id, "Timestamp test");
const updated = await store.addComment(task.id, "Timestamp test");
expect(updated.updatedAt).not.toBe(before);
});
@@ -1389,7 +1389,7 @@ describe("TaskStore", () => {
const allTasksBefore = await store.listTasks();
await store.addSteeringComment(task.id, "Need to fix edge case");
await store.addComment(task.id, "Need to fix edge case");
const allTasksAfter = await store.listTasks();
expect(allTasksAfter).toHaveLength(allTasksBefore.length + 1);
@@ -1406,7 +1406,7 @@ describe("TaskStore", () => {
const allTasksBefore = await store.listTasks();
await store.addSteeringComment(task.id, "Some feedback");
await store.addComment(task.id, "Some feedback");
const allTasksAfter = await store.listTasks();
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
@@ -1419,7 +1419,7 @@ describe("TaskStore", () => {
const allTasksBefore = await store.listTasks();
await store.addSteeringComment(task.id, "Some feedback");
await store.addComment(task.id, "Some feedback");
const allTasksAfter = await store.listTasks();
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
@@ -1433,7 +1433,7 @@ describe("TaskStore", () => {
const allTasksBefore = await store.listTasks();
await store.addSteeringComment(task.id, "Some feedback");
await store.addComment(task.id, "Some feedback");
const allTasksAfter = await store.listTasks();
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
@@ -1446,10 +1446,10 @@ describe("TaskStore", () => {
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");
const updated = await store.addComment(task.id, "Need to fix edge case");
expect(updated.steeringComments).toHaveLength(1);
expect(updated.steeringComments![0].text).toBe("Need to fix edge case");
expect(updated.comments).toHaveLength(1);
expect(updated.comments![0].text).toBe("Need to fix edge case");
});
it("refinement task has correct dependency on original done task", async () => {
@@ -1459,7 +1459,7 @@ describe("TaskStore", () => {
await store.moveTask(task.id, "in-review");
await store.moveTask(task.id, "done");
await store.addSteeringComment(task.id, "Need to fix edge case");
await store.addComment(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));
@@ -1477,7 +1477,7 @@ describe("TaskStore", () => {
const allTasksBefore = await store.listTasks();
await store.addSteeringComment(task.id, "Agent feedback", "agent");
await store.addComment(task.id, "Agent feedback", "agent");
const allTasksAfter = await store.listTasks();
expect(allTasksAfter).toHaveLength(allTasksBefore.length);
@@ -1491,10 +1491,10 @@ describe("TaskStore", () => {
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, " ");
const updated = await store.addComment(task.id, " ");
expect(updated.steeringComments).toHaveLength(1);
expect(updated.steeringComments![0].text).toBe(" ");
expect(updated.comments).toHaveLength(1);
expect(updated.comments![0].text).toBe(" ");
});
});
@@ -1504,7 +1504,7 @@ describe("TaskStore", () => {
const reopened = await store.getTask(task.id);
expect(reopened.comments).toBeUndefined();
expect(reopened.steeringComments).toBeUndefined();
expect(reopened.comments).toBeUndefined();
});
it("supports the task comment and merge details shapes", async () => {
@@ -2240,11 +2240,11 @@ describe("TaskStore", () => {
it("does NOT copy steering comments", async () => {
const task = await store.createTask({ description: "Test task" });
await store.addSteeringComment(task.id, "Test comment");
await store.addComment(task.id, "Test comment");
const duplicated = await store.duplicateTask(task.id);
expect(duplicated.steeringComments).toBeUndefined();
expect(duplicated.comments).toBeUndefined();
});
it("emits task:created event", async () => {

View File

@@ -150,7 +150,6 @@ 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 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),
@@ -172,12 +171,12 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
worktree, blockedBy, paused, baseBranch, baseCommitSha, modelPresetId, modelProvider,
modelId, validatorModelProvider, validatorModelId, mergeRetries, error,
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
dependencies, steps, log, attachments, steeringComments,
dependencies, steps, log, attachments,
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
`).run(
task.id,
@@ -209,7 +208,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
toJson(task.steps || []),
toJson(task.log || []),
toJson(task.attachments || []),
toJson(task.steeringComments || []),
toJson(task.comments || []),
toJson(task.workflowStepResults || []),
toJsonNullable(task.prInfo),
@@ -691,7 +689,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
createdAt: now,
updatedAt: now,
// Explicitly NOT copied: worktree, status, blockedBy, paused, baseBranch,
// attachments, steeringComments, prInfo, agent logs, size, reviewLevel
// attachments, comments, prInfo, agent logs, size, reviewLevel
};
const newDir = this.taskDir(newId);
@@ -1936,38 +1934,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
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;
});
// Delegate to unified addComment method
return this.addComment(id, text, author);
}
async updateTaskComment(id: string, commentId: string, text: string): Promise<Task> {
@@ -2025,17 +1993,17 @@ 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,
* Add a comment to a task.
* Comments are injected into the AI execution context.
* When a 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(
async addComment(
id: string,
text: string,
author: "user" | "agent" = "user",
author: string = "user",
): Promise<Task> {
// Phase 1: Add steering comment under lock
// Phase 1: Add comment under lock
const task = await this.withTaskLock(id, async () => {
const dir = this.taskDir(id);
const task = await this.readTaskJson(dir);
@@ -2048,21 +2016,22 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Generate unique ID: timestamp + random suffix for collision resistance
const commentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const comment: import("./types.js").SteeringComment = {
const comment: import("./types.js").TaskComment = {
id: commentId,
text,
createdAt: new Date().toISOString(),
author,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
if (!task.steeringComments) {
task.steeringComments = [];
if (!task.comments) {
task.comments = [];
}
task.steeringComments.push(comment);
task.comments.push(comment);
task.updatedAt = new Date().toISOString();
task.log.push({
timestamp: task.updatedAt,
action: "Steering comment added",
action: "Comment added",
outcome: `by ${author}`,
});
@@ -2080,7 +2049,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
await this.refineTask(id, text);
} catch {
// Silently ignore - refinement is best-effort and shouldn't fail
// the steering comment addition. refineTask already validates
// the comment addition. refineTask already validates
// feedback text, so empty/whitespace comments won't create refinements.
}
}
@@ -2374,7 +2343,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
validatorModelId: entry.validatorModelId,
breakIntoSubtasks: entry.breakIntoSubtasks,
modifiedFiles: entry.modifiedFiles,
// Intentionally NOT restoring: worktree, status, blockedBy, paused, baseBranch, baseCommitSha, error, steeringComments
// Intentionally NOT restoring: worktree, status, blockedBy, paused, baseBranch, baseCommitSha, error, comments
};
// Write task.json

View File

@@ -327,13 +327,6 @@ export interface TaskAttachment {
createdAt: string;
}
export interface SteeringComment {
id: string;
text: string;
createdAt: string;
author: "user" | "agent";
}
export interface TaskComment {
id: string;
text: string;
@@ -392,7 +385,6 @@ export interface Task {
* Set by the executor when creating the worktree. */
baseCommitSha?: string;
attachments?: TaskAttachment[];
steeringComments?: SteeringComment[];
comments?: TaskComment[];
/** PR information for tasks linked to GitHub pull requests */
prInfo?: PrInfo;