From d3bea4523cb5baca8f24e9744cf6062020d0e41e Mon Sep 17 00:00:00 2001 From: Selim Ataballyev Date: Fri, 19 Jun 2026 20:38:37 +0500 Subject: [PATCH 1/2] Gitignore bmalph/state/ (mutable runtime state) --- src/utils/constants.ts | 7 ++++++- tests/commands/doctor-health-checks.test.ts | 13 ++++++++++++- tests/commands/doctor.test.ts | 5 ++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index fedda99..1afec34 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -104,7 +104,12 @@ export const RALPH_STATUS_MAP = { // ============================================================================= /** Entries bmalph adds to .gitignore during init and checks during doctor */ -export const GITIGNORE_ENTRIES = [".ralph/logs/", "_bmad-output/", ".swarm/"] as const; +export const GITIGNORE_ENTRIES = [ + ".ralph/logs/", + "_bmad-output/", + ".swarm/", + "bmalph/state/", +] as const; // ============================================================================= // Swarm constants diff --git a/tests/commands/doctor-health-checks.test.ts b/tests/commands/doctor-health-checks.test.ts index 46e041d..ef5fc34 100644 --- a/tests/commands/doctor-health-checks.test.ts +++ b/tests/commands/doctor-health-checks.test.ts @@ -69,7 +69,9 @@ beforeEach(() => { describe("checkGitignore", () => { it("passes when .gitignore contains all required entries", async () => { - mockReadFile.mockResolvedValue("node_modules/\n.ralph/logs/\n_bmad-output/\n.swarm/\ndist/\n"); + mockReadFile.mockResolvedValue( + "node_modules/\n.ralph/logs/\n_bmad-output/\n.swarm/\nbmalph/state/\ndist/\n" + ); const result = await checkGitignore("/projects/webapp"); @@ -84,6 +86,15 @@ describe("checkGitignore", () => { expect(result.passed).toBe(false); }); + it("fails when bmalph/state/ (mutable runtime state) is missing from .gitignore", async () => { + mockReadFile.mockResolvedValue("node_modules/\n.ralph/logs/\n_bmad-output/\n.swarm/\n"); + + const result = await checkGitignore("/projects/webapp"); + + expect(result.passed).toBe(false); + expect(result.detail).toContain("bmalph/state/"); + }); + it("reports which entries are missing", async () => { mockReadFile.mockResolvedValue("node_modules/\ndist/\n"); diff --git a/tests/commands/doctor.test.ts b/tests/commands/doctor.test.ts index 0b0c7e4..02d4f18 100644 --- a/tests/commands/doctor.test.ts +++ b/tests/commands/doctor.test.ts @@ -114,7 +114,10 @@ describe("doctor command", { timeout: 15000 }, () => { join(testDir, "CLAUDE.md"), "# Project\n\n## BMAD-METHOD Integration\n\nSome content" ); - await writeFile(join(testDir, ".gitignore"), ".ralph/logs/\n_bmad-output/\n.swarm/\n"); + await writeFile( + join(testDir, ".gitignore"), + ".ralph/logs/\n_bmad-output/\n.swarm/\nbmalph/state/\n" + ); } describe("Node version check", () => { From dd809dbb943d25836ee80e947dab91a0cfcf806c Mon Sep 17 00:00:00 2001 From: Selim Ataballyev Date: Fri, 19 Jun 2026 23:01:45 +0500 Subject: [PATCH 2/2] Run updateGitignore on upgrade so existing installs migrate new entries --- src/commands/upgrade.ts | 4 ++++ src/installer.ts | 1 + tests/commands/upgrade.test.ts | 14 ++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/src/commands/upgrade.ts b/src/commands/upgrade.ts index 5eabb81..759c29b 100644 --- a/src/commands/upgrade.ts +++ b/src/commands/upgrade.ts @@ -4,6 +4,7 @@ import { isInitialized, copyBundledAssets, mergeInstructionsFile, + updateGitignore, previewUpgrade, getBundledVersions, } from "../installer.js"; @@ -63,6 +64,9 @@ async function runUpgrade(options: UpgradeOptions): Promise { const result = await copyBundledAssets(projectDir, platform); await mergeInstructionsFile(projectDir, platform); + // Migrate .gitignore so installs created before a new managed entry was added + // (e.g. bmalph/state/) pick it up on upgrade instead of failing doctor. + await updateGitignore(projectDir); // Update upstreamVersions in config to match bundled versions const config = await readConfig(projectDir); diff --git a/src/installer.ts b/src/installer.ts index 14dd5b7..76c1aef 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -20,6 +20,7 @@ export { generateManifests } from "./installer/bmad-assets.js"; export { mergeInstructionsFile, + updateGitignore, isInitialized, previewInstall, previewUpgrade, diff --git a/tests/commands/upgrade.test.ts b/tests/commands/upgrade.test.ts index f4be453..d5e96a5 100644 --- a/tests/commands/upgrade.test.ts +++ b/tests/commands/upgrade.test.ts @@ -10,6 +10,7 @@ vi.mock("../../src/installer.js", () => ({ isInitialized: vi.fn(), copyBundledAssets: vi.fn(), mergeInstructionsFile: vi.fn(), + updateGitignore: vi.fn(), previewUpgrade: vi.fn(), getBundledVersions: vi.fn(), })); @@ -136,6 +137,19 @@ describe("upgrade command", () => { expect(mergeInstructionsFile).toHaveBeenCalled(); }); + it("migrates .gitignore so older installs pick up newly managed entries", async () => { + const { isInitialized, copyBundledAssets, mergeInstructionsFile, updateGitignore } = + await import("../../src/installer.js"); + vi.mocked(isInitialized).mockResolvedValue(true); + vi.mocked(copyBundledAssets).mockResolvedValue({ updatedPaths: ["_bmad/"] }); + vi.mocked(mergeInstructionsFile).mockResolvedValue(undefined); + + const { upgradeCommand } = await import("../../src/commands/upgrade.js"); + await upgradeCommand({ force: true, projectDir: process.cwd() }); + + expect(updateGitignore).toHaveBeenCalledWith(expect.any(String)); + }); + it("displays upgrading message", async () => { const { isInitialized, copyBundledAssets, mergeInstructionsFile } = await import("../../src/installer.js");