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
### 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
- **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

View file

@ -134,7 +134,7 @@ Quick filesystem scan (no heavy reads):
### 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
- Creates .gsd/milestones/
- Creates .gsd/preferences.md (from wizard answers)
- Creates .gsd/PREFERENCES.md (from wizard answers)
- Creates .gitignore entries
- Seeds CONTEXT.md with detected project signals
- 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()`
### 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
### 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:
```yaml
# .gsd/preferences.md
# .gsd/PREFERENCES.md
---
version: 1
mode: team

View file

@ -521,7 +521,7 @@ An auto-generated `index.html` shows all reports with progression metrics across
### 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
---
@ -672,7 +672,7 @@ The best practice for working in teams is to ensure unique milestone names acros
### 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
---
@ -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`
@ -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)
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.`
5. Commit to git

View file

@ -1,14 +1,14 @@
# 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
| Command | Description |
|---------|-------------|
| `/gsd prefs` | Open the global preferences wizard (default) |
| `/gsd prefs global` | Interactive wizard for global preferences (`~/.gsd/preferences.md`) |
| `/gsd prefs project` | Interactive wizard for project 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 status` | Show current preference files, merged values, and skill resolution status |
| `/gsd prefs wizard` | Alias for `/gsd prefs global` |
| `/gsd prefs setup` | Alias for `/gsd prefs wizard` — creates preferences file if missing |
@ -42,8 +42,8 @@ token_profile: balanced
| Scope | Path | Applies to |
|-------|------|-----------|
| Global | `~/.gsd/preferences.md` | All projects |
| Project | `.gsd/preferences.md` | Current project only |
| Global | `~/.gsd/PREFERENCES.md` | All projects |
| Project | `.gsd/PREFERENCES.md` | Current project only |
**Merge behavior:**
- **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
Add to `~/.gsd/preferences.md` or `.gsd/preferences.md`:
Add to `~/.gsd/PREFERENCES.md` or `.gsd/PREFERENCES.md`:
```yaml
---

View file

@ -16,7 +16,7 @@ The setup wizard:
3. Lists servers the bot belongs to (or lets you pick)
4. Lists text channels in the selected server
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:**
- A Discord bot application with a token (from [Discord Developer Portal](https://discord.com/developers/applications))
@ -65,7 +65,7 @@ The setup wizard:
## Configuration
Remote questions are configured in `~/.gsd/preferences.md`:
Remote questions are configured in `~/.gsd/PREFERENCES.md`:
```yaml
remote_questions:

View file

@ -257,7 +257,7 @@ models:
## How the Pieces Fit Together
```
preferences.md
PREFERENCES.md
└─ token_profile: balanced
├─ resolveProfileDefaults() → model defaults + phase skip defaults
├─ 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:
```yaml
# .gsd/preferences.md (project-level, committed to git)
# .gsd/PREFERENCES.md (project-level, committed to git)
---
version: 1
mode: team
@ -38,7 +38,7 @@ Share planning artifacts (milestones, roadmaps, decisions) while keeping runtime
```
**What gets shared** (committed to git):
- `.gsd/preferences.md` — project preferences
- `.gsd/PREFERENCES.md` — project preferences
- `.gsd/PROJECT.md` — living project description
- `.gsd/REQUIREMENTS.md` — requirement contract
- `.gsd/DECISIONS.md` — architectural decisions
@ -50,7 +50,7 @@ Share planning artifacts (milestones, roadmaps, decisions) while keeping runtime
### 3. Commit the Preferences
```bash
git add .gsd/preferences.md
git add .gsd/PREFERENCES.md
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)
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:
```
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."
---
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
@ -40,8 +40,8 @@ token_profile: balanced
| Scope | Path | Applies to |
|-------|------|-----------|
| Global | `~/.gsd/preferences.md` | All projects |
| Project | `.gsd/preferences.md` | Current project only |
| Global | `~/.gsd/PREFERENCES.md` | All projects |
| Project | `.gsd/PREFERENCES.md` | Current project only |
**Merge behavior:**
- **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
```yaml
# .gsd/preferences.md (project-level, committed to git)
# .gsd/PREFERENCES.md (project-level, committed to git)
---
version: 1
mode: team
@ -43,7 +43,7 @@ Share planning artifacts while keeping runtime files local:
### 3. Commit
```bash
git add .gsd/preferences.md
git add .gsd/PREFERENCES.md
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
// via jiti at runtime. Importing it as .js fails because no .js exists
// 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 {
const prefsPath = GLOBAL_PREFERENCES_PATH;

View file

@ -771,7 +771,7 @@ export async function ensurePreferencesFile(
scope: "global" | "project",
): Promise<void> {
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) {
ctx.ui.notify("Could not load GSD preferences template.", "error");
return;

View file

@ -359,8 +359,8 @@ function detectV2Gsd(basePath: string): V2Detection | null {
if (!existsSync(gsdPath)) return null;
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"));
@ -714,8 +714,8 @@ function detectVerificationCommands(
*/
export function hasGlobalSetup(): boolean {
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 (
existsSync(join(gsdHome, "preferences.md")) ||
existsSync(join(gsdHome, "PREFERENCES.md"))
existsSync(join(gsdHome, "PREFERENCES.md")) ||
existsSync(join(gsdHome, "preferences.md"))
) {
return false;
}

View file

@ -1,6 +1,6 @@
# 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:
1. **Global:** `~/.gsd/preferences.md` — applies to all projects
2. **Project:** `.gsd/preferences.md` — applies to the current project only
1. **Global:** `~/.gsd/PREFERENCES.md` — applies to all projects
2. **Project:** `.gsd/PREFERENCES.md` — applies to the current project only
**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.
* 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.
*/
@ -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.
* Returns true if created, false if already exists.
*
* Checks both lowercase (canonical) and uppercase (legacy) to avoid
* creating a duplicate when an uppercase file already exists.
* Checks both uppercase (canonical) and lowercase (legacy) to avoid
* creating a duplicate when a lowercase file already exists.
*/
export function ensurePreferences(basePath: string): boolean {
const preferencesPath = join(gsdRoot(basePath), "preferences.md");
const legacyPath = join(gsdRoot(basePath), "PREFERENCES.md");
const preferencesPath = join(gsdRoot(basePath), "PREFERENCES.md");
const legacyPath = join(gsdRoot(basePath), "preferences.md");
if (existsSync(preferencesPath) || existsSync(legacyPath)) {
return false;

View file

@ -422,9 +422,9 @@ function bootstrapGsdDirectory(
const gsd = gsdRoot(basePath);
mkdirSync(join(gsd, "milestones"), { recursive: true });
// Write preferences.md from wizard answers
// Write PREFERENCES.md from wizard answers
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
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).
*/
export function resolveSearchProviderFromPreferences(): GSDPreferences["search_provider"] | undefined {

View file

@ -87,7 +87,7 @@ function gsdHome(): string {
}
function globalPreferencesPath(): string {
return join(gsdHome(), "preferences.md");
return join(gsdHome(), "PREFERENCES.md");
}
function legacyGlobalPreferencesPath(): string {
@ -95,16 +95,16 @@ function legacyGlobalPreferencesPath(): 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");
}
// 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 {
return globalPreferencesPath();
@ -122,13 +122,13 @@ export function getProjectGSDPreferencesPath(): string {
export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null {
return loadPreferencesFile(globalPreferencesPath(), "global")
?? loadPreferencesFile(globalPreferencesPathUppercase(), "global")
?? loadPreferencesFile(globalPreferencesPathLegacy(), "global")
?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
}
export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
return loadPreferencesFile(projectPreferencesPath(), "project")
?? loadPreferencesFile(projectPreferencesPathUppercase(), "project");
?? loadPreferencesFile(projectPreferencesPathLegacy(), "project");
}
export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null {
@ -223,7 +223,7 @@ export function parsePreferencesMarkdown(content: string): GSDPreferences | null
if (!_warnedUnrecognizedFormat) {
_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;
}
@ -502,7 +502,7 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
* Resolve the effective git isolation mode from preferences.
* 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
* 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
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.
- **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 {
const entries = this.getHookStatus();
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:", ""];

View file

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

View file

@ -99,7 +99,7 @@ test("detectProjectState: detects preferences in .gsd/", (t) => {
t.after(() => cleanup(dir));
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);
assert.ok(result.v2);
assert.equal(result.v2!.hasPreferences, true);

View file

@ -64,11 +64,11 @@ _None_
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 {
const gsdDir = join(dir, ".gsd");
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. */
@ -302,7 +302,7 @@ describe('doctor-git', async () => {
// ─── Test 7: none-mode skips orphaned worktree check ───────────────
// NOTE: loadEffectiveGSDPreferences() resolves PROJECT_PREFERENCES_PATH
// 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") {
test('none-mode skips orphaned worktree', async () => {
const dir = createRepoWithCompletedMilestone();
@ -409,7 +409,7 @@ describe('doctor-git', async () => {
cleanups.push(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");
writeFileSync(metaPath, JSON.stringify({ integrationBranch: "feat/does-not-exist" }, null, 2));

View file

@ -297,7 +297,7 @@ describe('doctor-proactive', async () => {
cleanups.push(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");
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-")));
mkdirSync(join(repo, ".gsd"), { recursive: true });
writeFileSync(
join(repo, ".gsd", "preferences.md"),
join(repo, ".gsd", "PREFERENCES.md"),
[
"---",
"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-")));
mkdirSync(join(repo, ".gsd"), { recursive: true });
writeFileSync(
join(repo, ".gsd", "preferences.md"),
join(repo, ".gsd", "PREFERENCES.md"),
[
"---",
"models:",

View file

@ -1142,7 +1142,7 @@ describe('git-service', async () => {
mkdirSync(join(repo, ".gsd", "runtime"), { recursive: true });
mkdirSync(join(repo, ".gsd", "activity"), { recursive: true });
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", "runtime", "units.json"), "{}");
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");
try {
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);
assert.ok(detection.v2);

View file

@ -8,7 +8,7 @@
* Uses the writeRunnerPreferences pattern from doctor-git.test.ts:
* PROJECT_PREFERENCES_PATH is a module-level constant frozen at import
* 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";
@ -24,7 +24,7 @@ import assert from 'node:assert/strict';
// --- 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 {
mkdirSync(join(process.cwd(), ".gsd"), { recursive: true });
@ -72,12 +72,12 @@ try {
// Test 4: shouldUseWorktreeIsolation returns false for no prefs (default: none)
// 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
// cannot control ~/.gsd/preferences.md.
// cannot control ~/.gsd/PREFERENCES.md.
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"));
if (!globalPrefsExist) {
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)', () => {
const globalPrefsExist = existsSync(join(homedir(), ".gsd", "preferences.md"))
const globalPrefsExist = existsSync(join(homedir(), ".gsd", "PREFERENCES.md"))
|| existsSync(join(homedir(), ".gsd", "PREFERENCES.md"));
if (!globalPrefsExist) {
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,
// preferences.git.isolation is undefined, and getIsolationMode returns "none".
// Default changed from "worktree" to "none" so GSD works out of the box
// without preferences.md (#2480).
// without PREFERENCES.md (#2480).
const { preferences } = validatePreferences({});
assert.equal(preferences.git?.isolation, undefined, "no isolation in empty prefs");
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);
});
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");
assert.ok(prefs);
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");
assert.ok(prefs);
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. */
export function preferBraveSearch(): boolean {
// preferences.md takes priority over env var
// PREFERENCES.md takes priority over env var
const prefsPref = resolveSearchProviderFromPreferences();
if (prefsPref === "brave" || prefsPref === "tavily" || prefsPref === "ollama") return true;
if (prefsPref === "native") return false;

View file

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

View file

@ -38,7 +38,7 @@ export async function collectHooksData(projectCwdOverride?: string): Promise<Hoo
}
// 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.
// In a cold child process, cycleCounts is empty, so activeCycles will be {}.
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) ─────────────────────
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 } {

View file

@ -84,7 +84,7 @@ function maskToken(token: string): string {
// ─── Helpers ──────────────────────────────────────────────────────────────────
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 {

View file

@ -1200,7 +1200,7 @@ export function ExperimentalPanel() {
{data && (
<p className="text-[11px] text-muted-foreground">
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.
</p>
)}