feat: workflow mode system (solo/team) with /gsd mode command (#651)
* feat: add workflow mode system (solo/team) with /gsd mode command Introduces a `mode` preference that bundles sensible defaults for solo developers vs team workflows, replacing the need to manually configure 5-8 individual git preferences. * fix: resolve TS2339 — use string narrowing for ctx.ui.select return type
This commit is contained in:
parent
cdf42fe001
commit
2042a30232
7 changed files with 361 additions and 20 deletions
|
|
@ -57,6 +57,29 @@ Use the `/worktree` (or `/wt`) command for manual worktree management:
|
|||
/worktree remove
|
||||
```
|
||||
|
||||
## Workflow Modes
|
||||
|
||||
Instead of configuring each git setting individually, set `mode` to get sensible defaults for your workflow:
|
||||
|
||||
```yaml
|
||||
mode: solo # personal projects — auto-push, squash, simple IDs
|
||||
mode: team # shared repos — unique IDs, push branches, pre-merge checks
|
||||
```
|
||||
|
||||
| Setting | `solo` | `team` |
|
||||
|---|---|---|
|
||||
| `git.auto_push` | `true` | `false` |
|
||||
| `git.push_branches` | `false` | `true` |
|
||||
| `git.pre_merge_check` | `false` | `true` |
|
||||
| `git.merge_strategy` | `"squash"` | `"squash"` |
|
||||
| `git.isolation` | `"worktree"` | `"worktree"` |
|
||||
| `git.commit_docs` | `true` | `true` |
|
||||
| `unique_milestone_ids` | `false` | `true` |
|
||||
|
||||
Mode defaults are the lowest priority — any explicit preference overrides them. For example, `mode: solo` with `git.auto_push: false` gives you everything from solo except auto-push.
|
||||
|
||||
Existing configs without `mode` work exactly as before — no defaults are injected.
|
||||
|
||||
## Git Preferences
|
||||
|
||||
Configure git behavior in preferences:
|
||||
|
|
|
|||
|
|
@ -4,19 +4,21 @@ GSD supports multi-user workflows where several developers work on the same repo
|
|||
|
||||
## Setup
|
||||
|
||||
### 1. Enable Unique Milestone IDs
|
||||
### 1. Set Team Mode
|
||||
|
||||
Prevent ID collisions when multiple developers create milestones:
|
||||
The simplest way to configure GSD for team use is to set `mode: team` in your project preferences. This enables unique milestone IDs, push branches, and pre-merge checks in one setting:
|
||||
|
||||
```yaml
|
||||
# .gsd/preferences.md (project-level, committed to git)
|
||||
---
|
||||
version: 1
|
||||
unique_milestone_ids: true
|
||||
mode: team
|
||||
---
|
||||
```
|
||||
|
||||
This generates milestone IDs like `M001-eh88as` instead of plain `M001`. The random suffix ensures no two developers clash.
|
||||
This is equivalent to manually setting `unique_milestone_ids: true`, `git.push_branches: true`, `git.pre_merge_check: true`, and other team-appropriate defaults. You can still override individual settings — for example, adding `git.auto_push: true` on top of `mode: team` if your team prefers auto-push.
|
||||
|
||||
Alternatively, you can configure each setting individually without using a mode (see [Git Strategy](git-strategy.md) for details).
|
||||
|
||||
### 2. Configure `.gitignore`
|
||||
|
||||
|
|
|
|||
|
|
@ -67,12 +67,12 @@ function projectRoot(): string {
|
|||
|
||||
export function registerGSDCommand(pi: ExtensionAPI): void {
|
||||
pi.registerCommand("gsd", {
|
||||
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|history|undo|skip|export|cleanup|prefs|config|hooks|run-hook|skill-health|doctor|migrate|remote|steer|knowledge",
|
||||
description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|migrate|remote|steer|knowledge",
|
||||
getArgumentCompletions: (prefix: string) => {
|
||||
const subcommands = [
|
||||
"help", "next", "auto", "stop", "pause", "status", "visualize", "queue", "quick", "discuss",
|
||||
"capture", "triage",
|
||||
"history", "undo", "skip", "export", "cleanup", "prefs",
|
||||
"history", "undo", "skip", "export", "cleanup", "mode", "prefs",
|
||||
"config", "hooks", "run-hook", "skill-health", "doctor", "migrate", "remote", "steer", "inspect", "knowledge",
|
||||
];
|
||||
const parts = prefix.trim().split(/\s+/);
|
||||
|
|
@ -90,6 +90,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
.map((f) => ({ value: `auto ${f}`, label: f }));
|
||||
}
|
||||
|
||||
if (parts[0] === "mode" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["global", "project"]
|
||||
.filter((cmd) => cmd.startsWith(subPrefix))
|
||||
.map((cmd) => ({ value: `mode ${cmd}`, label: cmd }));
|
||||
}
|
||||
|
||||
if (parts[0] === "prefs" && parts.length <= 2) {
|
||||
const subPrefix = parts[1] ?? "";
|
||||
return ["global", "project", "status", "wizard", "setup"]
|
||||
|
|
@ -177,6 +184,15 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "mode" || trimmed.startsWith("mode ")) {
|
||||
const modeArgs = trimmed.replace(/^mode\s*/, "").trim();
|
||||
const scope = modeArgs === "project" ? "project" : "global";
|
||||
const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath();
|
||||
await ensurePreferencesFile(path, ctx, scope);
|
||||
await handlePrefsMode(ctx, scope);
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed === "prefs" || trimmed.startsWith("prefs ")) {
|
||||
await handlePrefs(trimmed.replace(/^prefs\s*/, "").trim(), ctx);
|
||||
return;
|
||||
|
|
@ -401,6 +417,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
|
|||
" /gsd knowledge <type> <text> Add rule, pattern, or lesson to KNOWLEDGE.md",
|
||||
"",
|
||||
"CONFIGURATION",
|
||||
" /gsd mode Set workflow mode (solo/team) [global|project]",
|
||||
" /gsd prefs Manage preferences [global|project|status|wizard|setup]",
|
||||
" /gsd config Set API keys for external tools",
|
||||
" /gsd hooks Show post-unit hook configuration",
|
||||
|
|
@ -518,6 +535,36 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<
|
|||
ctx.ui.notify("Usage: /gsd prefs [global|project|status|wizard|setup]", "info");
|
||||
}
|
||||
|
||||
async function handlePrefsMode(ctx: ExtensionCommandContext, scope: "global" | "project"): Promise<void> {
|
||||
const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath();
|
||||
const existing = scope === "project" ? loadProjectGSDPreferences() : loadGlobalGSDPreferences();
|
||||
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : {};
|
||||
|
||||
await configureMode(ctx, prefs);
|
||||
|
||||
// Serialize and save
|
||||
prefs.version = prefs.version || 1;
|
||||
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
||||
|
||||
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
||||
if (existsSync(path)) {
|
||||
const existingContent = readFileSync(path, "utf-8");
|
||||
const closingIdx = existingContent.indexOf("\n---", existingContent.indexOf("---"));
|
||||
if (closingIdx !== -1) {
|
||||
const afterFrontmatter = existingContent.slice(closingIdx + 4);
|
||||
if (afterFrontmatter.trim()) {
|
||||
body = afterFrontmatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = `---\n${frontmatter}---${body}`;
|
||||
await saveFile(path, content);
|
||||
await ctx.waitForIdle();
|
||||
await ctx.reload();
|
||||
ctx.ui.notify(`Saved ${scope} preferences to ${path}`, "info");
|
||||
}
|
||||
|
||||
async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
||||
const trimmed = args.trim();
|
||||
const parts = trimmed ? trimmed.split(/\s+/) : [];
|
||||
|
|
@ -686,6 +733,10 @@ async function handleSkillHealth(args: string, ctx: ExtensionCommandContext): Pr
|
|||
|
||||
/** Build short summary strings for each preference category. */
|
||||
function buildCategorySummaries(prefs: Record<string, unknown>): Record<string, string> {
|
||||
// Mode
|
||||
const mode = prefs.mode as string | undefined;
|
||||
const modeSummary = mode ?? "(not set)";
|
||||
|
||||
// Models
|
||||
const models = prefs.models as Record<string, string> | undefined;
|
||||
let modelsSummary = "(not configured)";
|
||||
|
|
@ -752,6 +803,7 @@ function buildCategorySummaries(prefs: Record<string, unknown>): Record<string,
|
|||
}
|
||||
|
||||
return {
|
||||
mode: modeSummary,
|
||||
models: modelsSummary,
|
||||
timeouts: timeoutsSummary,
|
||||
git: gitSummary,
|
||||
|
|
@ -1052,6 +1104,37 @@ async function configureNotifications(ctx: ExtensionCommandContext, prefs: Recor
|
|||
}
|
||||
}
|
||||
|
||||
async function configureMode(ctx: ExtensionCommandContext, prefs: Record<string, unknown>): Promise<void> {
|
||||
const currentMode = prefs.mode as string | undefined;
|
||||
const modeChoice = await ctx.ui.select(
|
||||
`Workflow mode${currentMode ? ` (current: ${currentMode})` : ""}:`,
|
||||
[
|
||||
"solo — auto-push, squash, simple IDs (personal projects)",
|
||||
"team — unique IDs, push branches, pre-merge checks (shared repos)",
|
||||
"(none) — configure everything manually",
|
||||
"(keep current)",
|
||||
],
|
||||
);
|
||||
const modeStr = typeof modeChoice === "string" ? modeChoice : "";
|
||||
if (modeStr && modeStr !== "(keep current)") {
|
||||
if (modeStr.startsWith("solo")) {
|
||||
prefs.mode = "solo";
|
||||
ctx.ui.notify(
|
||||
"Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=false, merge_strategy=squash, isolation=worktree, commit_docs=true, unique_milestone_ids=false",
|
||||
"info",
|
||||
);
|
||||
} else if (modeStr.startsWith("team")) {
|
||||
prefs.mode = "team";
|
||||
ctx.ui.notify(
|
||||
"Mode: team — defaults: auto_push=false, push_branches=true, pre_merge_check=true, merge_strategy=squash, isolation=worktree, commit_docs=true, unique_milestone_ids=true",
|
||||
"info",
|
||||
);
|
||||
} else {
|
||||
delete prefs.mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function configureAdvanced(ctx: ExtensionCommandContext, prefs: Record<string, unknown>): Promise<void> {
|
||||
const currentUnique = prefs.unique_milestone_ids;
|
||||
const uniqueChoice = await ctx.ui.select(
|
||||
|
|
@ -1078,6 +1161,7 @@ async function handlePrefsWizard(
|
|||
while (true) {
|
||||
const summaries = buildCategorySummaries(prefs);
|
||||
const options = [
|
||||
`Workflow Mode ${summaries.mode}`,
|
||||
`Models ${summaries.models}`,
|
||||
`Timeouts ${summaries.timeouts}`,
|
||||
`Git ${summaries.git}`,
|
||||
|
|
@ -1092,7 +1176,8 @@ async function handlePrefsWizard(
|
|||
const choice = typeof raw === "string" ? raw : "";
|
||||
if (!choice || choice.includes("Save & Exit")) break;
|
||||
|
||||
if (choice.startsWith("Models")) await configureModels(ctx, prefs);
|
||||
if (choice.startsWith("Workflow Mode")) await configureMode(ctx, prefs);
|
||||
else if (choice.startsWith("Models")) await configureModels(ctx, prefs);
|
||||
else if (choice.startsWith("Timeouts")) await configureTimeouts(ctx, prefs);
|
||||
else if (choice.startsWith("Git")) await configureGit(ctx, prefs);
|
||||
else if (choice.startsWith("Skills")) await configureSkills(ctx, prefs);
|
||||
|
|
@ -1189,7 +1274,7 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
|
|||
|
||||
// Ordered keys for consistent output
|
||||
const orderedKeys = [
|
||||
"version", "always_use_skills", "prefer_skills", "avoid_skills",
|
||||
"version", "mode", "always_use_skills", "prefer_skills", "avoid_skills",
|
||||
"skill_rules", "custom_instructions", "models", "skill_discovery",
|
||||
"auto_supervisor", "uat_dispatch", "unique_milestone_ids",
|
||||
"budget_ceiling", "budget_enforcement", "context_pause_threshold",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,20 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|||
|
||||
- `version`: schema version. Start at `1`.
|
||||
|
||||
- `mode`: workflow mode — `"solo"` or `"team"`. Sets sensible defaults for git and project settings based on your workflow. Mode defaults are the lowest priority layer — any explicit preference overrides them. Omit to configure everything manually.
|
||||
|
||||
| Setting | `solo` | `team` |
|
||||
|---|---|---|
|
||||
| `git.auto_push` | `true` | `false` |
|
||||
| `git.push_branches` | `false` | `true` |
|
||||
| `git.pre_merge_check` | `false` | `true` |
|
||||
| `git.merge_strategy` | `"squash"` | `"squash"` |
|
||||
| `git.isolation` | `"worktree"` | `"worktree"` |
|
||||
| `git.commit_docs` | `true` | `true` |
|
||||
| `unique_milestone_ids` | `false` | `true` |
|
||||
|
||||
Quick setup: `/gsd mode` (global) or `/gsd mode project` (project-level).
|
||||
|
||||
- `always_use_skills`: skills GSD should use whenever they are relevant.
|
||||
|
||||
- `prefer_skills`: soft defaults GSD should prefer when relevant.
|
||||
|
|
@ -190,6 +204,45 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|||
|
||||
---
|
||||
|
||||
## Workflow Mode Examples
|
||||
|
||||
**Solo developer — auto-push, simple IDs:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
mode: solo
|
||||
---
|
||||
```
|
||||
|
||||
Equivalent to setting `git.auto_push: true`, `git.push_branches: false`, `git.pre_merge_check: false`, `git.merge_strategy: squash`, `git.isolation: worktree`, `git.commit_docs: true`, `unique_milestone_ids: false`.
|
||||
|
||||
**Team — unique IDs, push branches, pre-merge checks:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
mode: team
|
||||
---
|
||||
```
|
||||
|
||||
Equivalent to setting `git.auto_push: false`, `git.push_branches: true`, `git.pre_merge_check: true`, `git.merge_strategy: squash`, `git.isolation: worktree`, `git.commit_docs: true`, `unique_milestone_ids: true`.
|
||||
|
||||
**Mode with overrides — team mode but with auto-push:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
mode: team
|
||||
git:
|
||||
auto_push: true
|
||||
---
|
||||
```
|
||||
|
||||
Gets all team defaults except `auto_push`, which is explicitly overridden to `true`. Any explicit setting always wins over the mode default.
|
||||
|
||||
---
|
||||
|
||||
## Minimal Example
|
||||
|
||||
The cleanest preferences file only specifies what you actually want:
|
||||
|
|
|
|||
|
|
@ -18,9 +18,40 @@ const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(homedir(), ".gsd", "PREFERENCES.m
|
|||
const PROJECT_PREFERENCES_PATH_UPPERCASE = join(process.cwd(), ".gsd", "PREFERENCES.md");
|
||||
const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
|
||||
|
||||
// ─── Workflow Modes ──────────────────────────────────────────────────────────
|
||||
|
||||
export type WorkflowMode = "solo" | "team";
|
||||
|
||||
/** Default preference values for each workflow mode. */
|
||||
const MODE_DEFAULTS: Record<WorkflowMode, Partial<GSDPreferences>> = {
|
||||
solo: {
|
||||
git: {
|
||||
auto_push: true,
|
||||
push_branches: false,
|
||||
pre_merge_check: false,
|
||||
merge_strategy: "squash",
|
||||
isolation: "worktree",
|
||||
commit_docs: true,
|
||||
},
|
||||
unique_milestone_ids: false,
|
||||
},
|
||||
team: {
|
||||
git: {
|
||||
auto_push: false,
|
||||
push_branches: true,
|
||||
pre_merge_check: true,
|
||||
merge_strategy: "squash",
|
||||
isolation: "worktree",
|
||||
commit_docs: true,
|
||||
},
|
||||
unique_milestone_ids: true,
|
||||
},
|
||||
};
|
||||
|
||||
/** All recognized top-level keys in GSDPreferences. Used to detect typos / stale config. */
|
||||
const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
||||
"version",
|
||||
"mode",
|
||||
"always_use_skills",
|
||||
"prefer_skills",
|
||||
"avoid_skills",
|
||||
|
|
@ -116,6 +147,7 @@ export interface RemoteQuestionsConfig {
|
|||
|
||||
export interface GSDPreferences {
|
||||
version?: number;
|
||||
mode?: WorkflowMode;
|
||||
always_use_skills?: string[];
|
||||
prefer_skills?: string[];
|
||||
avoid_skills?: string[];
|
||||
|
|
@ -172,25 +204,49 @@ export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
|
|||
?? loadPreferencesFile(PROJECT_PREFERENCES_PATH_UPPERCASE, "project");
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply mode defaults as the lowest-priority layer.
|
||||
* Mode defaults fill in undefined fields; any explicit user value wins.
|
||||
*/
|
||||
export function applyModeDefaults(mode: WorkflowMode, prefs: GSDPreferences): GSDPreferences {
|
||||
const defaults = MODE_DEFAULTS[mode];
|
||||
if (!defaults) return prefs;
|
||||
return mergePreferences(defaults, prefs);
|
||||
}
|
||||
|
||||
export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null {
|
||||
const globalPreferences = loadGlobalGSDPreferences();
|
||||
const projectPreferences = loadProjectGSDPreferences();
|
||||
|
||||
if (!globalPreferences && !projectPreferences) return null;
|
||||
if (!globalPreferences) return projectPreferences;
|
||||
if (!projectPreferences) return globalPreferences;
|
||||
|
||||
const mergedWarnings = [
|
||||
...(globalPreferences.warnings ?? []),
|
||||
...(projectPreferences.warnings ?? []),
|
||||
];
|
||||
let result: LoadedGSDPreferences;
|
||||
if (!globalPreferences) {
|
||||
result = projectPreferences!;
|
||||
} else if (!projectPreferences) {
|
||||
result = globalPreferences;
|
||||
} else {
|
||||
const mergedWarnings = [
|
||||
...(globalPreferences.warnings ?? []),
|
||||
...(projectPreferences.warnings ?? []),
|
||||
];
|
||||
result = {
|
||||
path: projectPreferences.path,
|
||||
scope: "project",
|
||||
preferences: mergePreferences(globalPreferences.preferences, projectPreferences.preferences),
|
||||
...(mergedWarnings.length > 0 ? { warnings: mergedWarnings } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
path: projectPreferences.path,
|
||||
scope: "project",
|
||||
preferences: mergePreferences(globalPreferences.preferences, projectPreferences.preferences),
|
||||
...(mergedWarnings.length > 0 ? { warnings: mergedWarnings } : {}),
|
||||
};
|
||||
// Apply mode defaults as the lowest-priority layer
|
||||
if (result.preferences.mode) {
|
||||
result = {
|
||||
...result,
|
||||
preferences: applyModeDefaults(result.preferences.mode, result.preferences),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Skill Reference Resolution ───────────────────────────────────────────────
|
||||
|
|
@ -662,6 +718,7 @@ export function resolveInlineLevel(): InlineLevel {
|
|||
function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPreferences {
|
||||
return {
|
||||
version: override.version ?? base.version,
|
||||
mode: override.mode ?? base.mode,
|
||||
always_use_skills: mergeStringLists(base.always_use_skills, override.always_use_skills),
|
||||
prefer_skills: mergeStringLists(base.prefer_skills, override.prefer_skills),
|
||||
avoid_skills: mergeStringLists(base.avoid_skills, override.avoid_skills),
|
||||
|
|
@ -721,6 +778,16 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── Workflow Mode ──────────────────────────────────────────────────
|
||||
if (preferences.mode !== undefined) {
|
||||
const validModes = new Set<string>(["solo", "team"]);
|
||||
if (typeof preferences.mode === "string" && validModes.has(preferences.mode)) {
|
||||
validated.mode = preferences.mode as WorkflowMode;
|
||||
} else {
|
||||
errors.push(`invalid mode "${preferences.mode}" — must be one of: solo, team`);
|
||||
}
|
||||
}
|
||||
|
||||
const validDiscoveryModes = new Set(["auto", "suggest", "off"]);
|
||||
if (preferences.skill_discovery) {
|
||||
if (validDiscoveryModes.has(preferences.skill_discovery)) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
version: 1
|
||||
mode:
|
||||
always_use_skills: []
|
||||
prefer_skills: []
|
||||
avoid_skills: []
|
||||
|
|
|
|||
110
src/resources/extensions/gsd/tests/preferences-mode.test.ts
Normal file
110
src/resources/extensions/gsd/tests/preferences-mode.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// GSD Workflow Mode Tests — validates mode defaults, overrides, and validation
|
||||
|
||||
import { createTestContext } from "./test-helpers.ts";
|
||||
import { validatePreferences, applyModeDefaults } from "../preferences.ts";
|
||||
import type { GSDPreferences } from "../preferences.ts";
|
||||
|
||||
const { assertEq, assertTrue, report } = createTestContext();
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("\n=== mode: solo defaults ===");
|
||||
|
||||
{
|
||||
const prefs: GSDPreferences = { mode: "solo" };
|
||||
const result = applyModeDefaults("solo", prefs);
|
||||
assertEq(result.git?.auto_push, true, "solo — auto_push defaults to true");
|
||||
assertEq(result.git?.push_branches, false, "solo — push_branches defaults to false");
|
||||
assertEq(result.git?.pre_merge_check, false, "solo — pre_merge_check defaults to false");
|
||||
assertEq(result.git?.merge_strategy, "squash", "solo — merge_strategy defaults to squash");
|
||||
assertEq(result.git?.isolation, "worktree", "solo — isolation defaults to worktree");
|
||||
assertEq(result.git?.commit_docs, true, "solo — commit_docs defaults to true");
|
||||
assertEq(result.unique_milestone_ids, false, "solo — unique_milestone_ids defaults to false");
|
||||
}
|
||||
|
||||
console.log("\n=== mode: team defaults ===");
|
||||
|
||||
{
|
||||
const prefs: GSDPreferences = { mode: "team" };
|
||||
const result = applyModeDefaults("team", prefs);
|
||||
assertEq(result.git?.auto_push, false, "team — auto_push defaults to false");
|
||||
assertEq(result.git?.push_branches, true, "team — push_branches defaults to true");
|
||||
assertEq(result.git?.pre_merge_check, true, "team — pre_merge_check defaults to true");
|
||||
assertEq(result.git?.merge_strategy, "squash", "team — merge_strategy defaults to squash");
|
||||
assertEq(result.git?.isolation, "worktree", "team — isolation defaults to worktree");
|
||||
assertEq(result.git?.commit_docs, true, "team — commit_docs defaults to true");
|
||||
assertEq(result.unique_milestone_ids, true, "team — unique_milestone_ids defaults to true");
|
||||
}
|
||||
|
||||
console.log("\n=== explicit override wins over mode default ===");
|
||||
|
||||
{
|
||||
const prefs: GSDPreferences = {
|
||||
mode: "solo",
|
||||
git: { auto_push: false },
|
||||
};
|
||||
const result = applyModeDefaults("solo", prefs);
|
||||
assertEq(result.git?.auto_push, false, "solo + explicit auto_push=false — override wins");
|
||||
assertEq(result.git?.push_branches, false, "solo + override — other defaults still apply");
|
||||
assertEq(result.git?.merge_strategy, "squash", "solo + override — merge_strategy still defaults");
|
||||
}
|
||||
|
||||
console.log("\n=== no mode set — no defaults injected ===");
|
||||
|
||||
{
|
||||
const prefs: GSDPreferences = { git: { auto_push: true } };
|
||||
const { preferences } = validatePreferences(prefs);
|
||||
assertEq(preferences.mode, undefined, "no mode — mode is undefined");
|
||||
assertEq(preferences.git?.push_branches, undefined, "no mode — push_branches not injected");
|
||||
assertEq(preferences.unique_milestone_ids, undefined, "no mode — unique_milestone_ids not injected");
|
||||
}
|
||||
|
||||
console.log("\n=== invalid mode value → validation error ===");
|
||||
|
||||
{
|
||||
const { errors } = validatePreferences({ mode: "invalid" as any });
|
||||
assertTrue(errors.length > 0, "invalid mode — produces error");
|
||||
assertTrue(errors[0].includes("solo, team"), "invalid mode — error mentions valid values");
|
||||
}
|
||||
|
||||
console.log("\n=== valid mode values pass validation ===");
|
||||
|
||||
{
|
||||
const { errors: soloErrors, preferences: soloPrefs } = validatePreferences({ mode: "solo" });
|
||||
assertEq(soloErrors.length, 0, "mode: solo — no errors");
|
||||
assertEq(soloPrefs.mode, "solo", "mode: solo — value preserved");
|
||||
}
|
||||
{
|
||||
const { errors: teamErrors, preferences: teamPrefs } = validatePreferences({ mode: "team" });
|
||||
assertEq(teamErrors.length, 0, "mode: team — no errors");
|
||||
assertEq(teamPrefs.mode, "team", "mode: team — value preserved");
|
||||
}
|
||||
|
||||
console.log("\n=== deep merge: mode + explicit git.remote ===");
|
||||
|
||||
{
|
||||
const prefs: GSDPreferences = {
|
||||
mode: "team",
|
||||
git: { remote: "upstream" },
|
||||
};
|
||||
const result = applyModeDefaults("team", prefs);
|
||||
assertEq(result.git?.remote, "upstream", "team + git.remote — custom remote preserved");
|
||||
assertEq(result.git?.auto_push, false, "team + git.remote — team auto_push default applied");
|
||||
assertEq(result.git?.push_branches, true, "team + git.remote — team push_branches default applied");
|
||||
}
|
||||
|
||||
console.log("\n=== mode + unique_milestone_ids explicit override ===");
|
||||
|
||||
{
|
||||
const prefs: GSDPreferences = {
|
||||
mode: "team",
|
||||
unique_milestone_ids: false,
|
||||
};
|
||||
const result = applyModeDefaults("team", prefs);
|
||||
assertEq(result.unique_milestone_ids, false, "team + explicit unique_milestone_ids=false — override wins");
|
||||
assertEq(result.git?.push_branches, true, "team + override — other team defaults still apply");
|
||||
}
|
||||
|
||||
report();
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Add table
Reference in a new issue