chore: rename preferences.md to PREFERENCES.md for consistency (#2700) (#2738)

All other .gsd/ state files use uppercase naming (DECISIONS.md,
REQUIREMENTS.md, PROJECT.md, etc). This renames the canonical
preferences file to PREFERENCES.md while keeping a migration
fallback — the loader checks PREFERENCES.md first, then falls
back to lowercase preferences.md for existing installations.

Closes #2700

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Iouri Goussev 2026-03-26 18:09:59 -04:00 committed by GitHub
parent 6172246772
commit a952391b33
39 changed files with 90 additions and 90 deletions

View file

@ -11,7 +11,7 @@ Users on capped plans (e.g., Claude Pro) exhaust weekly token limits in 15-20 ho
## Current Architecture ## Current Architecture
### What Exists ### What Exists
- **Phase-based model config:** Users can set different models per phase via `preferences.md` (research, planning, execution, completion) - **Phase-based model config:** Users can set different models per phase via `PREFERENCES.md` (research, planning, execution, completion)
- **Fallback chains:** Each phase supports `fallbacks: [model1, model2]` for error recovery - **Fallback chains:** Each phase supports `fallbacks: [model1, model2]` for error recovery
- **Pre-dispatch hooks:** `PreDispatchResult` has a `model` field but it's **never applied** in `auto.ts` — this is a ready-made extension point - **Pre-dispatch hooks:** `PreDispatchResult` has a `model` field but it's **never applied** in `auto.ts` — this is a ready-made extension point
- **Model registry:** `ModelRegistry.getAvailable()` provides all configured models with metadata - **Model registry:** `ModelRegistry.getAvailable()` provides all configured models with metadata

View file

@ -134,7 +134,7 @@ Quick filesystem scan (no heavy reads):
### Task 1.4: `isFirstEverLaunch(): boolean` ### Task 1.4: `isFirstEverLaunch(): boolean`
Returns `true` if `~/.gsd/` doesn't exist or has no `preferences.md`. Returns `true` if `~/.gsd/` doesn't exist or has no `PREFERENCES.md`.
--- ---
@ -298,7 +298,7 @@ Step 8: Advanced (collapsed by default, expandable)
Step 9: Bootstrap .gsd/ structure Step 9: Bootstrap .gsd/ structure
- Creates .gsd/milestones/ - Creates .gsd/milestones/
- Creates .gsd/preferences.md (from wizard answers) - Creates .gsd/PREFERENCES.md (from wizard answers)
- Creates .gitignore entries - Creates .gitignore entries
- Seeds CONTEXT.md with detected project signals - Seeds CONTEXT.md with detected project signals
- Commits "chore: init gsd" (if commit_docs enabled) - Commits "chore: init gsd" (if commit_docs enabled)

View file

@ -42,7 +42,7 @@ The `/gsd prefs wizard` currently only configures 6 of 18+ preference fields. Us
- Added missing keys to `orderedKeys` in `serializePreferencesToFrontmatter()` - Added missing keys to `orderedKeys` in `serializePreferencesToFrontmatter()`
### Group 6: Update Template & Docs ✓ ### Group 6: Update Template & Docs ✓
- Updated `templates/preferences.md` with new fields - Updated `templates/PREFERENCES.md` with new fields
- Updated `docs/preferences-reference.md` with budget, notifications, git, hooks - Updated `docs/preferences-reference.md` with budget, notifications, git, hooks
### Group 7: Tests ✓ ### Group 7: Tests ✓

View file

@ -53,7 +53,7 @@ git rebase origin/main
GSD uses worktree-based isolation for multi-developer work. If you're contributing with GSD running, enable team mode in your project preferences: GSD uses worktree-based isolation for multi-developer work. If you're contributing with GSD running, enable team mode in your project preferences:
```yaml ```yaml
# .gsd/preferences.md # .gsd/PREFERENCES.md
--- ---
version: 1 version: 1
mode: team mode: team

View file

@ -521,7 +521,7 @@ An auto-generated `index.html` shows all reports with progression metrics across
### Preferences ### Preferences
GSD preferences live in `~/.gsd/preferences.md` (global) or `.gsd/preferences.md` (project). Manage with `/gsd prefs`. GSD preferences live in `~/.gsd/PREFERENCES.md` (global) or `.gsd/PREFERENCES.md` (project). Manage with `/gsd prefs`.
```yaml ```yaml
--- ---
@ -672,7 +672,7 @@ The best practice for working in teams is to ensure unique milestone names acros
### Unique Milestone Names ### Unique Milestone Names
Create or amend your `.gsd/preferences.md` file within the repo to include `unique_milestone_ids: true` e.g. Create or amend your `.gsd/PREFERENCES.md` file within the repo to include `unique_milestone_ids: true` e.g.
```markdown ```markdown
--- ---
@ -681,7 +681,7 @@ unique_milestone_ids: true
--- ---
``` ```
With the above `.gitignore` set up, the `.gsd/preferences.md` file is checked into the repo ensuring all teammates use unique milestone names to avoid collisions. With the above `.gitignore` set up, the `.gsd/PREFERENCES.md` file is checked into the repo ensuring all teammates use unique milestone names to avoid collisions.
Milestone names will now be generated with a 6 char random string appended e.g. instead of `M001` you'll get something like `M001-ush8s3` Milestone names will now be generated with a 6 char random string appended e.g. instead of `M001` you'll get something like `M001-ush8s3`
@ -689,7 +689,7 @@ Milestone names will now be generated with a 6 char random string appended e.g.
1. Ensure you are not in the middle of any milestones (clean state) 1. Ensure you are not in the middle of any milestones (clean state)
2. Update the `.gsd/` related entries in your `.gitignore` to follow the `Suggested .gitignore setup` section under `Working in teams` (ensure you are no longer blanket ignoring the whole `.gsd/` directory) 2. Update the `.gsd/` related entries in your `.gitignore` to follow the `Suggested .gitignore setup` section under `Working in teams` (ensure you are no longer blanket ignoring the whole `.gsd/` directory)
3. Update your `.gsd/preferences.md` file within the repo as per section `Unique Milestone Names` 3. Update your `.gsd/PREFERENCES.md` file within the repo as per section `Unique Milestone Names`
4. If you want to update all your existing milestones use this prompt in GSD: `I have turned on unique milestone ids, please update all old milestone ids to use this new format e.g. M001-abc123 where abc123 is a random 6 char lowercase alpha numeric string. Update all references in all .gsd file contents, file names and directory names. Validate your work once done to ensure referential integrity.` 4. If you want to update all your existing milestones use this prompt in GSD: `I have turned on unique milestone ids, please update all old milestone ids to use this new format e.g. M001-abc123 where abc123 is a random 6 char lowercase alpha numeric string. Update all references in all .gsd file contents, file names and directory names. Validate your work once done to ensure referential integrity.`
5. Commit to git 5. Commit to git

View file

@ -1,14 +1,14 @@
# Configuration # Configuration
GSD preferences live in `~/.gsd/preferences.md` (global) or `.gsd/preferences.md` (project-local). Manage interactively with `/gsd prefs`. GSD preferences live in `~/.gsd/PREFERENCES.md` (global) or `.gsd/PREFERENCES.md` (project-local). Manage interactively with `/gsd prefs`.
## `/gsd prefs` Commands ## `/gsd prefs` Commands
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `/gsd prefs` | Open the global preferences wizard (default) | | `/gsd prefs` | Open the global preferences wizard (default) |
| `/gsd prefs global` | Interactive wizard for global preferences (`~/.gsd/preferences.md`) | | `/gsd prefs global` | Interactive wizard for global preferences (`~/.gsd/PREFERENCES.md`) |
| `/gsd prefs project` | Interactive wizard for project preferences (`.gsd/preferences.md`) | | `/gsd prefs project` | Interactive wizard for project preferences (`.gsd/PREFERENCES.md`) |
| `/gsd prefs status` | Show current preference files, merged values, and skill resolution status | | `/gsd prefs status` | Show current preference files, merged values, and skill resolution status |
| `/gsd prefs wizard` | Alias for `/gsd prefs global` | | `/gsd prefs wizard` | Alias for `/gsd prefs global` |
| `/gsd prefs setup` | Alias for `/gsd prefs wizard` — creates preferences file if missing | | `/gsd prefs setup` | Alias for `/gsd prefs wizard` — creates preferences file if missing |
@ -42,8 +42,8 @@ token_profile: balanced
| Scope | Path | Applies to | | Scope | Path | Applies to |
|-------|------|-----------| |-------|------|-----------|
| Global | `~/.gsd/preferences.md` | All projects | | Global | `~/.gsd/PREFERENCES.md` | All projects |
| Project | `.gsd/preferences.md` | Current project only | | Project | `.gsd/PREFERENCES.md` | Current project only |
**Merge behavior:** **Merge behavior:**
- **Scalar fields** (`skill_discovery`, `budget_ceiling`): project wins if defined - **Scalar fields** (`skill_discovery`, `budget_ceiling`): project wins if defined

View file

@ -126,7 +126,7 @@ File overlaps are warnings, not blockers. Both milestones work in separate workt
## Configuration ## Configuration
Add to `~/.gsd/preferences.md` or `.gsd/preferences.md`: Add to `~/.gsd/PREFERENCES.md` or `.gsd/PREFERENCES.md`:
```yaml ```yaml
--- ---

View file

@ -16,7 +16,7 @@ The setup wizard:
3. Lists servers the bot belongs to (or lets you pick) 3. Lists servers the bot belongs to (or lets you pick)
4. Lists text channels in the selected server 4. Lists text channels in the selected server
5. Sends a test message to confirm permissions 5. Sends a test message to confirm permissions
6. Saves the configuration to `~/.gsd/preferences.md` 6. Saves the configuration to `~/.gsd/PREFERENCES.md`
**Bot requirements:** **Bot requirements:**
- A Discord bot application with a token (from [Discord Developer Portal](https://discord.com/developers/applications)) - A Discord bot application with a token (from [Discord Developer Portal](https://discord.com/developers/applications))
@ -65,7 +65,7 @@ The setup wizard:
## Configuration ## Configuration
Remote questions are configured in `~/.gsd/preferences.md`: Remote questions are configured in `~/.gsd/PREFERENCES.md`:
```yaml ```yaml
remote_questions: remote_questions:

View file

@ -257,7 +257,7 @@ models:
## How the Pieces Fit Together ## How the Pieces Fit Together
``` ```
preferences.md PREFERENCES.md
└─ token_profile: balanced └─ token_profile: balanced
├─ resolveProfileDefaults() → model defaults + phase skip defaults ├─ resolveProfileDefaults() → model defaults + phase skip defaults
├─ resolveInlineLevel() → standard ├─ resolveInlineLevel() → standard

View file

@ -9,7 +9,7 @@ GSD supports multi-user workflows where several developers work on the same repo
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: 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 ```yaml
# .gsd/preferences.md (project-level, committed to git) # .gsd/PREFERENCES.md (project-level, committed to git)
--- ---
version: 1 version: 1
mode: team mode: team
@ -38,7 +38,7 @@ Share planning artifacts (milestones, roadmaps, decisions) while keeping runtime
``` ```
**What gets shared** (committed to git): **What gets shared** (committed to git):
- `.gsd/preferences.md` — project preferences - `.gsd/PREFERENCES.md` — project preferences
- `.gsd/PROJECT.md` — living project description - `.gsd/PROJECT.md` — living project description
- `.gsd/REQUIREMENTS.md` — requirement contract - `.gsd/REQUIREMENTS.md` — requirement contract
- `.gsd/DECISIONS.md` — architectural decisions - `.gsd/DECISIONS.md` — architectural decisions
@ -50,7 +50,7 @@ Share planning artifacts (milestones, roadmaps, decisions) while keeping runtime
### 3. Commit the Preferences ### 3. Commit the Preferences
```bash ```bash
git add .gsd/preferences.md git add .gsd/PREFERENCES.md
git commit -m "chore: enable GSD team workflow" git commit -m "chore: enable GSD team workflow"
``` ```
@ -71,7 +71,7 @@ If you have an existing project with `.gsd/` blanket-ignored:
1. Ensure no milestones are in progress (clean state) 1. Ensure no milestones are in progress (clean state)
2. Update `.gitignore` to use the selective pattern above 2. Update `.gitignore` to use the selective pattern above
3. Add `unique_milestone_ids: true` to `.gsd/preferences.md` 3. Add `unique_milestone_ids: true` to `.gsd/PREFERENCES.md`
4. Optionally rename existing milestones to use unique IDs: 4. Optionally rename existing milestones to use unique IDs:
``` ```
I have turned on unique milestone ids, please update all old milestone I have turned on unique milestone ids, please update all old milestone

View file

@ -3,7 +3,7 @@ title: "Configuration"
description: "Preferences, model selection, MCP servers, hooks, and all settings." description: "Preferences, model selection, MCP servers, hooks, and all settings."
--- ---
GSD preferences live in `~/.gsd/preferences.md` (global) or `.gsd/preferences.md` (project-local). Manage interactively with `/gsd prefs`. GSD preferences live in `~/.gsd/PREFERENCES.md` (global) or `.gsd/PREFERENCES.md` (project-local). Manage interactively with `/gsd prefs`.
## Preferences commands ## Preferences commands
@ -40,8 +40,8 @@ token_profile: balanced
| Scope | Path | Applies to | | Scope | Path | Applies to |
|-------|------|-----------| |-------|------|-----------|
| Global | `~/.gsd/preferences.md` | All projects | | Global | `~/.gsd/PREFERENCES.md` | All projects |
| Project | `.gsd/preferences.md` | Current project only | | Project | `.gsd/PREFERENCES.md` | Current project only |
**Merge behavior:** **Merge behavior:**
- **Scalar fields** — project wins if defined - **Scalar fields** — project wins if defined

View file

@ -10,7 +10,7 @@ GSD supports multi-user workflows where several developers work on the same repo
### 1. Set team mode ### 1. Set team mode
```yaml ```yaml
# .gsd/preferences.md (project-level, committed to git) # .gsd/PREFERENCES.md (project-level, committed to git)
--- ---
version: 1 version: 1
mode: team mode: team
@ -43,7 +43,7 @@ Share planning artifacts while keeping runtime files local:
### 3. Commit ### 3. Commit
```bash ```bash
git add .gsd/preferences.md git add .gsd/PREFERENCES.md
git commit -m "chore: enable GSD team workflow" git commit -m "chore: enable GSD team workflow"
``` ```

View file

@ -16,7 +16,7 @@ import { appRoot } from "./app-paths.js";
// boundary — this file is compiled by tsc, but preferences.ts is loaded // boundary — this file is compiled by tsc, but preferences.ts is loaded
// via jiti at runtime. Importing it as .js fails because no .js exists // via jiti at runtime. Importing it as .js fails because no .js exists
// in dist/. See #592, #1110. // in dist/. See #592, #1110.
const GLOBAL_PREFERENCES_PATH = join(appRoot, "preferences.md"); const GLOBAL_PREFERENCES_PATH = join(appRoot, "PREFERENCES.md");
export function saveRemoteQuestionsConfig(channel: "slack" | "discord" | "telegram", channelId: string): void { export function saveRemoteQuestionsConfig(channel: "slack" | "discord" | "telegram", channelId: string): void {
const prefsPath = GLOBAL_PREFERENCES_PATH; const prefsPath = GLOBAL_PREFERENCES_PATH;

View file

@ -771,7 +771,7 @@ export async function ensurePreferencesFile(
scope: "global" | "project", scope: "global" | "project",
): Promise<void> { ): Promise<void> {
if (!existsSync(path)) { if (!existsSync(path)) {
const template = await loadFile(join(dirname(fileURLToPath(import.meta.url)), "templates", "preferences.md")); const template = await loadFile(join(dirname(fileURLToPath(import.meta.url)), "templates", "PREFERENCES.md"));
if (!template) { if (!template) {
ctx.ui.notify("Could not load GSD preferences template.", "error"); ctx.ui.notify("Could not load GSD preferences template.", "error");
return; return;

View file

@ -359,8 +359,8 @@ function detectV2Gsd(basePath: string): V2Detection | null {
if (!existsSync(gsdPath)) return null; if (!existsSync(gsdPath)) return null;
const hasPreferences = const hasPreferences =
existsSync(join(gsdPath, "preferences.md")) || existsSync(join(gsdPath, "PREFERENCES.md")) ||
existsSync(join(gsdPath, "PREFERENCES.md")); existsSync(join(gsdPath, "preferences.md"));
const hasContext = existsSync(join(gsdPath, "CONTEXT.md")); const hasContext = existsSync(join(gsdPath, "CONTEXT.md"));
@ -714,8 +714,8 @@ function detectVerificationCommands(
*/ */
export function hasGlobalSetup(): boolean { export function hasGlobalSetup(): boolean {
return ( return (
existsSync(join(gsdHome, "preferences.md")) || existsSync(join(gsdHome, "PREFERENCES.md")) ||
existsSync(join(gsdHome, "PREFERENCES.md")) existsSync(join(gsdHome, "preferences.md"))
); );
} }
@ -728,8 +728,8 @@ export function isFirstEverLaunch(): boolean {
// If we have preferences, not first launch // If we have preferences, not first launch
if ( if (
existsSync(join(gsdHome, "preferences.md")) || existsSync(join(gsdHome, "PREFERENCES.md")) ||
existsSync(join(gsdHome, "PREFERENCES.md")) existsSync(join(gsdHome, "preferences.md"))
) { ) {
return false; return false;
} }

View file

@ -1,6 +1,6 @@
# GSD Preferences Reference # GSD Preferences Reference
Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md` (project). Full documentation for `~/.gsd/PREFERENCES.md` (global) and `.gsd/PREFERENCES.md` (project).
--- ---
@ -51,8 +51,8 @@ skill_rules: []
Preferences are loaded from two locations and merged: Preferences are loaded from two locations and merged:
1. **Global:** `~/.gsd/preferences.md` — applies to all projects 1. **Global:** `~/.gsd/PREFERENCES.md` — applies to all projects
2. **Project:** `.gsd/preferences.md` — applies to the current project only 2. **Project:** `.gsd/PREFERENCES.md` — applies to the current project only
**Merge behavior** (see `mergePreferences()` in `preferences.ts`): **Merge behavior** (see `mergePreferences()` in `preferences.ts`):

View file

@ -1,8 +1,8 @@
/** /**
* GSD bootstrappers for .gitignore and preferences.md * GSD bootstrappers for .gitignore and PREFERENCES.md
* *
* Ensures baseline .gitignore exists with universally-correct patterns. * Ensures baseline .gitignore exists with universally-correct patterns.
* Creates an empty preferences.md template if it doesn't exist. * Creates an empty PREFERENCES.md template if it doesn't exist.
* Both idempotent non-destructive if already present. * Both idempotent non-destructive if already present.
*/ */
@ -216,16 +216,16 @@ export function untrackRuntimeFiles(basePath: string): void {
} }
/** /**
* Ensure basePath/.gsd/preferences.md exists as an empty template. * Ensure basePath/.gsd/PREFERENCES.md exists as an empty template.
* Creates the file with frontmatter only if it doesn't exist. * Creates the file with frontmatter only if it doesn't exist.
* Returns true if created, false if already exists. * Returns true if created, false if already exists.
* *
* Checks both lowercase (canonical) and uppercase (legacy) to avoid * Checks both uppercase (canonical) and lowercase (legacy) to avoid
* creating a duplicate when an uppercase file already exists. * creating a duplicate when a lowercase file already exists.
*/ */
export function ensurePreferences(basePath: string): boolean { export function ensurePreferences(basePath: string): boolean {
const preferencesPath = join(gsdRoot(basePath), "preferences.md"); const preferencesPath = join(gsdRoot(basePath), "PREFERENCES.md");
const legacyPath = join(gsdRoot(basePath), "PREFERENCES.md"); const legacyPath = join(gsdRoot(basePath), "preferences.md");
if (existsSync(preferencesPath) || existsSync(legacyPath)) { if (existsSync(preferencesPath) || existsSync(legacyPath)) {
return false; return false;

View file

@ -422,9 +422,9 @@ function bootstrapGsdDirectory(
const gsd = gsdRoot(basePath); const gsd = gsdRoot(basePath);
mkdirSync(join(gsd, "milestones"), { recursive: true }); mkdirSync(join(gsd, "milestones"), { recursive: true });
// Write preferences.md from wizard answers // Write PREFERENCES.md from wizard answers
const preferencesContent = buildPreferencesFile(prefs); const preferencesContent = buildPreferencesFile(prefs);
writeFileSync(join(gsd, "preferences.md"), preferencesContent, "utf-8"); writeFileSync(join(gsd, "PREFERENCES.md"), preferencesContent, "utf-8");
// Seed CONTEXT.md with detected project signals // Seed CONTEXT.md with detected project signals
const contextContent = buildContextSeed(signals); const contextContent = buildContextSeed(signals);

View file

@ -308,7 +308,7 @@ export function resolveContextSelection(): import("./types.js").ContextSelection
} }
/** /**
* Resolve the search provider preference from preferences.md. * Resolve the search provider preference from PREFERENCES.md.
* Returns undefined if not configured (caller falls back to existing behavior). * Returns undefined if not configured (caller falls back to existing behavior).
*/ */
export function resolveSearchProviderFromPreferences(): GSDPreferences["search_provider"] | undefined { export function resolveSearchProviderFromPreferences(): GSDPreferences["search_provider"] | undefined {

View file

@ -87,7 +87,7 @@ function gsdHome(): string {
} }
function globalPreferencesPath(): string { function globalPreferencesPath(): string {
return join(gsdHome(), "preferences.md"); return join(gsdHome(), "PREFERENCES.md");
} }
function legacyGlobalPreferencesPath(): string { function legacyGlobalPreferencesPath(): string {
@ -95,16 +95,16 @@ function legacyGlobalPreferencesPath(): string {
} }
function projectPreferencesPath(): string { function projectPreferencesPath(): string {
return join(gsdRoot(process.cwd()), "preferences.md");
}
// Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
// Check uppercase as a fallback so those files aren't silently ignored.
function globalPreferencesPathUppercase(): string {
return join(gsdHome(), "PREFERENCES.md");
}
function projectPreferencesPathUppercase(): string {
return join(gsdRoot(process.cwd()), "PREFERENCES.md"); return join(gsdRoot(process.cwd()), "PREFERENCES.md");
} }
// Legacy: older versions used lowercase preferences.md.
// Check lowercase as a fallback so those files aren't silently ignored.
function globalPreferencesPathLegacy(): string {
return join(gsdHome(), "preferences.md");
}
function projectPreferencesPathLegacy(): string {
return join(gsdRoot(process.cwd()), "preferences.md");
}
export function getGlobalGSDPreferencesPath(): string { export function getGlobalGSDPreferencesPath(): string {
return globalPreferencesPath(); return globalPreferencesPath();
@ -122,13 +122,13 @@ export function getProjectGSDPreferencesPath(): string {
export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null { export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null {
return loadPreferencesFile(globalPreferencesPath(), "global") return loadPreferencesFile(globalPreferencesPath(), "global")
?? loadPreferencesFile(globalPreferencesPathUppercase(), "global") ?? loadPreferencesFile(globalPreferencesPathLegacy(), "global")
?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global"); ?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
} }
export function loadProjectGSDPreferences(): LoadedGSDPreferences | null { export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
return loadPreferencesFile(projectPreferencesPath(), "project") return loadPreferencesFile(projectPreferencesPath(), "project")
?? loadPreferencesFile(projectPreferencesPathUppercase(), "project"); ?? loadPreferencesFile(projectPreferencesPathLegacy(), "project");
} }
export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null { export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null {
@ -223,7 +223,7 @@ export function parsePreferencesMarkdown(content: string): GSDPreferences | null
if (!_warnedUnrecognizedFormat) { if (!_warnedUnrecognizedFormat) {
_warnedUnrecognizedFormat = true; _warnedUnrecognizedFormat = true;
console.warn("[parsePreferencesMarkdown] preferences.md exists but uses an unrecognized format — skipping."); console.warn("[parsePreferencesMarkdown] PREFERENCES.md exists but uses an unrecognized format — skipping.");
} }
return null; return null;
} }
@ -502,7 +502,7 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
* Resolve the effective git isolation mode from preferences. * Resolve the effective git isolation mode from preferences.
* Returns "none" (default), "worktree", or "branch". * Returns "none" (default), "worktree", or "branch".
* *
* Default is "none" so GSD works out of the box without preferences.md. * Default is "none" so GSD works out of the box without PREFERENCES.md.
* Worktree isolation requires explicit opt-in because it depends on git * Worktree isolation requires explicit opt-in because it depends on git
* branch infrastructure that must be set up before use. * branch infrastructure that must be set up before use.
*/ */

View file

@ -92,7 +92,7 @@ Titles live inside file content (headings, frontmatter), not in file or director
### Isolation Model ### Isolation Model
Auto-mode supports three isolation modes (configured in `.gsd/preferences.md` under `taskIsolation.mode`): Auto-mode supports three isolation modes (configured in `.gsd/PREFERENCES.md` under `taskIsolation.mode`):
- **worktree** (default): Work happens in `.gsd/worktrees/<MID>/`, a full git worktree on the `milestone/<MID>` branch. Each worktree has its own working copy and `.gsd/` directory. Squash-merged back to the integration branch on milestone completion. - **worktree** (default): Work happens in `.gsd/worktrees/<MID>/`, a full git worktree on the `milestone/<MID>` branch. Each worktree has its own working copy and `.gsd/` directory. Squash-merged back to the integration branch on milestone completion.
- **branch**: Work happens in the project root on a `milestone/<MID>` branch. No worktree directory — files are checked out in-place. - **branch**: Work happens in the project root on a `milestone/<MID>` branch. No worktree directory — files are checked out in-place.

View file

@ -524,7 +524,7 @@ export class RuleRegistry {
formatHookStatus(): string { formatHookStatus(): string {
const entries = this.getHookStatus(); const entries = this.getHookStatus();
if (entries.length === 0) { if (entries.length === 0) {
return "No hooks configured. Add post_unit_hooks or pre_dispatch_hooks to .gsd/preferences.md"; return "No hooks configured. Add post_unit_hooks or pre_dispatch_hooks to .gsd/PREFERENCES.md";
} }
const lines: string[] = ["Configured Hooks:", ""]; const lines: string[] = ["Configured Hooks:", ""];

View file

@ -126,7 +126,7 @@ describe(
before(() => { before(() => {
tempDir = mkdtempSync(join(tmpdir(), 'gsd-tui-test-')); tempDir = mkdtempSync(join(tmpdir(), 'gsd-tui-test-'));
prefsPath = join(tempDir, 'preferences.md'); prefsPath = join(tempDir, 'PREFERENCES.md');
prefs = { version: 1 }; prefs = { version: 1 };
}); });

View file

@ -99,7 +99,7 @@ test("detectProjectState: detects preferences in .gsd/", (t) => {
t.after(() => cleanup(dir)); t.after(() => cleanup(dir));
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true }); mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
writeFileSync(join(dir, ".gsd", "preferences.md"), "---\nversion: 1\n---\n", "utf-8"); writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), "---\nversion: 1\n---\n", "utf-8");
const result = detectProjectState(dir); const result = detectProjectState(dir);
assert.ok(result.v2); assert.ok(result.v2);
assert.equal(result.v2!.hasPreferences, true); assert.equal(result.v2!.hasPreferences, true);

View file

@ -64,11 +64,11 @@ _None_
return dir; return dir;
} }
/** Write a .gsd/preferences.md with the given git isolation mode. */ /** Write a .gsd/PREFERENCES.md with the given git isolation mode. */
function writePreferencesFile(dir: string, isolation: "none" | "worktree" | "branch"): void { function writePreferencesFile(dir: string, isolation: "none" | "worktree" | "branch"): void {
const gsdDir = join(dir, ".gsd"); const gsdDir = join(dir, ".gsd");
mkdirSync(gsdDir, { recursive: true }); mkdirSync(gsdDir, { recursive: true });
writeFileSync(join(gsdDir, "preferences.md"), `---\ngit:\n isolation: "${isolation}"\n---\n`); writeFileSync(join(gsdDir, "PREFERENCES.md"), `---\ngit:\n isolation: "${isolation}"\n---\n`);
} }
/** Create a repo with an in-progress milestone. */ /** Create a repo with an in-progress milestone. */
@ -302,7 +302,7 @@ describe('doctor-git', async () => {
// ─── Test 7: none-mode skips orphaned worktree check ─────────────── // ─── Test 7: none-mode skips orphaned worktree check ───────────────
// NOTE: loadEffectiveGSDPreferences() resolves PROJECT_PREFERENCES_PATH // NOTE: loadEffectiveGSDPreferences() resolves PROJECT_PREFERENCES_PATH
// at module load time from process.cwd(). We write the prefs file to // at module load time from process.cwd(). We write the prefs file to
// the test runner's cwd .gsd/preferences.md and clean up afterwards. // the test runner's cwd .gsd/PREFERENCES.md and clean up afterwards.
if (process.platform !== "win32") { if (process.platform !== "win32") {
test('none-mode skips orphaned worktree', async () => { test('none-mode skips orphaned worktree', async () => {
const dir = createRepoWithCompletedMilestone(); const dir = createRepoWithCompletedMilestone();
@ -409,7 +409,7 @@ describe('doctor-git', async () => {
cleanups.push(dir); cleanups.push(dir);
run("git branch trunk", dir); run("git branch trunk", dir);
writeFileSync(join(dir, ".gsd", "preferences.md"), `---\ngit:\n isolation: "worktree"\n main_branch: "trunk"\n---\n`); writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), `---\ngit:\n isolation: "worktree"\n main_branch: "trunk"\n---\n`);
const metaPath = join(dir, ".gsd", "milestones", "M001", "M001-META.json"); const metaPath = join(dir, ".gsd", "milestones", "M001", "M001-META.json");
writeFileSync(metaPath, JSON.stringify({ integrationBranch: "feat/does-not-exist" }, null, 2)); writeFileSync(metaPath, JSON.stringify({ integrationBranch: "feat/does-not-exist" }, null, 2));

View file

@ -297,7 +297,7 @@ describe('doctor-proactive', async () => {
cleanups.push(dir); cleanups.push(dir);
run("git branch trunk", dir); run("git branch trunk", dir);
writeFileSync(join(dir, ".gsd", "preferences.md"), `---\ngit:\n main_branch: "trunk"\n---\n`); writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), `---\ngit:\n main_branch: "trunk"\n---\n`);
const metaPath = join(dir, ".gsd", "milestones", "M001", "M001-META.json"); const metaPath = join(dir, ".gsd", "milestones", "M001", "M001-META.json");
writeFileSync(metaPath, JSON.stringify({ integrationBranch: "feature/missing" }, null, 2)); writeFileSync(metaPath, JSON.stringify({ integrationBranch: "feature/missing" }, null, 2));

View file

@ -419,7 +419,7 @@ test("runProviderChecks uses provider-qualified anthropic-vertex model IDs", ()
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-repo-"))); const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-prefix-repo-")));
mkdirSync(join(repo, ".gsd"), { recursive: true }); mkdirSync(join(repo, ".gsd"), { recursive: true });
writeFileSync( writeFileSync(
join(repo, ".gsd", "preferences.md"), join(repo, ".gsd", "PREFERENCES.md"),
[ [
"---", "---",
"models:", "models:",
@ -454,7 +454,7 @@ test("runProviderChecks uses object provider field for anthropic-vertex models",
const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-repo-"))); const repo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-providers-vertex-provider-repo-")));
mkdirSync(join(repo, ".gsd"), { recursive: true }); mkdirSync(join(repo, ".gsd"), { recursive: true });
writeFileSync( writeFileSync(
join(repo, ".gsd", "preferences.md"), join(repo, ".gsd", "PREFERENCES.md"),
[ [
"---", "---",
"models:", "models:",

View file

@ -1142,7 +1142,7 @@ describe('git-service', async () => {
mkdirSync(join(repo, ".gsd", "runtime"), { recursive: true }); mkdirSync(join(repo, ".gsd", "runtime"), { recursive: true });
mkdirSync(join(repo, ".gsd", "activity"), { recursive: true }); mkdirSync(join(repo, ".gsd", "activity"), { recursive: true });
writeFileSync(join(repo, ".gsd", "milestones", "M001", "ROADMAP.md"), "# Roadmap"); writeFileSync(join(repo, ".gsd", "milestones", "M001", "ROADMAP.md"), "# Roadmap");
writeFileSync(join(repo, ".gsd", "preferences.md"), "---\nversion: 1\n---"); writeFileSync(join(repo, ".gsd", "PREFERENCES.md"), "---\nversion: 1\n---");
writeFileSync(join(repo, ".gsd", "STATE.md"), "# State"); writeFileSync(join(repo, ".gsd", "STATE.md"), "# State");
writeFileSync(join(repo, ".gsd", "runtime", "units.json"), "{}"); writeFileSync(join(repo, ".gsd", "runtime", "units.json"), "{}");
writeFileSync(join(repo, ".gsd", "activity", "log.jsonl"), "{}"); writeFileSync(join(repo, ".gsd", "activity", "log.jsonl"), "{}");

View file

@ -123,7 +123,7 @@ test("init-wizard: v2 .gsd/ preferences detected", (t) => {
const dir = makeTempDir("prefs-detect"); const dir = makeTempDir("prefs-detect");
try { try {
mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true }); mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
writeFileSync(join(dir, ".gsd", "preferences.md"), "---\nversion: 1\nmode: solo\n---\n", "utf-8"); writeFileSync(join(dir, ".gsd", "PREFERENCES.md"), "---\nversion: 1\nmode: solo\n---\n", "utf-8");
const detection = detectProjectState(dir); const detection = detectProjectState(dir);
assert.ok(detection.v2); assert.ok(detection.v2);

View file

@ -8,7 +8,7 @@
* Uses the writeRunnerPreferences pattern from doctor-git.test.ts: * Uses the writeRunnerPreferences pattern from doctor-git.test.ts:
* PROJECT_PREFERENCES_PATH is a module-level constant frozen at import * PROJECT_PREFERENCES_PATH is a module-level constant frozen at import
* time, so process.chdir() won't redirect preference loading. We write * time, so process.chdir() won't redirect preference loading. We write
* prefs to the runner's cwd .gsd/preferences.md and clean up in finally. * prefs to the runner's cwd .gsd/PREFERENCES.md and clean up in finally.
*/ */
import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs"; import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
@ -24,7 +24,7 @@ import assert from 'node:assert/strict';
// --- Preferences helpers (same pattern as doctor-git.test.ts K001) --- // --- Preferences helpers (same pattern as doctor-git.test.ts K001) ---
const RUNNER_PREFS_PATH = join(process.cwd(), ".gsd", "preferences.md"); const RUNNER_PREFS_PATH = join(process.cwd(), ".gsd", "PREFERENCES.md");
function writeRunnerPreferences(isolation: "none" | "worktree" | "branch"): void { function writeRunnerPreferences(isolation: "none" | "worktree" | "branch"): void {
mkdirSync(join(process.cwd(), ".gsd"), { recursive: true }); mkdirSync(join(process.cwd(), ".gsd"), { recursive: true });
@ -72,12 +72,12 @@ try {
// Test 4: shouldUseWorktreeIsolation returns false for no prefs (default: none) // Test 4: shouldUseWorktreeIsolation returns false for no prefs (default: none)
// Worktree isolation requires explicit opt-in — default is "none" so GSD // Worktree isolation requires explicit opt-in — default is "none" so GSD
// works out of the box without preferences.md (#2480). // works out of the box without PREFERENCES.md (#2480).
// Skip if global prefs exist — they override the default and this test // Skip if global prefs exist — they override the default and this test
// cannot control ~/.gsd/preferences.md. // cannot control ~/.gsd/PREFERENCES.md.
test('shouldUseWorktreeIsolation returns false for no prefs (default: none)', () => { test('shouldUseWorktreeIsolation returns false for no prefs (default: none)', () => {
const globalPrefsExist = existsSync(join(homedir(), ".gsd", "preferences.md")) const globalPrefsExist = existsSync(join(homedir(), ".gsd", "PREFERENCES.md"))
|| existsSync(join(homedir(), ".gsd", "PREFERENCES.md")); || existsSync(join(homedir(), ".gsd", "PREFERENCES.md"));
if (!globalPrefsExist) { if (!globalPrefsExist) {
try { try {
@ -91,9 +91,9 @@ test('shouldUseWorktreeIsolation returns false for no prefs (default: none)', ()
} }
}); });
// Test 5: getIsolationMode returns "none" when no preferences.md exists (#2480) // Test 5: getIsolationMode returns "none" when no PREFERENCES.md exists (#2480)
test('getIsolationMode returns "none" with no prefs (default)', () => { test('getIsolationMode returns "none" with no prefs (default)', () => {
const globalPrefsExist = existsSync(join(homedir(), ".gsd", "preferences.md")) const globalPrefsExist = existsSync(join(homedir(), ".gsd", "PREFERENCES.md"))
|| existsSync(join(homedir(), ".gsd", "PREFERENCES.md")); || existsSync(join(homedir(), ".gsd", "PREFERENCES.md"));
if (!globalPrefsExist) { if (!globalPrefsExist) {
try { try {

View file

@ -45,7 +45,7 @@ test("getIsolationMode defaults to none when preferences have no isolation setti
// Validate the default via validatePreferences: when no isolation is set, // Validate the default via validatePreferences: when no isolation is set,
// preferences.git.isolation is undefined, and getIsolationMode returns "none". // preferences.git.isolation is undefined, and getIsolationMode returns "none".
// Default changed from "worktree" to "none" so GSD works out of the box // Default changed from "worktree" to "none" so GSD works out of the box
// without preferences.md (#2480). // without PREFERENCES.md (#2480).
const { preferences } = validatePreferences({}); const { preferences } = validatePreferences({});
assert.equal(preferences.git?.isolation, undefined, "no isolation in empty prefs"); assert.equal(preferences.git?.isolation, undefined, "no isolation in empty prefs");
const isolation = preferences.git?.isolation; const isolation = preferences.git?.isolation;

View file

@ -63,13 +63,13 @@ test("show_token_cost defaults to undefined (disabled) when not set", () => {
assert.equal(preferences.show_token_cost, undefined); assert.equal(preferences.show_token_cost, undefined);
}); });
test("empty preferences.md does not enable show_token_cost", () => { test("empty PREFERENCES.md does not enable show_token_cost", () => {
const prefs = parsePreferencesMarkdown("---\nversion: 1\n---\n"); const prefs = parsePreferencesMarkdown("---\nversion: 1\n---\n");
assert.ok(prefs); assert.ok(prefs);
assert.equal(prefs.show_token_cost, undefined); assert.equal(prefs.show_token_cost, undefined);
}); });
test("preferences.md with show_token_cost: true enables the preference", () => { test("PREFERENCES.md with show_token_cost: true enables the preference", () => {
const prefs = parsePreferencesMarkdown("---\nshow_token_cost: true\n---\n"); const prefs = parsePreferencesMarkdown("---\nshow_token_cost: true\n---\n");
assert.ok(prefs); assert.ok(prefs);
assert.equal(prefs.show_token_cost, true); assert.equal(prefs.show_token_cost, true);

View file

@ -28,7 +28,7 @@ export const MAX_NATIVE_SEARCHES_PER_SESSION = 15;
/** When true, skip native web search injection and keep Brave/custom tools active on Anthropic. */ /** When true, skip native web search injection and keep Brave/custom tools active on Anthropic. */
export function preferBraveSearch(): boolean { export function preferBraveSearch(): boolean {
// preferences.md takes priority over env var // PREFERENCES.md takes priority over env var
const prefsPref = resolveSearchProviderFromPreferences(); const prefsPref = resolveSearchProviderFromPreferences();
if (prefsPref === "brave" || prefsPref === "tavily" || prefsPref === "ollama") return true; if (prefsPref === "brave" || prefsPref === "tavily" || prefsPref === "ollama") return true;
if (prefsPref === "native") return false; if (prefsPref === "native") return false;

View file

@ -105,7 +105,7 @@ export function resolveSearchProvider(overridePreference?: string): SearchProvid
if (overridePreference && VALID_PREFERENCES.has(overridePreference)) { if (overridePreference && VALID_PREFERENCES.has(overridePreference)) {
pref = overridePreference as SearchProviderPreference pref = overridePreference as SearchProviderPreference
} else { } else {
// preferences.md takes priority over auth.json // PREFERENCES.md takes priority over auth.json
const mdPref = resolveSearchProviderFromPreferences() const mdPref = resolveSearchProviderFromPreferences()
if (mdPref && mdPref !== 'auto' && mdPref !== 'native') { if (mdPref && mdPref !== 'auto' && mdPref !== 'native') {
pref = mdPref as SearchProviderPreference pref = mdPref as SearchProviderPreference

View file

@ -38,7 +38,7 @@ export async function collectHooksData(projectCwdOverride?: string): Promise<Hoo
} }
// getHookStatus() internally calls resolvePostUnitHooks() and resolvePreDispatchHooks() // getHookStatus() internally calls resolvePostUnitHooks() and resolvePreDispatchHooks()
// from preferences.ts, which read from process.cwd()/.gsd/preferences.md. // from preferences.ts, which read from process.cwd()/.gsd/PREFERENCES.md.
// We set cwd to projectCwd so preferences resolution finds the right files. // We set cwd to projectCwd so preferences resolution finds the right files.
// In a cold child process, cycleCounts is empty, so activeCycles will be {}. // In a cold child process, cycleCounts is empty, so activeCycles will be {}.
const script = [ const script = [

View file

@ -11,7 +11,7 @@ const NO_STORE = { "Cache-Control": "no-store" } as const
// ─── Helpers (same pattern as remote-questions/route.ts) ───────────────────── // ─── Helpers (same pattern as remote-questions/route.ts) ─────────────────────
function getPreferencesPath(): string { function getPreferencesPath(): string {
return join(homedir(), ".gsd", "preferences.md") return join(homedir(), ".gsd", "PREFERENCES.md")
} }
function parseFrontmatter(content: string): { data: Record<string, unknown>; body: string } { function parseFrontmatter(content: string): { data: Record<string, unknown>; body: string } {

View file

@ -84,7 +84,7 @@ function maskToken(token: string): string {
// ─── Helpers ────────────────────────────────────────────────────────────────── // ─── Helpers ──────────────────────────────────────────────────────────────────
function getPreferencesPath(): string { function getPreferencesPath(): string {
return join(homedir(), ".gsd", "preferences.md") return join(homedir(), ".gsd", "PREFERENCES.md")
} }
function clamp(value: number | undefined, defaultVal: number, min: number, max: number): number { function clamp(value: number | undefined, defaultVal: number, min: number, max: number): number {

View file

@ -1200,7 +1200,7 @@ export function ExperimentalPanel() {
{data && ( {data && (
<p className="text-[11px] text-muted-foreground"> <p className="text-[11px] text-muted-foreground">
Changes are written to{" "} Changes are written to{" "}
<span className="font-mono">{prefs?.path ?? "~/.gsd/preferences.md"}</span> <span className="font-mono">{prefs?.path ?? "~/.gsd/PREFERENCES.md"}</span>
{" "}and take effect on the next session. {" "}and take effect on the next session.
</p> </p>
)} )}