From bda85565ef974d07c1c202c0ac4b2613241dd184 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 29 Sep 2026 18:25:31 +0000 Subject: [PATCH] fix(init): guide project.md migration (#1999) * feat(init): offer project.md migration * fix(config): preserve context newline state * test(init): prove migration preserves files * docs(setup): document project.md migration * fix(init): guide project.md migration * fix(init): cover migration destinations --- .changeset/calm-project-context.md | 5 +++++ docs-lab/start/setup.md | 16 ++++++++++++++++ src/core/legacy-cleanup.ts | 14 +++++++++----- test/core/init.test.ts | 16 ++++++++++++++++ test/core/legacy-cleanup.test.ts | 24 ++++++++++++++++-------- test/setup-docs-claims.test.ts | 24 ++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 .changeset/calm-project-context.md diff --git a/.changeset/calm-project-context.md b/.changeset/calm-project-context.md new file mode 100644 index 00000000..abee21e5 --- /dev/null +++ b/.changeset/calm-project-context.md @@ -0,0 +1,5 @@ +--- +'@fission-ai/openspec': patch +--- + +Guide users through an AI-assisted migration from legacy `project.md` to `config.yaml`. diff --git a/docs-lab/start/setup.md b/docs-lab/start/setup.md index 47ed2339..668e0379 100644 --- a/docs-lab/start/setup.md +++ b/docs-lab/start/setup.md @@ -34,6 +34,22 @@ Re-running init is safe: - Running init again with a new tool selected adds that tool. - The `--tools` flag skips the picker ([CLI reference](../reference/cli.md)). +### Migrate an existing `project.md` + +Init does not copy legacy `openspec/project.md` into `config.yaml`. It keeps the file and prints an AI-assisted migration request. + +In your AI chat: + +``` +Review openspec/project.md and migrate its useful content to openspec/config.yaml. +Keep context concise: include only project-wide facts needed during artifact creation, apply, and archive. +Move artifact-specific guidance into rules for the matching artifacts. +Move guidance for apply or archive into the matching operations entry. +Leave out generic, outdated, or verbose material. Do not delete project.md. +``` + +Review `config.yaml`, then delete `project.md` when ready. + ## What init installs Running init creates two things in your project: diff --git a/src/core/legacy-cleanup.ts b/src/core/legacy-cleanup.ts index e1d20047..151071a0 100644 --- a/src/core/legacy-cleanup.ts +++ b/src/core/legacy-cleanup.ts @@ -1185,11 +1185,15 @@ export function formatProjectMdMigrationHint(): string { lines.push(' • openspec/project.md'); lines.push(chalk.dim(' We won\'t delete this file. It may contain useful project context.')); lines.push(''); - lines.push(chalk.dim(' The new openspec/config.yaml has a "context:" section for planning')); - lines.push(chalk.dim(' context. This is included in every OpenSpec request and works more')); - lines.push(chalk.dim(' reliably than the old project.md approach.')); + lines.push(chalk.dim(' Ask your AI assistant:')); lines.push(''); - lines.push(chalk.dim(' Review project.md, move any useful content to config.yaml\'s context')); - lines.push(chalk.dim(' section, then delete the file when ready.')); + lines.push(chalk.dim(' Review openspec/project.md and migrate its useful content to')); + lines.push(chalk.dim(' openspec/config.yaml. Keep context concise: include only project-wide')); + lines.push(chalk.dim(' facts needed during artifact creation, apply, and archive. Move')); + lines.push(chalk.dim(' artifact-specific guidance into rules for the matching artifacts.')); + lines.push(chalk.dim(' Move guidance for apply or archive into the matching operations entry.')); + lines.push(chalk.dim(' Leave out generic, outdated, or verbose material. Do not delete project.md.')); + lines.push(''); + lines.push(chalk.dim(' Review config.yaml, then delete project.md when ready.')); return lines.join('\n'); } diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 9b7cf1b6..ad1e33d7 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -156,6 +156,22 @@ describe('InitCommand', () => { expect(content).toContain('schema: spec-driven'); }); + it('should guide project.md migration without copying or deleting it', async () => { + const openspecPath = path.join(testDir, 'openspec'); + const projectMdPath = path.join(openspecPath, 'project.md'); + await fs.mkdir(openspecPath, { recursive: true }); + await fs.writeFile(projectMdPath, '# Migrate me later\n'); + + await new InitCommand({ tools: 'none', force: true }).execute(testDir); + + expect(readProjectConfig(testDir)?.context).toBeUndefined(); + expect(await fs.readFile(projectMdPath, 'utf-8')).toBe('# Migrate me later\n'); + expect(vi.mocked(console.log).mock.calls.flat().join('\n')).toContain( + 'Ask your AI assistant' + ); + expect(confirmMock).not.toHaveBeenCalled(); + }); + it('should add the requested artifact language to a new config', async () => { const initCommand = new InitCommand({ tools: 'none', diff --git a/test/core/legacy-cleanup.test.ts b/test/core/legacy-cleanup.test.ts index eae69978..232d0e3a 100644 --- a/test/core/legacy-cleanup.test.ts +++ b/test/core/legacy-cleanup.test.ts @@ -1046,7 +1046,8 @@ ${OPENSPEC_MARKERS.end}`); expect(summary).toContain('• openspec/project.md'); expect(summary).toContain('won\'t delete this file'); expect(summary).toContain('config.yaml'); - expect(summary).toContain('"context:"'); + expect(summary).toContain('Ask your AI assistant'); + expect(summary).toContain('rules for the matching artifacts'); }); it('should include attention section with other legacy artifacts', () => { @@ -1148,19 +1149,26 @@ ${OPENSPEC_MARKERS.end}`); expect(hint).toContain('openspec/project.md'); expect(hint).toContain('won\'t delete this file'); expect(hint).toContain('config.yaml'); - expect(hint).toContain('"context:"'); + expect(hint).toContain('Ask your AI assistant'); }); - it('should include actionable instructions', () => { + it('should include a pasteable AI-assisted migration request', () => { const hint = formatProjectMdMigrationHint(); - expect(hint).toContain('move any useful content'); - expect(hint).toContain('delete the file when ready'); + expect(hint).toContain('Review openspec/project.md'); + expect(hint).toContain('migrate its useful content to'); + expect(hint).toContain('Do not delete project.md'); + expect(hint).toContain('Review config.yaml, then delete project.md when ready'); }); - it('should explain the new context section benefits', () => { + it('should guide the agent to distill and route the content', () => { const hint = formatProjectMdMigrationHint(); - expect(hint).toContain('included in every OpenSpec request'); - expect(hint).toContain('reliably'); + expect(hint).toContain('Keep context concise'); + expect(hint).toContain('only project-wide'); + expect(hint).toContain('artifact creation, apply, and archive'); + expect(hint).toContain('rules for the matching artifacts'); + expect(hint).toContain('matching operations entry'); + expect(hint).toContain('Leave out generic'); + expect(hint).toContain('outdated, or verbose material'); }); }); diff --git a/test/setup-docs-claims.test.ts b/test/setup-docs-claims.test.ts index 50a3198d..f47ff753 100644 --- a/test/setup-docs-claims.test.ts +++ b/test/setup-docs-claims.test.ts @@ -4,6 +4,7 @@ import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { claudeAdapter } from '../src/core/command-generation/adapters/claude.js'; import { AI_TOOLS } from '../src/core/config.js'; +import { formatProjectMdMigrationHint } from '../src/core/legacy-cleanup.js'; import { CORE_WORKFLOWS } from '../src/core/profiles.js'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); @@ -20,6 +21,29 @@ const CORE_SECTION = PROFILES.split('## The core set')[1].split( )[0]; describe('setup documentation', () => { + it('matches the AI-assisted project.md migration guidance', () => { + const hint = formatProjectMdMigrationHint(); + const claims = [ + 'Review openspec/project.md and migrate its useful content to', + 'Keep context concise', + 'only project-wide', + 'artifact creation, apply, and archive', + 'rules for the matching artifacts', + 'matching operations entry', + 'Leave out generic', + 'outdated, or verbose material', + 'Do not delete project.md', + ]; + + expect(SETUP).toContain('Init does not copy legacy `openspec/project.md`'); + for (const claim of claims) { + expect(hint).toContain(claim); + expect(SETUP).toContain(claim); + } + expect(hint).toContain('Review config.yaml, then delete project.md when ready.'); + expect(SETUP).toContain('Review `config.yaml`, then delete `project.md` when ready.'); + }); + it('keeps the Claude Code paths and recovery commands aligned with OpenSpec', () => { const claude = AI_TOOLS.find((tool) => tool.value === 'claude'); const claudeCommandPath = claudeAdapter.getFilePath('').split(path.sep).join('/');