feat(workspace): workspace mode toggle, derived task prefix, default coding workflow (#1741)

## Problem

After PR #1739 merged the core workspace auto-detection fix, additional
workspace UX was needed:
1. Onboarding should interactively confirm workspace mode and let the
user pick a task prefix and default workflow.
2. Task prefix should be derived from the project name (2-4 chars
uppercase) instead of a hardcoded constant.
3. Default fallback prefix should be `FN` (matching the product name),
not the legacy `KB`.
4. Dashboard registration should also auto-derive prefix and set the
coding workflow.
5. Default workflow should be `builtin:coding`.

## Changes

### `packages/cli/src/project-resolver.ts`
- **Interactive workspace confirmation**: When sub-repos are detected,
ask the user to confirm workspace mode instead of auto-applying
(non-interactive/dashboard still auto-applies).
- **`suggestTaskPrefix(projectName)`**: Derives a 2-4 char prefix from
the project name (first letters of words, or first chars of a single
word).
- **Onboarding prefix prompt**: Shows the suggested prefix and lets the
user confirm or override.
- **Default coding workflow**: Sets `defaultWorkflowId:
"builtin:coding"` for new projects.

### `packages/core/src/settings-schema.ts`
- `DEFAULT_PROJECT_SETTINGS.workspaceMode`: changed from `false` to
`undefined` so `TaskStore.init()` does not write `workspaceMode: false`
to config.json before auto-detection runs (which would block it via
`isWorkspaceModeExplicitlyDisabled`).
- `DEFAULT_PROJECT_SETTINGS.taskPrefix`: changed to `undefined` (falls
back to `"FN"` in store).
- `DEFAULT_PROJECT_SETTINGS.defaultWorkflowId`: set to
`"builtin:coding"`.

### `packages/core/src/store.ts`
- Task prefix fallback changed from `"KB"` to `"FN"`.

### `packages/dashboard/src/routes/register-project-routes.ts`
- Auto-derives task prefix from project name for dashboard
registrations.
- Sets `defaultWorkflowId: "builtin:coding"` for new projects.

## Testing
- `pnpm typecheck` — pass
- `pnpm lint` — pass
- `vitest run git-repository.test.ts` — 8/8 pass

<!-- stage-review-badge-begin -->

---

<a href="https://stagereview.app/Runfusion/Fusion/pull/1741">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
  </picture>
</a>

<!-- stage-review-badge-end -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Project onboarding now proposes a task prefix derived from the project
name, and stores it during setup (interactive onboarding and new
registrations).
* New projects start with the default coding workflow enabled to improve
first-time experience.
* **Bug Fixes**
* Improved distributed task ID prefix fallback when no custom prefix is
configured (now uses `FN`).
* **Chores**
* Updated default project settings so `taskPrefix` and `workspaceMode`
begin unset, aligning with onboarding behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-06-24 11:45:33 -07:00
committed by GitHub
6 changed files with 72 additions and 3 deletions

View File

@@ -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);
}

View File

@@ -1957,3 +1957,4 @@ export {
clearSyncPassphrase,
hasSyncPassphraseConfigured,
} from "./secrets-sync-passphrase.js";
export { suggestTaskPrefix } from "./task-prefix.js";

View File

@@ -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<ProjectSettingsSchema>;
/**

View File

@@ -4151,7 +4151,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
},
): Promise<Task> {
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({

View File

@@ -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";
}

View File

@@ -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