bmalph 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,17 +2,18 @@ import { readFile, readdir, cp, mkdir, access, rm, rename } from "fs/promises";
2
2
  import { join } from "path";
3
3
  import { debug, info, warn } from "../utils/logger.js";
4
4
  import { isEnoent, formatError } from "../utils/errors.js";
5
- import { atomicWriteFile } from "../utils/file-system.js";
5
+ import { atomicWriteFile, exists } from "../utils/file-system.js";
6
6
  import { readConfig } from "../utils/config.js";
7
7
  import { readState, writeState } from "../utils/state.js";
8
8
  import { parseStoriesWithWarnings } from "./story-parsing.js";
9
- import { generateFixPlan, parseFixPlan, mergeFixPlanProgress, detectOrphanedCompletedStories, detectRenumberedStories, } from "./fix-plan.js";
9
+ import { generateFixPlan, parseFixPlan, mergeFixPlanProgress, detectOrphanedCompletedStories, detectRenumberedStories, buildCompletedTitleMap, normalizeTitle, } from "./fix-plan.js";
10
10
  import { detectTechStack, customizeAgentMd } from "./tech-stack.js";
11
- import { findArtifactsDir, validateArtifacts } from "./artifacts.js";
11
+ import { findArtifactsDir } from "./artifacts.js";
12
+ import { runPreflight } from "./preflight.js";
12
13
  import { extractProjectContext, generateProjectContextMd, generatePrompt, detectTruncation, } from "./context.js";
13
14
  import { generateSpecsChangelog, formatChangelog } from "./specs-changelog.js";
14
15
  import { generateSpecsIndex, formatSpecsIndexMd } from "./specs-index.js";
15
- export async function runTransition(projectDir) {
16
+ export async function runTransition(projectDir, options) {
16
17
  info("Locating BMAD artifacts...");
17
18
  const artifactsDir = await findArtifactsDir(projectDir);
18
19
  if (!artifactsDir) {
@@ -20,6 +21,19 @@ export async function runTransition(projectDir) {
20
21
  }
21
22
  // Find and parse stories file
22
23
  const files = await readdir(artifactsDir);
24
+ // Read artifact contents early for preflight validation and later use
25
+ const artifactContents = new Map();
26
+ for (const file of files) {
27
+ if (file.endsWith(".md")) {
28
+ try {
29
+ const content = await readFile(join(artifactsDir, file), "utf-8");
30
+ artifactContents.set(file, content);
31
+ }
32
+ catch (err) {
33
+ warn(`Could not read artifact ${file}: ${formatError(err)}`);
34
+ }
35
+ }
36
+ }
23
37
  const storiesPattern = /^(epics[-_]?(and[-_]?)?)?stor(y|ies)([-_]\d+)?\.md$/i;
24
38
  const storiesFile = files.find((f) => storiesPattern.test(f) || /epic/i.test(f));
25
39
  if (!storiesFile) {
@@ -33,10 +47,40 @@ export async function runTransition(projectDir) {
33
47
  if (stories.length === 0) {
34
48
  throw new Error("No stories parsed from the epics file. Ensure stories follow the format: ### Story N.M: Title");
35
49
  }
50
+ // Pre-flight validation
51
+ info("Pre-flight validation...");
52
+ const preflightResult = runPreflight(artifactContents, files, stories, parseWarnings);
53
+ for (const issue of preflightResult.issues) {
54
+ if (issue.severity === "error") {
55
+ warn(` ERROR ${issue.id}: ${issue.message}`);
56
+ if (issue.suggestion)
57
+ warn(` ${issue.suggestion}`);
58
+ }
59
+ else if (issue.severity === "warning") {
60
+ warn(` WARN ${issue.id}: ${issue.message}`);
61
+ if (issue.suggestion)
62
+ warn(` ${issue.suggestion}`);
63
+ }
64
+ else {
65
+ info(` INFO ${issue.id}: ${issue.message}`);
66
+ }
67
+ }
68
+ if (!preflightResult.pass) {
69
+ if (options?.force) {
70
+ warn("Pre-flight validation has errors but --force was used, continuing...");
71
+ }
72
+ else {
73
+ const errors = preflightResult.issues.filter((i) => i.severity === "error");
74
+ throw new Error(`Pre-flight validation failed: ${errors.map((e) => e.message).join("; ")}. Use --force to override.`);
75
+ }
76
+ }
77
+ // Track generated files for summary output
78
+ const generatedFiles = [];
36
79
  // Check existing fix_plan for completed items (smart merge)
37
80
  let completedIds = new Set();
38
81
  let existingItems = [];
39
82
  const fixPlanPath = join(projectDir, ".ralph/@fix_plan.md");
83
+ const fixPlanExisted = await exists(fixPlanPath);
40
84
  try {
41
85
  const existingFixPlan = await readFile(fixPlanPath, "utf-8");
42
86
  existingItems = parseFixPlan(existingFixPlan);
@@ -57,16 +101,30 @@ export async function runTransition(projectDir) {
57
101
  for (const w of orphanWarnings) {
58
102
  warn(w);
59
103
  }
60
- // Detect renumbered stories (Bug #3)
61
- const renumberWarnings = detectRenumberedStories(existingItems, stories);
62
- for (const w of renumberWarnings) {
63
- warn(w);
64
- }
104
+ // Build title maps for title-based merge (Gap 3: renumbered story preservation)
105
+ const completedTitles = buildCompletedTitleMap(existingItems);
106
+ const newTitleMap = new Map(stories.map((s) => [s.id, s.title]));
65
107
  // Generate new fix_plan from current stories, preserving completion status
66
108
  info(`Generating fix plan for ${stories.length} stories...`);
67
109
  const newFixPlan = generateFixPlan(stories, storiesFile);
68
- const mergedFixPlan = mergeFixPlanProgress(newFixPlan, completedIds);
110
+ const mergedFixPlan = mergeFixPlanProgress(newFixPlan, completedIds, newTitleMap, completedTitles);
111
+ // Detect which stories were preserved via title match (for renumber warning suppression)
112
+ const preservedIds = new Set();
113
+ for (const [id, title] of newTitleMap) {
114
+ if (!completedIds.has(id) && completedTitles.has(normalizeTitle(title))) {
115
+ preservedIds.add(id);
116
+ }
117
+ }
118
+ // Detect renumbered stories (Bug #3), skipping auto-preserved ones
119
+ const renumberWarnings = detectRenumberedStories(existingItems, stories, preservedIds);
120
+ for (const w of renumberWarnings) {
121
+ warn(w);
122
+ }
69
123
  await atomicWriteFile(fixPlanPath, mergedFixPlan);
124
+ generatedFiles.push({
125
+ path: ".ralph/@fix_plan.md",
126
+ action: fixPlanExisted ? "updated" : "created",
127
+ });
70
128
  // Track whether progress was preserved for return value
71
129
  const fixPlanPreserved = completedIds.size > 0;
72
130
  // Generate changelog before overwriting specs/
@@ -78,6 +136,7 @@ export async function runTransition(projectDir) {
78
136
  if (changes.length > 0) {
79
137
  const changelog = formatChangelog(changes, new Date().toISOString());
80
138
  await atomicWriteFile(join(projectDir, ".ralph/SPECS_CHANGELOG.md"), changelog);
139
+ generatedFiles.push({ path: ".ralph/SPECS_CHANGELOG.md", action: "updated" });
81
140
  debug(`Generated SPECS_CHANGELOG.md with ${changes.length} changes`);
82
141
  }
83
142
  }
@@ -114,6 +173,7 @@ export async function runTransition(projectDir) {
114
173
  await access(specsTmpDir);
115
174
  await rm(specsDir, { recursive: true, force: true });
116
175
  await rename(specsTmpDir, specsDir);
176
+ generatedFiles.push({ path: ".ralph/specs/", action: "updated" });
117
177
  debug("Copied _bmad-output/ to .ralph/specs/ (atomic)");
118
178
  }
119
179
  else {
@@ -129,13 +189,20 @@ export async function runTransition(projectDir) {
129
189
  await access(specsTmpDir);
130
190
  await rm(specsDir, { recursive: true, force: true });
131
191
  await rename(specsTmpDir, specsDir);
192
+ generatedFiles.push({ path: ".ralph/specs/", action: "updated" });
132
193
  }
133
194
  // Generate SPECS_INDEX.md for intelligent spec reading
134
195
  info("Generating SPECS_INDEX.md...");
196
+ const specsIndexPath = join(projectDir, ".ralph/SPECS_INDEX.md");
197
+ const specsIndexExisted = await exists(specsIndexPath);
135
198
  try {
136
199
  const specsIndex = await generateSpecsIndex(specsDir);
137
200
  if (specsIndex.totalFiles > 0) {
138
- await atomicWriteFile(join(projectDir, ".ralph/SPECS_INDEX.md"), formatSpecsIndexMd(specsIndex));
201
+ await atomicWriteFile(specsIndexPath, formatSpecsIndexMd(specsIndex));
202
+ generatedFiles.push({
203
+ path: ".ralph/SPECS_INDEX.md",
204
+ action: specsIndexExisted ? "updated" : "created",
205
+ });
139
206
  debug(`Generated SPECS_INDEX.md with ${specsIndex.totalFiles} files`);
140
207
  }
141
208
  }
@@ -143,18 +210,6 @@ export async function runTransition(projectDir) {
143
210
  warn(`Could not generate SPECS_INDEX.md: ${formatError(err)}`);
144
211
  }
145
212
  // Generate PROJECT_CONTEXT.md from planning artifacts
146
- const artifactContents = new Map();
147
- for (const file of files) {
148
- if (file.endsWith(".md")) {
149
- try {
150
- const content = await readFile(join(artifactsDir, file), "utf-8");
151
- artifactContents.set(file, content);
152
- }
153
- catch (err) {
154
- warn(`Could not read artifact ${file}: ${formatError(err)}`);
155
- }
156
- }
157
- }
158
213
  let projectName = "project";
159
214
  try {
160
215
  const config = await readConfig(projectDir);
@@ -167,6 +222,8 @@ export async function runTransition(projectDir) {
167
222
  }
168
223
  // Extract project context for both PROJECT_CONTEXT.md and PROMPT.md
169
224
  info("Generating PROJECT_CONTEXT.md...");
225
+ const projectContextPath = join(projectDir, ".ralph/PROJECT_CONTEXT.md");
226
+ const projectContextExisted = await exists(projectContextPath);
170
227
  let projectContext = null;
171
228
  let truncationWarnings = [];
172
229
  if (artifactContents.size > 0) {
@@ -174,15 +231,21 @@ export async function runTransition(projectDir) {
174
231
  projectContext = context;
175
232
  truncationWarnings = detectTruncation(truncated);
176
233
  const contextMd = generateProjectContextMd(projectContext, projectName);
177
- await atomicWriteFile(join(projectDir, ".ralph/PROJECT_CONTEXT.md"), contextMd);
234
+ await atomicWriteFile(projectContextPath, contextMd);
235
+ generatedFiles.push({
236
+ path: ".ralph/PROJECT_CONTEXT.md",
237
+ action: projectContextExisted ? "updated" : "created",
238
+ });
178
239
  debug("Generated PROJECT_CONTEXT.md");
179
240
  }
180
241
  // Generate PROMPT.md with embedded context
181
242
  info("Generating PROMPT.md...");
182
243
  // Try to preserve rich PROMPT.md template if it has the placeholder
183
244
  let prompt;
245
+ let promptExisted = false;
184
246
  try {
185
247
  const existingPrompt = await readFile(join(projectDir, ".ralph/PROMPT.md"), "utf-8");
248
+ promptExisted = true;
186
249
  if (existingPrompt.includes("[YOUR PROJECT NAME]")) {
187
250
  prompt = existingPrompt.replace(/\[YOUR PROJECT NAME\]/g, projectName);
188
251
  }
@@ -201,29 +264,39 @@ export async function runTransition(projectDir) {
201
264
  prompt = generatePrompt(projectName, projectContext ?? undefined);
202
265
  }
203
266
  await atomicWriteFile(join(projectDir, ".ralph/PROMPT.md"), prompt);
267
+ generatedFiles.push({ path: ".ralph/PROMPT.md", action: promptExisted ? "updated" : "created" });
204
268
  // Customize @AGENT.md based on detected tech stack from architecture
205
269
  const architectureFile = files.find((f) => /architect/i.test(f));
206
270
  if (architectureFile) {
207
- try {
208
- const archContent = await readFile(join(artifactsDir, architectureFile), "utf-8");
209
- const stack = detectTechStack(archContent);
210
- if (stack) {
211
- const agentPath = join(projectDir, ".ralph/@AGENT.md");
212
- const agentTemplate = await readFile(agentPath, "utf-8");
213
- const customized = customizeAgentMd(agentTemplate, stack);
214
- await atomicWriteFile(agentPath, customized);
215
- debug("Customized @AGENT.md with detected tech stack");
271
+ const archContent = artifactContents.get(architectureFile);
272
+ if (archContent) {
273
+ try {
274
+ const stack = detectTechStack(archContent);
275
+ if (stack) {
276
+ const agentPath = join(projectDir, ".ralph/@AGENT.md");
277
+ const agentTemplate = await readFile(agentPath, "utf-8");
278
+ const customized = customizeAgentMd(agentTemplate, stack);
279
+ await atomicWriteFile(agentPath, customized);
280
+ generatedFiles.push({ path: ".ralph/@AGENT.md", action: "updated" });
281
+ debug("Customized @AGENT.md with detected tech stack");
282
+ }
283
+ }
284
+ catch (err) {
285
+ warn(`Could not customize @AGENT.md: ${formatError(err)}`);
216
286
  }
217
- }
218
- catch (err) {
219
- warn(`Could not customize @AGENT.md: ${formatError(err)}`);
220
287
  }
221
288
  }
222
- // Validate artifacts and collect warnings
223
- const artifactWarnings = await validateArtifacts(files, artifactsDir);
289
+ // Collect warnings from all sources
290
+ const preflightWarnings = preflightResult.issues
291
+ .filter((i) => i.severity === "warning" || (i.severity === "error" && options?.force))
292
+ .map((i) => i.message);
293
+ // Keep parse warnings not already covered by preflight (e.g., malformed IDs)
294
+ const nonPreflightParseWarnings = parseWarnings.filter((w) => !/has no acceptance criteria/i.test(w) &&
295
+ !/has no description/i.test(w) &&
296
+ !/not under an epic/i.test(w));
224
297
  const warnings = [
225
- ...parseWarnings,
226
- ...artifactWarnings,
298
+ ...preflightWarnings,
299
+ ...nonPreflightParseWarnings,
227
300
  ...orphanWarnings,
228
301
  ...renumberWarnings,
229
302
  ...truncationWarnings,
@@ -239,5 +312,11 @@ export async function runTransition(projectDir) {
239
312
  };
240
313
  await writeState(projectDir, newState);
241
314
  info("Transition complete: phase 4 (implementing)");
242
- return { storiesCount: stories.length, warnings, fixPlanPreserved };
315
+ return {
316
+ storiesCount: stories.length,
317
+ warnings,
318
+ fixPlanPreserved,
319
+ preflightIssues: preflightResult.issues,
320
+ generatedFiles,
321
+ };
243
322
  }
@@ -0,0 +1,6 @@
1
+ import type { Story, PreflightIssue, PreflightResult } from "./types.js";
2
+ export declare function validatePrd(content: string | null): PreflightIssue[];
3
+ export declare function validateArchitecture(content: string | null): PreflightIssue[];
4
+ export declare function validateStories(stories: Story[], parseWarnings: string[]): PreflightIssue[];
5
+ export declare function validateReadiness(content: string | null): PreflightIssue[];
6
+ export declare function runPreflight(artifactContents: Map<string, string>, files: string[], stories: Story[], parseWarnings: string[]): PreflightResult;
@@ -0,0 +1,154 @@
1
+ import { extractSection } from "./context.js";
2
+ function hasSection(content, patterns) {
3
+ return patterns.some((p) => extractSection(content, p) !== "");
4
+ }
5
+ export function validatePrd(content) {
6
+ if (content === null) {
7
+ return [
8
+ {
9
+ id: "W1",
10
+ severity: "warning",
11
+ message: "No PRD document found in planning artifacts",
12
+ suggestion: "Create a PRD using the /create-prd BMAD workflow.",
13
+ },
14
+ ];
15
+ }
16
+ const issues = [];
17
+ if (!hasSection(content, [
18
+ /^##\s+Executive Summary/m,
19
+ /^##\s+Vision/m,
20
+ /^##\s+Goals/m,
21
+ /^##\s+Project Goals/m,
22
+ ])) {
23
+ issues.push({
24
+ id: "W3",
25
+ severity: "warning",
26
+ message: "PRD missing Executive Summary or Vision section",
27
+ suggestion: "Ralph will lack project context — PROJECT_CONTEXT.md will have empty goals.",
28
+ });
29
+ }
30
+ if (!hasSection(content, [/^##\s+Functional Requirements/m])) {
31
+ issues.push({
32
+ id: "W4",
33
+ severity: "warning",
34
+ message: "PRD missing Functional Requirements section",
35
+ suggestion: "Ralph may miss key requirements during implementation.",
36
+ });
37
+ }
38
+ if (!hasSection(content, [/^##\s+Non-Functional/m, /^##\s+NFR/m, /^##\s+Quality/m])) {
39
+ issues.push({
40
+ id: "W5",
41
+ severity: "warning",
42
+ message: "PRD missing Non-Functional Requirements section",
43
+ suggestion: "Ralph will not enforce performance, security, or quality constraints.",
44
+ });
45
+ }
46
+ if (!hasSection(content, [/^##\s+Scope/m, /^##\s+In Scope/m, /^##\s+Out of Scope/m])) {
47
+ issues.push({
48
+ id: "W6",
49
+ severity: "warning",
50
+ message: "PRD missing Scope section",
51
+ suggestion: "Ralph may implement beyond intended boundaries.",
52
+ });
53
+ }
54
+ return issues;
55
+ }
56
+ export function validateArchitecture(content) {
57
+ if (content === null) {
58
+ return [
59
+ {
60
+ id: "W2",
61
+ severity: "warning",
62
+ message: "No architecture document found in planning artifacts",
63
+ suggestion: "Create an architecture doc using the /create-architecture BMAD workflow.",
64
+ },
65
+ ];
66
+ }
67
+ const issues = [];
68
+ if (!hasSection(content, [/^##\s+Tech Stack/m, /^##\s+Technology Stack/m])) {
69
+ issues.push({
70
+ id: "W7",
71
+ severity: "warning",
72
+ message: "Architecture missing Tech Stack section",
73
+ suggestion: "Ralph cannot customize @AGENT.md without knowing the tech stack.",
74
+ });
75
+ }
76
+ return issues;
77
+ }
78
+ export function validateStories(stories, parseWarnings) {
79
+ const issues = [];
80
+ for (const warning of parseWarnings) {
81
+ if (/has no acceptance criteria/i.test(warning)) {
82
+ issues.push({
83
+ id: "W8",
84
+ severity: "warning",
85
+ message: warning,
86
+ suggestion: "Ralph cannot verify completion without acceptance criteria.",
87
+ });
88
+ }
89
+ else if (/has no description/i.test(warning)) {
90
+ issues.push({
91
+ id: "W9",
92
+ severity: "warning",
93
+ message: warning,
94
+ suggestion: "Ralph will lack context for implementing this story.",
95
+ });
96
+ }
97
+ else if (/not under an epic/i.test(warning)) {
98
+ issues.push({
99
+ id: "W10",
100
+ severity: "warning",
101
+ message: warning,
102
+ suggestion: "Story grouping helps Ralph understand feature boundaries.",
103
+ });
104
+ }
105
+ }
106
+ if (stories.length < 3) {
107
+ issues.push({
108
+ id: "I2",
109
+ severity: "info",
110
+ message: `Only ${stories.length} ${stories.length === 1 ? "story" : "stories"} found (fewer than 3 is suspiciously small scope)`,
111
+ });
112
+ }
113
+ return issues;
114
+ }
115
+ export function validateReadiness(content) {
116
+ if (content === null) {
117
+ return [
118
+ {
119
+ id: "I1",
120
+ severity: "info",
121
+ message: "No readiness report found (optional artifact)",
122
+ },
123
+ ];
124
+ }
125
+ if (/NO[-\s]?GO/i.test(content)) {
126
+ return [
127
+ {
128
+ id: "E1",
129
+ severity: "error",
130
+ message: "Readiness report indicates NO-GO status",
131
+ suggestion: "Address issues in the readiness report, or use --force to override.",
132
+ },
133
+ ];
134
+ }
135
+ return [];
136
+ }
137
+ export function runPreflight(artifactContents, files, stories, parseWarnings) {
138
+ const prdFile = files.find((f) => /prd/i.test(f));
139
+ const prdContent = prdFile ? (artifactContents.get(prdFile) ?? null) : null;
140
+ const archFile = files.find((f) => /architect/i.test(f));
141
+ const archContent = archFile ? (artifactContents.get(archFile) ?? null) : null;
142
+ const readinessFile = files.find((f) => /readiness/i.test(f));
143
+ const readinessContent = readinessFile ? (artifactContents.get(readinessFile) ?? null) : null;
144
+ const issues = [
145
+ ...validatePrd(prdContent),
146
+ ...validateArchitecture(archContent),
147
+ ...validateStories(stories, parseWarnings),
148
+ ...validateReadiness(readinessContent),
149
+ ];
150
+ return {
151
+ issues,
152
+ pass: !issues.some((i) => i.severity === "error"),
153
+ };
154
+ }
@@ -2,7 +2,7 @@ import type { SpecFileType, Priority, SpecsIndex } from "./types.js";
2
2
  /**
3
3
  * Detects the type of a spec file based on its filename.
4
4
  */
5
- export declare function detectSpecFileType(filename: string, _content: string): SpecFileType;
5
+ export declare function detectSpecFileType(filename: string, content: string): SpecFileType;
6
6
  /**
7
7
  * Determines the reading priority for a spec file based on its type.
8
8
  */
@@ -3,7 +3,7 @@ import { LARGE_FILE_THRESHOLD_BYTES, DEFAULT_SNIPPET_MAX_LENGTH } from "../utils
3
3
  /**
4
4
  * Detects the type of a spec file based on its filename.
5
5
  */
6
- export function detectSpecFileType(filename, _content) {
6
+ export function detectSpecFileType(filename, content) {
7
7
  const lower = filename.toLowerCase();
8
8
  if (lower.includes("prd"))
9
9
  return "prd";
@@ -22,6 +22,28 @@ export function detectSpecFileType(filename, _content) {
22
22
  return "readiness";
23
23
  if (lower.includes("sprint"))
24
24
  return "sprint";
25
+ return detectFromContent(content);
26
+ }
27
+ /**
28
+ * Content-based fallback when filename doesn't match any known pattern.
29
+ * Checks first 2000 characters for heading patterns.
30
+ */
31
+ function detectFromContent(content) {
32
+ const snippet = content.slice(0, 2000);
33
+ if (/^##\s+Functional Requirements/m.test(snippet) || /^##\s+Executive Summary/m.test(snippet))
34
+ return "prd";
35
+ if (/^##\s+Tech Stack/m.test(snippet) || /^##\s+Architecture Decision/m.test(snippet))
36
+ return "architecture";
37
+ if (/^###\s+Story\s+\d+\.\d+:/m.test(snippet))
38
+ return "stories";
39
+ if (/^##\s+Design Principles/m.test(snippet) || /^##\s+User Flows/m.test(snippet))
40
+ return "ux";
41
+ if (/^##\s+Test Strategy/m.test(snippet) || /^##\s+Test Cases/m.test(snippet))
42
+ return "test-design";
43
+ if (/^##\s+GO\s*\/\s*NO-GO/m.test(snippet) || /^##\s+Readiness/m.test(snippet))
44
+ return "readiness";
45
+ if (/^##\s+Key Findings/m.test(snippet) || /^##\s+Market Analysis/m.test(snippet))
46
+ return "research";
25
47
  return "other";
26
48
  }
27
49
  /**
@@ -35,6 +57,7 @@ export function determinePriority(type, _size) {
35
57
  return "critical";
36
58
  case "test-design":
37
59
  case "readiness":
60
+ case "research":
38
61
  return "high";
39
62
  case "ux":
40
63
  case "sprint":
@@ -6,6 +6,8 @@ export interface ProjectContext {
6
6
  scopeBoundaries: string;
7
7
  targetUsers: string;
8
8
  nonFunctionalRequirements: string;
9
+ designGuidelines: string;
10
+ researchInsights: string;
9
11
  }
10
12
  export interface Story {
11
13
  epic: string;
@@ -38,7 +40,7 @@ export interface SpecsChange {
38
40
  status: "added" | "modified" | "removed";
39
41
  summary?: string;
40
42
  }
41
- export type SpecFileType = "prd" | "architecture" | "stories" | "ux" | "test-design" | "readiness" | "sprint" | "brainstorm" | "other";
43
+ export type SpecFileType = "prd" | "architecture" | "stories" | "ux" | "test-design" | "readiness" | "sprint" | "brainstorm" | "research" | "other";
42
44
  export type Priority = "critical" | "high" | "medium" | "low";
43
45
  export interface SpecFileMetadata {
44
46
  path: string;
@@ -53,8 +55,28 @@ export interface SpecsIndex {
53
55
  totalSizeKb: number;
54
56
  files: SpecFileMetadata[];
55
57
  }
58
+ export type PreflightSeverity = "error" | "warning" | "info";
59
+ export interface PreflightIssue {
60
+ id: string;
61
+ severity: PreflightSeverity;
62
+ message: string;
63
+ suggestion?: string;
64
+ }
65
+ export interface PreflightResult {
66
+ issues: PreflightIssue[];
67
+ pass: boolean;
68
+ }
69
+ export interface TransitionOptions {
70
+ force?: boolean;
71
+ }
72
+ export interface GeneratedFile {
73
+ path: string;
74
+ action: "created" | "updated";
75
+ }
56
76
  export interface TransitionResult {
57
77
  storiesCount: number;
58
78
  warnings: string[];
59
79
  fixPlanPreserved: boolean;
80
+ preflightIssues?: PreflightIssue[];
81
+ generatedFiles: GeneratedFile[];
60
82
  }
@@ -1,5 +1,5 @@
1
1
  export interface DryRunAction {
2
- type: "create" | "modify" | "skip";
2
+ type: "create" | "modify" | "skip" | "delete" | "warn";
3
3
  path: string;
4
4
  reason?: string;
5
5
  }
@@ -11,6 +11,12 @@ export function logDryRunAction(action) {
11
11
  case "skip":
12
12
  console.log(`${prefix} Would skip: ${chalk.dim(action.path)}${action.reason ? ` (${action.reason})` : ""}`);
13
13
  break;
14
+ case "delete":
15
+ console.log(`${prefix} Would delete: ${chalk.red(action.path)}`);
16
+ break;
17
+ case "warn":
18
+ console.log(`${prefix} Warning: ${chalk.yellow(action.path)}${action.reason ? ` (${action.reason})` : ""}`);
19
+ break;
14
20
  }
15
21
  }
16
22
  export function formatDryRunSummary(actions) {
@@ -19,9 +25,18 @@ export function formatDryRunSummary(actions) {
19
25
  }
20
26
  const lines = [];
21
27
  lines.push(chalk.blue("\n[dry-run] Would perform the following actions:\n"));
28
+ const deletes = actions.filter((a) => a.type === "delete");
22
29
  const creates = actions.filter((a) => a.type === "create");
23
30
  const modifies = actions.filter((a) => a.type === "modify");
24
31
  const skips = actions.filter((a) => a.type === "skip");
32
+ const warns = actions.filter((a) => a.type === "warn");
33
+ if (deletes.length > 0) {
34
+ lines.push(chalk.red("Would delete:"));
35
+ for (const action of deletes) {
36
+ lines.push(` ${action.path}`);
37
+ }
38
+ lines.push("");
39
+ }
25
40
  if (creates.length > 0) {
26
41
  lines.push(chalk.green("Would create:"));
27
42
  for (const action of creates) {
@@ -43,6 +58,13 @@ export function formatDryRunSummary(actions) {
43
58
  }
44
59
  lines.push("");
45
60
  }
61
+ if (warns.length > 0) {
62
+ lines.push(chalk.yellow("Warnings:"));
63
+ for (const action of warns) {
64
+ lines.push(` ${action.path}${action.reason ? ` (${action.reason})` : ""}`);
65
+ }
66
+ lines.push("");
67
+ }
46
68
  lines.push(chalk.dim("No changes made."));
47
69
  return lines.join("\n");
48
70
  }
@@ -185,8 +185,8 @@ export function normalizeRalphStatus(data) {
185
185
  return {
186
186
  loopCount,
187
187
  status,
188
- tasksCompleted: 0,
189
- tasksTotal: 0,
188
+ tasksCompleted: typeof data.tasks_completed === "number" ? data.tasks_completed : 0,
189
+ tasksTotal: typeof data.tasks_total === "number" ? data.tasks_total : 0,
190
190
  };
191
191
  }
192
192
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmalph",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1436,6 +1436,21 @@ main() {
1436
1436
  exit 1
1437
1437
  fi
1438
1438
 
1439
+ # Check required dependencies
1440
+ if ! command -v jq &> /dev/null; then
1441
+ log_status "ERROR" "Required dependency 'jq' is not installed."
1442
+ echo ""
1443
+ echo "jq is required for JSON processing in the Ralph loop."
1444
+ echo ""
1445
+ echo "Install jq:"
1446
+ echo " macOS: brew install jq"
1447
+ echo " Ubuntu: sudo apt-get install jq"
1448
+ echo " Windows: choco install jq (or: winget install jqlang.jq)"
1449
+ echo ""
1450
+ echo "After installing, run this command again."
1451
+ exit 1
1452
+ fi
1453
+
1439
1454
  # Initialize session tracking before entering the loop
1440
1455
  init_session_tracking
1441
1456
 
@@ -0,0 +1,16 @@
1
+ # Check Project Health
2
+
3
+ Run diagnostic checks on the bmalph installation and report any issues.
4
+
5
+ ## How to Run
6
+
7
+ Execute the CLI command:
8
+ bmalph doctor
9
+
10
+ ## What It Does
11
+
12
+ - Verifies required directories exist (`_bmad/`, `.ralph/`, `bmalph/`)
13
+ - Checks that slash commands are installed correctly
14
+ - Validates the instructions file contains the BMAD snippet
15
+ - Reports version mismatches between installed and bundled assets
16
+ - Suggests remediation steps for any issues found