Skip to content

Commit 9724d0c

Browse files
committed
feat(cli): add workflow guidance and phase context to commands
Add `bmalph guide` command showing the full BMAD → Ralph workflow. Enhance init, start, and resume commands with phase info (agent, goal, outputs) so users understand what each phase does before it runs.
1 parent 8b05ab5 commit 9724d0c

12 files changed

Lines changed: 294 additions & 12 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bmalph",
3-
"version": "0.2.1",
3+
"version": "0.3.0",
44
"description": "Unified AI Development Framework - BMAD phases with Ralph execution loop for Claude Code",
55
"type": "module",
66
"bin": {

src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { startCommand } from "./commands/start.js";
44
import { resumeCommand } from "./commands/resume.js";
55
import { statusCommand } from "./commands/status.js";
66
import { resetCommand } from "./commands/reset.js";
7+
import { guideCommand } from "./commands/guide.js";
78

89
const program = new Command();
910

@@ -42,4 +43,9 @@ program
4243
.option("--hard", "Also remove artifacts")
4344
.action(resetCommand);
4445

46+
program
47+
.command("guide")
48+
.description("Show the full workflow overview")
49+
.action(guideCommand);
50+
4551
program.parse();

src/commands/guide.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import chalk from "chalk";
2+
3+
export function guideCommand(): void {
4+
console.log(chalk.bold("\nBMALPH Workflow\n"));
5+
6+
console.log(chalk.blue(" BMAD — Structured Planning"));
7+
console.log(chalk.dim(" ─────────────────────────────────────────"));
8+
console.log(` Phase 1 Analysis Requirements, research, risks`);
9+
console.log(` Phase 2 Planning PRD, stories, MVP scope`);
10+
console.log(` Phase 3 Design Architecture, data model, API\n`);
11+
12+
console.log(chalk.blue(" Ralph — Autonomous Implementation"));
13+
console.log(chalk.dim(" ─────────────────────────────────────────"));
14+
console.log(` Phase 4 Implementation TDD build, code review, validation\n`);
15+
16+
console.log(chalk.dim(" Each phase runs iteratively with human checkpoints."));
17+
console.log(chalk.dim(" The loop detects completion and advances automatically.\n"));
18+
19+
console.log("Commands:");
20+
console.log(` ${chalk.cyan("bmalph start")} Begin from Phase 1`);
21+
console.log(` ${chalk.cyan("bmalph status")} Check current progress`);
22+
console.log(` ${chalk.cyan("bmalph resume")} Continue from last checkpoint`);
23+
console.log(` ${chalk.cyan("/bmalph")} Interactive mode in Claude Code`);
24+
}

src/commands/init.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,13 @@ export async function initCommand(options: InitOptions): Promise<void> {
8484
console.log(chalk.green("\nbmalph initialized successfully!"));
8585
console.log(`\n Project: ${chalk.bold(config.name)}`);
8686
console.log(` Level: ${chalk.bold(String(config.level))}`);
87+
console.log(`\nWorkflow:`);
88+
console.log(`\n Phase 1 — Analysis Gather requirements, research, constraints`);
89+
console.log(` Phase 2 — Planning PRD, user stories, MVP scope`);
90+
console.log(` Phase 3 — Design Architecture, data model, conventions`);
91+
console.log(` Phase 4 — Implementation TDD build via Ralph loop`);
8792
console.log(`\nNext steps:`);
88-
console.log(` ${chalk.cyan("bmalph start")} - Start the execution loop`);
89-
console.log(` ${chalk.cyan("/bmalph")} - Use interactively in Claude Code`);
93+
console.log(` ${chalk.cyan("bmalph start")} Start from Phase 1 (Analysis)`);
94+
console.log(` ${chalk.cyan("bmalph start -p 4")} Skip to implementation (if you already have a plan)`);
95+
console.log(` ${chalk.cyan("/bmalph")} Use interactively in Claude Code`);
9096
}

src/commands/resume.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { spawn } from "child_process";
22
import { join } from "path";
33
import chalk from "chalk";
4-
import { readPhaseState, getPhaseLabel } from "../utils/state.js";
4+
import { readPhaseState, getPhaseInfo } from "../utils/state.js";
55
import { isInitialized } from "../installer.js";
66

77
export async function resumeCommand(): Promise<void> {
@@ -23,11 +23,12 @@ export async function resumeCommand(): Promise<void> {
2323
return;
2424
}
2525

26-
console.log(
27-
chalk.blue(
28-
`Resuming from Phase ${state.currentPhase} (${getPhaseLabel(state.currentPhase)}), iteration ${state.iteration}...`
29-
)
30-
);
26+
const phaseInfo = getPhaseInfo(state.currentPhase);
27+
console.log(chalk.blue(`\nResuming Phase ${state.currentPhase}${phaseInfo.name} (iteration ${state.iteration})`));
28+
console.log(` Agent: ${chalk.bold(phaseInfo.agent)}`);
29+
console.log(` Goal: ${phaseInfo.goal}`);
30+
console.log(` Outputs: ${phaseInfo.outputs.join(", ")}`);
31+
console.log(chalk.dim("\nPress Ctrl+C to interrupt.\n"));
3132

3233
const scriptPath = join(projectDir, "bmalph/bmalph.sh");
3334

src/commands/start.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { spawn } from "child_process";
22
import { join } from "path";
33
import chalk from "chalk";
44
import { readConfig } from "../utils/config.js";
5-
import { writePhaseState, type PhaseState } from "../utils/state.js";
5+
import { writePhaseState, getPhaseInfo, type PhaseState } from "../utils/state.js";
66
import { isInitialized } from "../installer.js";
77

88
interface StartOptions {
@@ -40,8 +40,12 @@ export async function startCommand(options: StartOptions): Promise<void> {
4040

4141
await writePhaseState(projectDir, state);
4242

43-
console.log(chalk.blue(`Starting bmalph loop at phase ${startPhase}...`));
44-
console.log(chalk.dim("Press Ctrl+C to interrupt.\n"));
43+
const phaseInfo = getPhaseInfo(startPhase);
44+
console.log(chalk.blue(`\nStarting Phase ${startPhase}${phaseInfo.name}`));
45+
console.log(` Agent: ${chalk.bold(phaseInfo.agent)}`);
46+
console.log(` Goal: ${phaseInfo.goal}`);
47+
console.log(` Outputs: ${phaseInfo.outputs.join(", ")}`);
48+
console.log(chalk.dim("\nPress Ctrl+C to interrupt.\n"));
4549

4650
const scriptPath = join(projectDir, "bmalph/bmalph.sh");
4751

src/utils/state.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,40 @@ export function getPhaseLabel(phase: number): string {
6767
};
6868
return labels[phase] ?? "Unknown";
6969
}
70+
71+
export interface PhaseInfo {
72+
name: string;
73+
agent: string;
74+
goal: string;
75+
outputs: string[];
76+
}
77+
78+
export function getPhaseInfo(phase: number): PhaseInfo {
79+
const info: Record<number, PhaseInfo> = {
80+
1: {
81+
name: "Analysis",
82+
agent: "Mary (Analyst)",
83+
goal: "Gather requirements, constraints, and risks",
84+
outputs: ["requirements.md", "constraints.md", "research.md", "risks.md"],
85+
},
86+
2: {
87+
name: "Planning",
88+
agent: "Larry (PM)",
89+
goal: "Create PRD, user stories, and MVP scope",
90+
outputs: ["prd.md", "stories.md", "mvp-scope.md"],
91+
},
92+
3: {
93+
name: "Design",
94+
agent: "Mo (Architect)",
95+
goal: "Define architecture, data model, and conventions",
96+
outputs: ["architecture.md", "data-model.md", "conventions.md"],
97+
},
98+
4: {
99+
name: "Implementation",
100+
agent: "Ralph (Developer)",
101+
goal: "TDD build, code review, and validation",
102+
outputs: ["code", "tests", "documentation"],
103+
},
104+
};
105+
return info[phase] ?? { name: "Unknown", agent: "Unknown", goal: "Unknown", outputs: [] };
106+
}

tests/commands/guide.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2+
3+
vi.mock("chalk", () => ({
4+
default: {
5+
red: (s: string) => s,
6+
green: (s: string) => s,
7+
blue: (s: string) => s,
8+
yellow: (s: string) => s,
9+
bold: (s: string) => s,
10+
dim: (s: string) => s,
11+
cyan: (s: string) => s,
12+
white: (s: string) => s,
13+
gray: (s: string) => s,
14+
},
15+
}));
16+
17+
describe("guide command", () => {
18+
let consoleSpy: ReturnType<typeof vi.spyOn>;
19+
20+
beforeEach(() => {
21+
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
22+
});
23+
24+
afterEach(() => {
25+
consoleSpy.mockRestore();
26+
vi.restoreAllMocks();
27+
});
28+
29+
it("displays all 4 phases", async () => {
30+
const { guideCommand } = await import("../../src/commands/guide.js");
31+
guideCommand();
32+
33+
const output = consoleSpy.mock.calls.map((c) => c[0]).join("\n");
34+
expect(output).toContain("Phase 1");
35+
expect(output).toContain("Analysis");
36+
expect(output).toContain("Phase 2");
37+
expect(output).toContain("Planning");
38+
expect(output).toContain("Phase 3");
39+
expect(output).toContain("Design");
40+
expect(output).toContain("Phase 4");
41+
expect(output).toContain("Implementation");
42+
});
43+
44+
it("shows BMAD and Ralph sections", async () => {
45+
const { guideCommand } = await import("../../src/commands/guide.js");
46+
guideCommand();
47+
48+
const output = consoleSpy.mock.calls.map((c) => c[0]).join("\n");
49+
expect(output).toContain("BMAD");
50+
expect(output).toContain("Ralph");
51+
});
52+
53+
it("shows available commands", async () => {
54+
const { guideCommand } = await import("../../src/commands/guide.js");
55+
guideCommand();
56+
57+
const output = consoleSpy.mock.calls.map((c) => c[0]).join("\n");
58+
expect(output).toContain("bmalph start");
59+
expect(output).toContain("bmalph status");
60+
expect(output).toContain("bmalph resume");
61+
expect(output).toContain("/bmalph");
62+
});
63+
});

tests/commands/init.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,33 @@ describe("init command", () => {
8181
expect(mergeClaudeMd).toHaveBeenCalled();
8282
});
8383

84+
it("displays workflow roadmap with all phases", async () => {
85+
const { isInitialized, scaffoldProject, mergeClaudeMd } = await import(
86+
"../../src/installer.js"
87+
);
88+
const { writeConfig } = await import("../../src/utils/config.js");
89+
90+
vi.mocked(isInitialized).mockResolvedValue(false);
91+
vi.mocked(scaffoldProject).mockResolvedValue(undefined);
92+
vi.mocked(mergeClaudeMd).mockResolvedValue(undefined);
93+
vi.mocked(writeConfig).mockResolvedValue(undefined);
94+
95+
const { initCommand } = await import("../../src/commands/init.js");
96+
await initCommand({ name: "my-proj", description: "A project", level: "2" });
97+
98+
const output = consoleSpy.mock.calls.map((c) => c[0]).join("\n");
99+
expect(output).toContain("Phase 1");
100+
expect(output).toContain("Analysis");
101+
expect(output).toContain("Phase 2");
102+
expect(output).toContain("Planning");
103+
expect(output).toContain("Phase 3");
104+
expect(output).toContain("Design");
105+
expect(output).toContain("Phase 4");
106+
expect(output).toContain("Implementation");
107+
expect(output).toContain("bmalph start");
108+
expect(output).toContain("bmalph start -p 4");
109+
});
110+
84111
it("prompts user when options missing", async () => {
85112
const { isInitialized, scaffoldProject, mergeClaudeMd } = await import(
86113
"../../src/installer.js"

tests/commands/resume.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ vi.mock("../../src/utils/state.js", () => ({
3232
};
3333
return labels[phase] ?? "Unknown";
3434
}),
35+
getPhaseInfo: vi.fn((phase: number) => {
36+
const info: Record<number, { name: string; agent: string; goal: string; outputs: string[] }> = {
37+
1: { name: "Analysis", agent: "Mary (Analyst)", goal: "Gather requirements, constraints, and risks", outputs: ["requirements.md", "constraints.md", "research.md", "risks.md"] },
38+
2: { name: "Planning", agent: "Larry (PM)", goal: "Create PRD, user stories, and MVP scope", outputs: ["prd.md", "stories.md", "mvp-scope.md"] },
39+
3: { name: "Design", agent: "Mo (Architect)", goal: "Define architecture, data model, and conventions", outputs: ["architecture.md", "data-model.md", "conventions.md"] },
40+
4: { name: "Implementation", agent: "Ralph (Developer)", goal: "TDD build, code review, and validation", outputs: ["code", "tests", "documentation"] },
41+
};
42+
return info[phase] ?? { name: "Unknown", agent: "Unknown", goal: "Unknown", outputs: [] };
43+
}),
3544
}));
3645

3746
describe("resume command", () => {
@@ -126,4 +135,31 @@ describe("resume command", () => {
126135
expect.objectContaining({ stdio: "inherit" })
127136
);
128137
});
138+
139+
it("shows phase info before spawning", async () => {
140+
const { isInitialized } = await import("../../src/installer.js");
141+
const { readPhaseState } = await import("../../src/utils/state.js");
142+
const { spawn } = await import("child_process");
143+
144+
vi.mocked(isInitialized).mockResolvedValue(true);
145+
vi.mocked(readPhaseState).mockResolvedValue({
146+
currentPhase: 3,
147+
iteration: 1,
148+
status: "running",
149+
startedAt: "2025-01-01T00:00:00.000Z",
150+
lastUpdated: "2025-01-01T01:00:00.000Z",
151+
});
152+
153+
const fakeChild = new EventEmitter();
154+
vi.mocked(spawn).mockReturnValue(fakeChild as never);
155+
156+
const { resumeCommand } = await import("../../src/commands/resume.js");
157+
await resumeCommand();
158+
159+
const output = consoleSpy.mock.calls.map((c) => c[0]).join("\n");
160+
expect(output).toContain("Phase 3");
161+
expect(output).toContain("Design");
162+
expect(output).toContain("Mo (Architect)");
163+
expect(output).toContain("architecture");
164+
});
129165
});

0 commit comments

Comments
 (0)