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.
- package/README.md +56 -23
- package/dist/cli.js +13 -0
- package/dist/commands/doctor.js +22 -6
- package/dist/commands/implement.d.ts +6 -0
- package/dist/commands/implement.js +82 -0
- package/dist/commands/reset.d.ts +7 -0
- package/dist/commands/reset.js +81 -0
- package/dist/commands/status.js +86 -10
- package/dist/platform/claude-code.js +0 -1
- package/dist/reset.d.ts +18 -0
- package/dist/reset.js +181 -0
- package/dist/transition/artifact-scan.d.ts +27 -0
- package/dist/transition/artifact-scan.js +91 -0
- package/dist/transition/artifacts.d.ts +1 -0
- package/dist/transition/artifacts.js +1 -0
- package/dist/transition/context.js +34 -0
- package/dist/transition/fix-plan.d.ts +8 -2
- package/dist/transition/fix-plan.js +33 -7
- package/dist/transition/orchestration.d.ts +2 -2
- package/dist/transition/orchestration.js +120 -41
- package/dist/transition/preflight.d.ts +6 -0
- package/dist/transition/preflight.js +154 -0
- package/dist/transition/specs-index.d.ts +1 -1
- package/dist/transition/specs-index.js +24 -1
- package/dist/transition/types.d.ts +23 -1
- package/dist/utils/dryrun.d.ts +1 -1
- package/dist/utils/dryrun.js +22 -0
- package/dist/utils/validate.js +2 -2
- package/package.json +1 -1
- package/ralph/ralph_loop.sh +15 -0
- package/slash-commands/bmalph-doctor.md +16 -0
- package/slash-commands/bmalph-implement.md +18 -141
- package/slash-commands/bmalph-status.md +15 -0
- package/slash-commands/bmalph-upgrade.md +15 -0
|
@@ -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
|
|
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
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
//
|
|
223
|
-
const
|
|
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
|
-
...
|
|
226
|
-
...
|
|
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 {
|
|
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,
|
|
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,
|
|
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
|
}
|
package/dist/utils/dryrun.d.ts
CHANGED
package/dist/utils/dryrun.js
CHANGED
|
@@ -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
|
}
|
package/dist/utils/validate.js
CHANGED
|
@@ -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
package/ralph/ralph_loop.sh
CHANGED
|
@@ -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
|