feat(FN-2602): merge fusion/fn-2602 (auto-resolved)

- feat(FN-2602): complete Step 7 — add release changeset
- test(FN-2602): complete Step 6 — align migration and schema tests
- test(FN-2602): complete Step 5 — update store status assertions
- feat(FN-2602): complete Step 4 — rename triage prompt labels
- feat(FN-2602): complete Step 3 — add status rename migration
- feat(FN-2602): complete Step 2 — rename respecify status literals
- feat(FN-2602): complete Step 1 — rename triage display labels
This commit is contained in:
Fusion
2026-04-26 12:10:21 -07:00
committed by gsxdsm
parent 9d7f58542b
commit 8097db235a
12 changed files with 57 additions and 41 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Rename status values: specifying→planning, needs-respecify→needs-replan. Display label "Triage"→"Planning". Includes DB migration for existing records.

View File

@@ -131,7 +131,7 @@ describe("Database", () => {
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
});
it("seeds lastModified", () => {
@@ -154,7 +154,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
});
it("does not overwrite existing config on re-init", () => {
@@ -761,7 +761,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -786,11 +786,11 @@ describe("schema migrations", () => {
const db = new Database(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
db.close();
});
@@ -825,7 +825,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("priority");
@@ -866,7 +866,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -935,7 +935,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -976,7 +976,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -1000,7 +1000,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -1104,7 +1104,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1473,7 +1473,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -776,7 +776,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
const db1 = createDatabase(legacyDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(46);
expect(db1.getSchemaVersion()).toBe(47);
db1.close();
// Step 2: Manually downgrade to version 32 and drop insight tables
@@ -811,7 +811,7 @@ describe("Migration: pre-33 DB upgrade", () => {
expect(tableNamesBefore).not.toContain("project_insight_runs");
// Now run init — this triggers the v32→v33 migration
db3.init();
expect(db3.getSchemaVersion()).toBe(46);
expect(db3.getSchemaVersion()).toBe(47);
// Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare(
@@ -842,12 +842,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try {
const db1 = createDatabase(testDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(46);
expect(db1.getSchemaVersion()).toBe(47);
db1.close();
const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(46);
expect(db2.getSchemaVersion()).toBe(47);
db2.close();
} finally {
rmSync(testDir, { recursive: true, force: true });

View File

@@ -2626,7 +2626,7 @@ describe("MissionStore", () => {
describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 40 after migration", () => {
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
});
it("mission_features table has loop state columns", () => {

View File

@@ -739,7 +739,7 @@ describe("RoadmapStore", () => {
describe("schema version", () => {
it("schema version is 40 after init", () => {
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
});
});

View File

@@ -465,7 +465,7 @@ describe("Run Audit", () => {
});
it("schema version is bumped to 40", () => {
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
});
});
});

View File

@@ -4895,7 +4895,7 @@ Task with acceptance criteria
priorStatus: "awaiting-approval",
phase: "addComment:awaiting-approval-invalidation",
stage: "status-update",
nextStatus: "needs-respecify",
nextStatus: "needs-replan",
runId: "run-invalidation-failure",
agentId: "agent-invalidation",
error: "status update failed",
@@ -4923,7 +4923,7 @@ Task with acceptance criteria
const persisted = await store.getTask(task.id);
expect(persisted.comments).toHaveLength(1);
expect(persisted.comments![0].text).toBe("New user feedback");
expect(persisted.status).toBe("needs-respecify");
expect(persisted.status).toBe("needs-replan");
expect(logEntrySpy).toHaveBeenCalled();
@@ -4941,7 +4941,7 @@ Task with acceptance criteria
priorStatus: "awaiting-approval",
phase: "addComment:awaiting-approval-invalidation",
stage: "post-invalidation-log-entry",
nextStatus: "needs-respecify",
nextStatus: "needs-replan",
runId: "run-post-invalidation-log-failure",
agentId: "agent-invalidation",
error: "log entry failed",
@@ -5005,7 +5005,7 @@ Task with acceptance criteria
expect(read1.steeringComments).toHaveLength(1);
// Simulate a write-back (updateTask writes via upsertTask)
await store.updateTask(task.id, { status: "specifying" });
await store.updateTask(task.id, { status: "planning" });
// Read again — should still have exactly 1 comment, not 2
const read2 = await store.getTask(task.id);
@@ -5025,7 +5025,7 @@ Task with acceptance criteria
expect(fetched.comments).toHaveLength(2);
expect(fetched.steeringComments).toHaveLength(2);
// Write back via an innocuous update
await store.updateTask(task.id, { status: "specifying" });
await store.updateTask(task.id, { status: "planning" });
}
// Final read — still exactly 2 comments
@@ -5050,7 +5050,7 @@ Task with acceptance criteria
for (let i = 0; i < 3; i++) {
const fetched = await store.getTask(task.id);
expect(fetched.comments).toHaveLength(2);
await store.updateTask(task.id, { status: "specifying" });
await store.updateTask(task.id, { status: "planning" });
}
const final = await store.getTask(task.id);
@@ -5076,7 +5076,7 @@ Task with acceptance criteria
expect(refinement).toBeDefined();
});
it("transitions awaiting-approval to needs-respecify when user comments on triage task", async () => {
it("transitions awaiting-approval to needs-replan when user comments on triage task", async () => {
const task = await store.createTask({ description: "Task in triage" });
// Keep in triage but set awaiting-approval status
await store.updateTask(task.id, { status: "awaiting-approval" });
@@ -5086,15 +5086,15 @@ Task with acceptance criteria
// Re-read the task to get the Phase 3 status update
const updated = await store.getTask(task.id);
// Task should remain in triage but status should change to needs-respecify
// Task should remain in triage but status should change to needs-replan
expect(updated.column).toBe("triage");
expect(updated.status).toBe("needs-respecify");
expect(updated.status).toBe("needs-replan");
// Comment should still be added
expect(updated.comments).toHaveLength(1);
expect(updated.comments![0].text).toBe("I want to change the approach");
});
it("does NOT transition to needs-respecify when agent comments on awaiting-approval task", async () => {
it("does NOT transition to needs-replan when agent comments on awaiting-approval task", async () => {
const task = await store.createTask({ description: "Task in triage" });
await store.updateTask(task.id, { status: "awaiting-approval" });
@@ -5106,7 +5106,7 @@ Task with acceptance criteria
expect(updated.comments).toHaveLength(1);
});
it("does NOT transition to needs-respecify when user comments on non-awaiting-approval triage task", async () => {
it("does NOT transition to needs-replan when user comments on non-awaiting-approval triage task", async () => {
const task = await store.createTask({ description: "Task in triage" });
// Task is in triage with no status (not awaiting-approval)
expect(task.status).toBeUndefined();

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(46);
expect(db.getSchemaVersion()).toBe(47);
const index = db
.prepare(

View File

@@ -86,7 +86,7 @@ export function probeFts5(db: DatabaseSync): boolean {
// ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 46;
const SCHEMA_VERSION = 47;
function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined,
@@ -1812,6 +1812,17 @@ export class Database {
});
}
// Status value rename (FN-2602)
// Rename stored status strings: specifying→planning, needs-respecify→needs-replan
if (version < 47) {
this.applyMigration(47, () => {
if (this.hasTable("tasks") && this.hasColumn("tasks", "status")) {
this.db.exec("UPDATE tasks SET status = 'planning' WHERE status = 'specifying'");
this.db.exec("UPDATE tasks SET status = 'needs-replan' WHERE status = 'needs-respecify'");
}
});
}
}
/**

View File

@@ -132,18 +132,18 @@ Call \`task_done()\` to signal completion.
},
"triage-welcome": {
key: "triage-welcome",
name: "Triage Welcome",
name: "Planning Welcome",
roles: ["triage"],
description: "Introductory section for the triage/specification agent",
description: "Introductory section for the planning agent",
defaultContent: `You are a task specification agent for "fn", an AI-orchestrated task board.
Your job: take a rough task description and produce a fully specified PROMPT.md that another AI agent can execute autonomously in a fresh context with zero memory of this conversation.`,
},
"triage-context": {
key: "triage-context",
name: "Triage Context",
name: "Planning Context",
roles: ["triage"],
description: "Context-gathering instructions for triage",
description: "Context-gathering instructions for planning",
defaultContent: `## What you receive
- A raw task title and optional description (the user's rough idea)
- Access to the project's files so you can understand context`,

View File

@@ -4357,7 +4357,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
let invalidatedStatus = false;
try {
await this.updateTask(id, {
status: "needs-respecify",
status: "needs-replan",
});
invalidatedStatus = true;
} catch (err) {
@@ -4365,7 +4365,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
...commentContextBase,
phase: "addComment:awaiting-approval-invalidation",
stage: "status-update",
nextStatus: "needs-respecify",
nextStatus: "needs-replan",
error: err instanceof Error ? err.message : String(err),
});
}
@@ -4383,7 +4383,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
...commentContextBase,
phase: "addComment:awaiting-approval-invalidation",
stage: "post-invalidation-log-entry",
nextStatus: "needs-respecify",
nextStatus: "needs-replan",
error: err instanceof Error ? err.message : String(err),
});
}

View File

@@ -1697,7 +1697,7 @@ export interface MergeResult extends MergeDetails {
}
export const COLUMN_LABELS: Record<Column, string> = {
triage: "Triage",
triage: "Planning",
todo: "Todo",
"in-progress": "In Progress",
"in-review": "In Review",
@@ -1706,7 +1706,7 @@ export const COLUMN_LABELS: Record<Column, string> = {
};
export const COLUMN_DESCRIPTIONS: Record<Column, string> = {
triage: "Raw ideas — AI will specify these",
triage: "Raw ideas — AI will plan these",
todo: "Specified and ready to start",
"in-progress": "AI is working on this in a worktree",
"in-review": "Complete — ready to merge",