From 800f845e15452b8883c0eea0425f09e74fbb2eaf Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 24 Jun 2026 10:20:00 -0700 Subject: [PATCH 1/5] feat(workspace): fix auto-detection, derive prefix from name, default coding workflow - Fix workspace detection: change workspaceMode default from false to undefined so isWorkspaceModeExplicitlyDisabled no longer blocks auto-detection on fresh projects (config.json was being written with workspaceMode:false during store.init(), causing the guard to skip detection before it ever ran) - Derive task prefix from project name (first 2-4 chars) instead of hardcoded 'FN' as the suggested default - Default workflow is now builtin:coding instead of undefined - CLI registerProjectInteractive: onboarding prompt for task prefix confirmation after project name - Dashboard POST /api/projects: auto-derive prefix and set default workflow for new registrations --- packages/cli/src/project-resolver.ts | 39 +++++++++++++++++++ packages/core/src/settings-schema.ts | 6 +-- .../src/routes/register-project-routes.ts | 22 +++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/project-resolver.ts b/packages/cli/src/project-resolver.ts index 9e19789add..346678e4bc 100644 --- a/packages/cli/src/project-resolver.ts +++ b/packages/cli/src/project-resolver.ts @@ -537,6 +537,19 @@ export function suggestProjectName(path: string): string { return parts[parts.length - 1] || "unnamed"; } +/** + * FNXC:TaskPrefix 2026-06-24-18:00: + * Derive a task prefix from a project name by taking the first 2-4 uppercase + * letters. Falls back to "FN" for short names. Used as the suggested default + * during project onboarding so each project gets a recognizable prefix. + */ +export function suggestTaskPrefix(projectName: string): string { + const cleaned = projectName.replace(/[^a-zA-Z]/g, "").toUpperCase(); + if (cleaned.length >= 2 && cleaned.length <= 4) return cleaned; + if (cleaned.length > 4) return cleaned.slice(0, 4); + return "FN"; +} + /** * Resolve absolute path and validate it exists. */ @@ -743,6 +756,32 @@ export async function registerProjectInteractive( // Best-effort stamp only. } + /* + FNXC:Onboarding 2026-06-24-18:00: + After registration, prompt the user to confirm a task prefix and default workflow. + The prefix defaults to the first 2-4 chars of the project name so each project gets + recognizable task IDs (e.g., "MYPR" for "my-project"). The workflow defaults to coding. + Both are persisted to config.json via the TaskStore. + */ + if (interactive) { + const { TaskStore } = await import("@fusion/core"); + const store = new TaskStore(absPath); + await store.init(); + + const suggestedPrefix = suggestTaskPrefix(name); + const rl = createInterface({ input: process.stdin, output: process.stdout }); + const prefixInput = await rl.question(`\n Task prefix [${suggestedPrefix}]: `); + rl.close(); + const prefix = prefixInput.trim().toUpperCase() || suggestedPrefix; + + await store.updateSettings({ + taskPrefix: prefix, + defaultWorkflowId: "builtin:coding", + }); + await store.close(); + console.log(` ✓ Task prefix set to "${prefix}", default workflow: coding`); + } + return createResolvedProject(project); } diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 77eb6e7176..d57d323ccb 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -258,7 +258,7 @@ export const DEFAULT_GLOBAL_SETTINGS = { export const DEFAULT_PROJECT_SETTINGS = { globalPause: false, globalPauseReason: undefined, - defaultWorkflowId: undefined, + defaultWorkflowId: "builtin:coding", enabledBuiltinWorkflowIds: undefined, approvedWorkflowCliCommands: undefined, approvedCliAutonomyAdapters: undefined, @@ -307,7 +307,7 @@ export const DEFAULT_PROJECT_SETTINGS = { onFailure: "fail", }, worktreesDir: undefined, - taskPrefix: "FN", + taskPrefix: undefined, taskAttributionTrailerNames: ["Fusion-Task-Id"], commitMsgHookEnabled: true, includeTaskIdInCommit: true, @@ -538,7 +538,7 @@ export const DEFAULT_PROJECT_SETTINGS = { researchDefaultTimeout: 300000, researchMaxSourcesPerRun: 20, researchMaxSynthesisRounds: 2, - workspaceMode: false, + workspaceMode: undefined, } satisfies CompleteSettings; /** diff --git a/packages/dashboard/src/routes/register-project-routes.ts b/packages/dashboard/src/routes/register-project-routes.ts index 2866252493..da084ec62e 100644 --- a/packages/dashboard/src/routes/register-project-routes.ts +++ b/packages/dashboard/src/routes/register-project-routes.ts @@ -374,6 +374,28 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => { // Memory bootstrap failure is non-fatal - project registration succeeded }); + /* + FNXC:Onboarding 2026-06-24-18:00: + For new registrations (not reattachments), set a derived task prefix and default + workflow via the per-project TaskStore config.json so the project is immediately + usable without manual settings configuration. + */ + if (activeProjectWithOutcome.outcome === "registered") { + try { + const { TaskStore } = await import("@fusion/core"); + const store = new TaskStore(normalizedPath); + await store.init(); + const prefix = normalizedName.replace(/[^a-zA-Z]/g, "").toUpperCase().slice(0, 4) || "FN"; + await store.updateSettings({ + taskPrefix: prefix, + defaultWorkflowId: "builtin:coding", + }); + await store.close(); + } catch { + // Non-fatal: project registration succeeded; settings can be configured later + } + } + // Notify the host (serve.ts/daemon.ts) so it can run project-setup // side-effects like installing the fusion Claude-skill into // .claude/skills/fusion when pi-claude-cli is configured. The callback From 9a7c0c6154abf4949feb25fdb9cad0bc947dd7d1 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 24 Jun 2026 10:24:39 -0700 Subject: [PATCH 2/5] fix: fallback task prefix to FN (was KB) when unset The hardcoded fallback prefix in store.ts was 'KB' (legacy name). Changed to 'FN' to match the product name and dashboard placeholder. --- packages/core/src/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index cc921f093a..b5e85e2411 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -4151,7 +4151,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} }, ): Promise { const settings = await this.getSettingsFast(); - const prefix = (settings.taskPrefix || "KB").trim().toUpperCase(); + const prefix = (settings.taskPrefix || "FN").trim().toUpperCase(); const allocator = this.getDistributedTaskIdAllocator(); const nodeId = await this.resolveLocalNodeIdForTaskAllocation(); const reservation = await allocator.reserveDistributedTaskId({ From aae76cecc3e71f9162cbc8613b309f5eccd7dc03 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 24 Jun 2026 10:45:21 -0700 Subject: [PATCH 3/5] Address PR review feedback (#1741) - Close first TaskStore before creating second in interactive registration (P1) - Revert defaultWorkflowId default to undefined; set explicitly in onboarding only (P1) - Add alpha-only filter + 2-char min to interactive prefix input (P2) - Move suggestTaskPrefix to @fusion/core, share between CLI and dashboard (P2) - Fix suggestTaskPrefix JSDoc to match implementation (P2) --- packages/cli/src/project-resolver.ts | 18 ++++-------------- packages/core/src/index.ts | 1 + packages/core/src/settings-schema.ts | 2 +- packages/core/src/task-prefix.ts | 17 +++++++++++++++++ .../src/routes/register-project-routes.ts | 4 ++-- 5 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 packages/core/src/task-prefix.ts diff --git a/packages/cli/src/project-resolver.ts b/packages/cli/src/project-resolver.ts index 346678e4bc..6fc82e19b4 100644 --- a/packages/cli/src/project-resolver.ts +++ b/packages/cli/src/project-resolver.ts @@ -18,6 +18,7 @@ import { writeProjectIdentity, detectWorkspaceRepos, saveWorkspaceConfig, + suggestTaskPrefix, type RegisteredProject, type TaskStore, } from "@fusion/core"; @@ -537,19 +538,6 @@ export function suggestProjectName(path: string): string { return parts[parts.length - 1] || "unnamed"; } -/** - * FNXC:TaskPrefix 2026-06-24-18:00: - * Derive a task prefix from a project name by taking the first 2-4 uppercase - * letters. Falls back to "FN" for short names. Used as the suggested default - * during project onboarding so each project gets a recognizable prefix. - */ -export function suggestTaskPrefix(projectName: string): string { - const cleaned = projectName.replace(/[^a-zA-Z]/g, "").toUpperCase(); - if (cleaned.length >= 2 && cleaned.length <= 4) return cleaned; - if (cleaned.length > 4) return cleaned.slice(0, 4); - return "FN"; -} - /** * Resolve absolute path and validate it exists. */ @@ -698,6 +686,7 @@ export async function registerProjectInteractive( // Persist workspaceMode in config.json so it's visible/toggleable in the dashboard await store.updateSettings({ workspaceMode: true }); } + await store.close(); console.log(` ✓ Initialized fn at ${absPath}`); } else { throw new ProjectResolutionError( @@ -772,7 +761,8 @@ export async function registerProjectInteractive( const rl = createInterface({ input: process.stdin, output: process.stdout }); const prefixInput = await rl.question(`\n Task prefix [${suggestedPrefix}]: `); rl.close(); - const prefix = prefixInput.trim().toUpperCase() || suggestedPrefix; + const rawPrefix = prefixInput.trim().toUpperCase().replace(/[^A-Z]/g, ""); + const prefix = rawPrefix.length >= 2 ? rawPrefix : suggestedPrefix; await store.updateSettings({ taskPrefix: prefix, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 309ffdf8d1..7a16c53537 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1957,3 +1957,4 @@ export { clearSyncPassphrase, hasSyncPassphraseConfigured, } from "./secrets-sync-passphrase.js"; +export { suggestTaskPrefix } from "./task-prefix.js"; diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index d57d323ccb..a296a22bdd 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -258,7 +258,7 @@ export const DEFAULT_GLOBAL_SETTINGS = { export const DEFAULT_PROJECT_SETTINGS = { globalPause: false, globalPauseReason: undefined, - defaultWorkflowId: "builtin:coding", + defaultWorkflowId: undefined, enabledBuiltinWorkflowIds: undefined, approvedWorkflowCliCommands: undefined, approvedCliAutonomyAdapters: undefined, diff --git a/packages/core/src/task-prefix.ts b/packages/core/src/task-prefix.ts new file mode 100644 index 0000000000..047a39d39d --- /dev/null +++ b/packages/core/src/task-prefix.ts @@ -0,0 +1,17 @@ +/** + * FNXC:TaskPrefix 2026-06-24-18:00: + * Derive a task prefix from a project name. Strips non-alpha characters, uppercases, + * and takes the first 2-4 characters. Falls back to "FN" for names with fewer than + * 2 letters. Used during project onboarding (CLI and dashboard) so each project gets + * a recognizable prefix for task IDs (e.g. "MYPR" for "my-project"). + * + * Note: the result is the first 2-4 letters of the cleaned (alpha-only, uppercased) + * name, NOT the initials of each word. For "my-project" the result is "MYPR" + * (first 4 of "MYPROJECT"), not "MP". + */ +export function suggestTaskPrefix(projectName: string): string { + const cleaned = projectName.replace(/[^a-zA-Z]/g, "").toUpperCase(); + if (cleaned.length >= 2 && cleaned.length <= 4) return cleaned; + if (cleaned.length > 4) return cleaned.slice(0, 4); + return "FN"; +} diff --git a/packages/dashboard/src/routes/register-project-routes.ts b/packages/dashboard/src/routes/register-project-routes.ts index da084ec62e..bb28f6f709 100644 --- a/packages/dashboard/src/routes/register-project-routes.ts +++ b/packages/dashboard/src/routes/register-project-routes.ts @@ -382,10 +382,10 @@ export const registerProjectRoutes: ApiRouteRegistrar = (ctx) => { */ if (activeProjectWithOutcome.outcome === "registered") { try { - const { TaskStore } = await import("@fusion/core"); + const { TaskStore, suggestTaskPrefix } = await import("@fusion/core"); const store = new TaskStore(normalizedPath); await store.init(); - const prefix = normalizedName.replace(/[^a-zA-Z]/g, "").toUpperCase().slice(0, 4) || "FN"; + const prefix = suggestTaskPrefix(normalizedName); await store.updateSettings({ taskPrefix: prefix, defaultWorkflowId: "builtin:coding", From 06adc190ce00aaa70bafc03aa25425430ee8db39 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 24 Jun 2026 11:34:38 -0700 Subject: [PATCH 4/5] Add max-length cap (10) to interactive prefix input (#1741) CodeRabbit: suggestTaskPrefix caps at 4 chars, but user input was uncapped. Align with the dashboard validation regex (1-10 uppercase letters). --- packages/cli/src/project-resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/project-resolver.ts b/packages/cli/src/project-resolver.ts index 6fc82e19b4..e564214bee 100644 --- a/packages/cli/src/project-resolver.ts +++ b/packages/cli/src/project-resolver.ts @@ -762,7 +762,7 @@ export async function registerProjectInteractive( const prefixInput = await rl.question(`\n Task prefix [${suggestedPrefix}]: `); rl.close(); const rawPrefix = prefixInput.trim().toUpperCase().replace(/[^A-Z]/g, ""); - const prefix = rawPrefix.length >= 2 ? rawPrefix : suggestedPrefix; + const prefix = rawPrefix.length >= 2 && rawPrefix.length <= 10 ? rawPrefix : suggestedPrefix; await store.updateSettings({ taskPrefix: prefix, From b317a39d802cd827dfa1e9dea06a1b52c93e3707 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 24 Jun 2026 11:39:40 -0700 Subject: [PATCH 5/5] Cap interactive prefix input to 5 chars (#1741) --- packages/cli/src/project-resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/project-resolver.ts b/packages/cli/src/project-resolver.ts index e564214bee..044a718867 100644 --- a/packages/cli/src/project-resolver.ts +++ b/packages/cli/src/project-resolver.ts @@ -762,7 +762,7 @@ export async function registerProjectInteractive( const prefixInput = await rl.question(`\n Task prefix [${suggestedPrefix}]: `); rl.close(); const rawPrefix = prefixInput.trim().toUpperCase().replace(/[^A-Z]/g, ""); - const prefix = rawPrefix.length >= 2 && rawPrefix.length <= 10 ? rawPrefix : suggestedPrefix; + const prefix = rawPrefix.length >= 2 && rawPrefix.length <= 5 ? rawPrefix : suggestedPrefix; await store.updateSettings({ taskPrefix: prefix,