diff --git a/packages/cli/src/project-resolver.ts b/packages/cli/src/project-resolver.ts index 9e19789add..044a718867 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"; @@ -685,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( @@ -743,6 +745,33 @@ 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 rawPrefix = prefixInput.trim().toUpperCase().replace(/[^A-Z]/g, ""); + const prefix = rawPrefix.length >= 2 && rawPrefix.length <= 5 ? rawPrefix : 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/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 77eb6e7176..a296a22bdd 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -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/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({ 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 2866252493..bb28f6f709 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, suggestTaskPrefix } = await import("@fusion/core"); + const store = new TaskStore(normalizedPath); + await store.init(); + const prefix = suggestTaskPrefix(normalizedName); + 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