feat(FN-2981): add droid-cli auth and status routes with experimental agent

This merge lands v0.12.0 with two major features: a droid-cli provider integration adding auth routes, status endpoints, and a settings toggle hook for controlling CLI-based authentication, plus a new experimental agent onboarding modal with a create-agent form. The release also stabilizes engine st

Fusion-Task-Id: FN-2981
This commit is contained in:
Fusion
2026-05-01 09:14:10 -07:00
committed by gsxdsm
parent 113f182e25
commit 2948ffac71
22 changed files with 541 additions and 87 deletions

View File

@@ -131,7 +131,7 @@ describe("Database", () => {
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(55);
expect(db.getSchemaVersion()).toBe(56);
});
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(55);
expect(db.getSchemaVersion()).toBe(56);
});
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(55);
expect(db.getSchemaVersion()).toBe(56);
// 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(55);
expect(db.getSchemaVersion()).toBe(56);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(55);
expect(db.getSchemaVersion()).toBe(56);
db.close();
});
@@ -825,7 +825,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(55);
expect(db.getSchemaVersion()).toBe(56);
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(55);
expect(db.getSchemaVersion()).toBe(56);
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(55);
expect(db.getSchemaVersion()).toBe(56);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -1038,7 +1038,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(55);
expect(db.getSchemaVersion()).toBe(56);
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("attachments");
@@ -1112,7 +1112,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(55);
expect(db.getSchemaVersion()).toBe(56);
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" }]);
@@ -1136,7 +1136,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(55);
expect(db.getSchemaVersion()).toBe(56);
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" }]);
@@ -1240,7 +1240,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(55);
expect(db.getSchemaVersion()).toBe(56);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1691,7 +1691,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(55);
expect(db.getSchemaVersion()).toBe(56);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -779,7 +779,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(55);
expect(db1.getSchemaVersion()).toBe(56);
db1.close();
// Step 2: Manually downgrade to version 32 and drop insight tables
@@ -814,7 +814,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(55);
expect(db3.getSchemaVersion()).toBe(56);
// Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare(
@@ -845,12 +845,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try {
const db1 = createDatabase(testDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(55);
expect(db1.getSchemaVersion()).toBe(56);
db1.close();
const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(55);
expect(db2.getSchemaVersion()).toBe(56);
db2.close();
} finally {
rmSync(testDir, { recursive: true, force: true });

View File

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

View File

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

View File

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

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(55);
expect(db.getSchemaVersion()).toBe(56);
const index = db
.prepare(

View File

@@ -56,6 +56,7 @@ interface ChatSessionRow {
modelId: string | null;
createdAt: string;
updatedAt: string;
cliSessionFile: string | null;
}
/** Database row shape for chat_messages. */
@@ -97,6 +98,7 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
modelId: row.modelId ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
cliSessionFile: row.cliSessionFile ?? null,
};
}
@@ -138,6 +140,7 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
modelId: input.modelId ?? null,
createdAt: now,
updatedAt: now,
cliSessionFile: null,
};
this.db.prepare(`
@@ -331,6 +334,24 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
return this.updateSession(id, { status: "archived" });
}
/**
* Persist the pi/Claude CLI session file path for a chat. Called once,
* after the SessionManager for the chat first creates its on-disk file,
* so subsequent turns can reopen it via SessionManager.open.
*
* Does not bump updatedAt or emit events — this is internal plumbing,
* not a user-visible state change.
*
* @param id - Session ID
* @param cliSessionFile - Absolute path to the session file, or null to clear
*/
setCliSessionFile(id: string, cliSessionFile: string | null): void {
this.db
.prepare("UPDATE chat_sessions SET cliSessionFile = ? WHERE id = ?")
.run(cliSessionFile, id);
this.db.bumpLastModified();
}
/**
* Delete a chat session and all its messages.
* Messages are cascade-deleted via foreign key constraint.

View File

@@ -37,6 +37,14 @@ export interface ChatSession {
createdAt: string;
/** When the session was last updated */
updatedAt: string;
/**
* Absolute path to the pi/Claude CLI session file backing this chat, if
* any. Set on the first assistant turn (when SessionManager.create
* provisions the file) and reused by SessionManager.open on subsequent
* turns so the on-disk CLI session is resumed instead of recreated. Null
* for sessions that have never produced an assistant reply.
*/
cliSessionFile: string | null;
}
/**

View File

@@ -2115,6 +2115,17 @@ export class Database {
});
}
// Persist the pi/Claude CLI session file path per chat so quick-chat
// turns reuse the same on-disk session instead of starting fresh each
// user message.
if (version < 56) {
this.applyMigration(56, () => {
if (this.hasTable("chat_sessions")) {
this.addColumnIfMissing("chat_sessions", "cliSessionFile", "TEXT");
}
});
}
}
/**

View File

@@ -41,6 +41,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
showGitHubStarButton: true,
modelOnboardingComplete: undefined,
useClaudeCli: undefined,
useDroidCli: undefined,
// Global baseline lanes for per-role model selection
executionGlobalProvider: undefined,
executionGlobalModelId: undefined,

View File

@@ -1349,6 +1349,13 @@ export interface GlobalSettings {
* array in the agent settings for `"npm:pi-claude-cli"` (legacy signal).
* Setting this field explicitly (true/false) always wins. */
useClaudeCli?: boolean;
/** When true, route Factory AI model calls through the locally-installed Droid CLI
* via the `droid-cli` provider path (instead of direct API provider calls).
*
* When left undefined, Droid CLI routing stays disabled unless explicitly enabled
* by the dashboard auth toggle. Setting this field explicitly (true/false)
* always wins. */
useDroidCli?: boolean;
/** Global baseline AI model provider for task execution (executor agent).
* This is the global lane that project-level `executionProvider` can override.
* Must be set together with `executionGlobalModelId`. Falls back to