bmalph 2.7.4 → 2.7.6
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 +87 -34
- package/dist/commands/doctor-checks.js +5 -4
- package/dist/commands/doctor-checks.js.map +1 -1
- package/dist/commands/doctor-runtime-checks.js +104 -86
- package/dist/commands/doctor-runtime-checks.js.map +1 -1
- package/dist/commands/run.js +4 -0
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.js +12 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/installer/bmad-assets.js +182 -0
- package/dist/installer/bmad-assets.js.map +1 -0
- package/dist/installer/commands.js +324 -0
- package/dist/installer/commands.js.map +1 -0
- package/dist/installer/install.js +42 -0
- package/dist/installer/install.js.map +1 -0
- package/dist/installer/metadata.js +56 -0
- package/dist/installer/metadata.js.map +1 -0
- package/dist/installer/project-files.js +169 -0
- package/dist/installer/project-files.js.map +1 -0
- package/dist/installer/ralph-assets.js +91 -0
- package/dist/installer/ralph-assets.js.map +1 -0
- package/dist/installer/template-files.js +168 -0
- package/dist/installer/template-files.js.map +1 -0
- package/dist/installer/types.js +2 -0
- package/dist/installer/types.js.map +1 -0
- package/dist/installer.js +5 -790
- package/dist/installer.js.map +1 -1
- package/dist/platform/cursor-runtime-checks.js +81 -0
- package/dist/platform/cursor-runtime-checks.js.map +1 -0
- package/dist/platform/cursor.js +4 -3
- package/dist/platform/cursor.js.map +1 -1
- package/dist/platform/detect.js +28 -5
- package/dist/platform/detect.js.map +1 -1
- package/dist/platform/instructions-snippet.js +18 -0
- package/dist/platform/instructions-snippet.js.map +1 -1
- package/dist/platform/resolve.js +23 -5
- package/dist/platform/resolve.js.map +1 -1
- package/dist/run/ralph-process.js +84 -15
- package/dist/run/ralph-process.js.map +1 -1
- package/dist/transition/artifact-loading.js +91 -0
- package/dist/transition/artifact-loading.js.map +1 -0
- package/dist/transition/artifact-scan.js +15 -3
- package/dist/transition/artifact-scan.js.map +1 -1
- package/dist/transition/context-output.js +85 -0
- package/dist/transition/context-output.js.map +1 -0
- package/dist/transition/fix-plan-sync.js +119 -0
- package/dist/transition/fix-plan-sync.js.map +1 -0
- package/dist/transition/orchestration.js +25 -362
- package/dist/transition/orchestration.js.map +1 -1
- package/dist/transition/specs-sync.js +78 -2
- package/dist/transition/specs-sync.js.map +1 -1
- package/dist/utils/ralph-runtime-state.js +222 -0
- package/dist/utils/ralph-runtime-state.js.map +1 -0
- package/dist/utils/state.js +17 -16
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/validate.js +16 -0
- package/dist/utils/validate.js.map +1 -1
- package/dist/watch/renderer.js +48 -6
- package/dist/watch/renderer.js.map +1 -1
- package/dist/watch/state-reader.js +79 -44
- package/dist/watch/state-reader.js.map +1 -1
- package/package.json +1 -1
- package/ralph/RALPH-REFERENCE.md +60 -16
- package/ralph/drivers/claude-code.sh +25 -0
- package/ralph/drivers/codex.sh +11 -0
- package/ralph/drivers/copilot.sh +11 -0
- package/ralph/drivers/cursor.sh +58 -29
- package/ralph/lib/circuit_breaker.sh +3 -3
- package/ralph/lib/date_utils.sh +28 -9
- package/ralph/lib/response_analyzer.sh +220 -17
- package/ralph/ralph_loop.sh +464 -121
- package/ralph/templates/PROMPT.md +5 -0
- package/ralph/templates/ralphrc.template +14 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-scan.js","sourceRoot":"","sources":["../../src/transition/artifact-scan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"artifact-scan.js","sourceRoot":"","sources":["../../src/transition/artifact-scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { debug, info, warn } from "../utils/logger.js";
|
|
4
|
+
import { isEnoent, formatError } from "../utils/errors.js";
|
|
5
|
+
import { atomicWriteFile, exists } from "../utils/file-system.js";
|
|
6
|
+
import { readConfig } from "../utils/config.js";
|
|
7
|
+
import { combineArtifactContents } from "./artifact-collection.js";
|
|
8
|
+
import { extractProjectContext, generateProjectContextMd, generatePrompt, detectTruncation, } from "./context.js";
|
|
9
|
+
import { detectTechStack, customizeAgentMd } from "./tech-stack.js";
|
|
10
|
+
export async function generateContextOutputs(projectDir, inputs) {
|
|
11
|
+
const generatedFiles = [];
|
|
12
|
+
let projectName = "project";
|
|
13
|
+
try {
|
|
14
|
+
const config = await readConfig(projectDir);
|
|
15
|
+
if (config?.name) {
|
|
16
|
+
projectName = config.name;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
debug(`Could not read config for project name: ${formatError(err)}`);
|
|
21
|
+
}
|
|
22
|
+
info("Generating PROJECT_CONTEXT.md...");
|
|
23
|
+
const projectContextPath = join(projectDir, ".ralph/PROJECT_CONTEXT.md");
|
|
24
|
+
const projectContextExisted = await exists(projectContextPath);
|
|
25
|
+
let projectContext = null;
|
|
26
|
+
let truncationWarnings = [];
|
|
27
|
+
if (inputs.artifactContents.size > 0) {
|
|
28
|
+
const { context, truncated } = extractProjectContext(inputs.artifactContents);
|
|
29
|
+
projectContext = context;
|
|
30
|
+
truncationWarnings = detectTruncation(truncated);
|
|
31
|
+
const contextMd = generateProjectContextMd(projectContext, projectName);
|
|
32
|
+
await atomicWriteFile(projectContextPath, contextMd);
|
|
33
|
+
generatedFiles.push({
|
|
34
|
+
path: ".ralph/PROJECT_CONTEXT.md",
|
|
35
|
+
action: projectContextExisted ? "updated" : "created",
|
|
36
|
+
});
|
|
37
|
+
debug("Generated PROJECT_CONTEXT.md");
|
|
38
|
+
}
|
|
39
|
+
info("Generating PROMPT.md...");
|
|
40
|
+
let prompt;
|
|
41
|
+
let promptExisted = false;
|
|
42
|
+
try {
|
|
43
|
+
const existingPrompt = await readFile(join(projectDir, ".ralph/PROMPT.md"), "utf-8");
|
|
44
|
+
promptExisted = true;
|
|
45
|
+
if (existingPrompt.includes("[YOUR PROJECT NAME]")) {
|
|
46
|
+
prompt = existingPrompt.replace(/\[YOUR PROJECT NAME\]/g, projectName);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
prompt = generatePrompt(projectName, projectContext ?? undefined);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (isEnoent(err)) {
|
|
54
|
+
debug("No existing PROMPT.md found, generating from template");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
warn(`Could not read existing PROMPT.md: ${formatError(err)}`);
|
|
58
|
+
}
|
|
59
|
+
prompt = generatePrompt(projectName, projectContext ?? undefined);
|
|
60
|
+
}
|
|
61
|
+
await atomicWriteFile(join(projectDir, ".ralph/PROMPT.md"), prompt);
|
|
62
|
+
generatedFiles.push({ path: ".ralph/PROMPT.md", action: promptExisted ? "updated" : "created" });
|
|
63
|
+
const combinedArchitectureContent = combineArtifactContents(inputs.collectedArtifacts.architectureFiles, inputs.artifactContents);
|
|
64
|
+
if (combinedArchitectureContent) {
|
|
65
|
+
try {
|
|
66
|
+
const stack = detectTechStack(combinedArchitectureContent);
|
|
67
|
+
if (stack) {
|
|
68
|
+
const agentPath = join(projectDir, ".ralph/@AGENT.md");
|
|
69
|
+
const agentTemplate = await readFile(agentPath, "utf-8");
|
|
70
|
+
const customized = customizeAgentMd(agentTemplate, stack);
|
|
71
|
+
await atomicWriteFile(agentPath, customized);
|
|
72
|
+
generatedFiles.push({ path: ".ralph/@AGENT.md", action: "updated" });
|
|
73
|
+
debug("Customized @AGENT.md with detected tech stack");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
warn(`Could not customize @AGENT.md: ${formatError(err)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
warnings: truncationWarnings,
|
|
82
|
+
generatedFiles,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=context-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-output.js","sourceRoot":"","sources":["../../src/transition/context-output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,cAAc,EACd,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AASpE,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAAkB,EAClB,MAA8B;IAE9B,MAAM,cAAc,GAAoB,EAAE,CAAC;IAC3C,IAAI,WAAW,GAAG,SAAS,CAAC;IAE5B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,CAAC;QAC5C,IAAI,MAAM,EAAE,IAAI,EAAE,CAAC;YACjB,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,2CAA2C,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,CAAC,kCAAkC,CAAC,CAAC;IACzC,MAAM,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,2BAA2B,CAAC,CAAC;IACzE,MAAM,qBAAqB,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IAC/D,IAAI,cAAc,GAA0B,IAAI,CAAC;IACjD,IAAI,kBAAkB,GAAa,EAAE,CAAC;IAEtC,IAAI,MAAM,CAAC,gBAAgB,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,qBAAqB,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAC9E,cAAc,GAAG,OAAO,CAAC;QACzB,kBAAkB,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACjD,MAAM,SAAS,GAAG,wBAAwB,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QACxE,MAAM,eAAe,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC;QACrD,cAAc,CAAC,IAAI,CAAC;YAClB,IAAI,EAAE,2BAA2B;YACjC,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;SACtD,CAAC,CAAC;QACH,KAAK,CAAC,8BAA8B,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAChC,IAAI,MAAc,CAAC;IACnB,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,EAAE,OAAO,CAAC,CAAC;QACrF,aAAa,GAAG,IAAI,CAAC;QACrB,IAAI,cAAc,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YACnD,MAAM,GAAG,cAAc,CAAC,OAAO,CAAC,wBAAwB,EAAE,WAAW,CAAC,CAAC;QACzE,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,cAAc,CAAC,WAAW,EAAE,cAAc,IAAI,SAAS,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAClB,KAAK,CAAC,uDAAuD,CAAC,CAAC;QACjE,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,sCAAsC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,MAAM,GAAG,cAAc,CAAC,WAAW,EAAE,cAAc,IAAI,SAAS,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,eAAe,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC,CAAC;IACpE,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IAEjG,MAAM,2BAA2B,GAAG,uBAAuB,CACzD,MAAM,CAAC,kBAAkB,CAAC,iBAAiB,EAC3C,MAAM,CAAC,gBAAgB,CACxB,CAAC;IACF,IAAI,2BAA2B,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,eAAe,CAAC,2BAA2B,CAAC,CAAC;YAC3D,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;gBACvD,MAAM,aAAa,GAAG,MAAM,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;gBACzD,MAAM,UAAU,GAAG,gBAAgB,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;gBAC1D,MAAM,eAAe,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;gBAC7C,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;gBACrE,KAAK,CAAC,+CAA+C,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,kCAAkC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,kBAAkB;QAC5B,cAAc;KACf,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { debug, info, warn } from "../utils/logger.js";
|
|
4
|
+
import { isEnoent, formatError } from "../utils/errors.js";
|
|
5
|
+
import { atomicWriteFile, exists } from "../utils/file-system.js";
|
|
6
|
+
import { generateFixPlan, parseFixPlan, mergeFixPlanProgress, detectOrphanedCompletedStories, detectRenumberedStories, buildCompletedTitleMap, normalizeTitle, } from "./fix-plan.js";
|
|
7
|
+
import { parseSprintStatus } from "./sprint-status.js";
|
|
8
|
+
export async function syncFixPlan(projectDir, inputs) {
|
|
9
|
+
let completedIds = new Set();
|
|
10
|
+
let existingItems = [];
|
|
11
|
+
let orphanWarnings = [];
|
|
12
|
+
let renumberWarnings = [];
|
|
13
|
+
const completionWarnings = [];
|
|
14
|
+
let useTitleBasedMerge = true;
|
|
15
|
+
let fixPlanPreserved = false;
|
|
16
|
+
const fixPlanPath = join(projectDir, ".ralph/@fix_plan.md");
|
|
17
|
+
const fixPlanExisted = await exists(fixPlanPath);
|
|
18
|
+
try {
|
|
19
|
+
const existingFixPlan = await readFile(fixPlanPath, "utf-8");
|
|
20
|
+
existingItems = parseFixPlan(existingFixPlan);
|
|
21
|
+
debug(`Found ${existingItems.filter((item) => item.completed).length} completed stories in existing fix_plan`);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (isEnoent(err)) {
|
|
25
|
+
debug("No existing fix_plan found, starting fresh");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
warn(`Could not read existing fix_plan: ${formatError(err)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const sprintStatusSource = await resolveSprintStatusSource(projectDir, inputs);
|
|
32
|
+
if (sprintStatusSource) {
|
|
33
|
+
useTitleBasedMerge = false;
|
|
34
|
+
if (sprintStatusSource.readError) {
|
|
35
|
+
completionWarnings.push(`Sprint status file "${sprintStatusSource.displayPath}" could not be read: ${sprintStatusSource.readError}. All stories were left unchecked.`);
|
|
36
|
+
}
|
|
37
|
+
else if (!sprintStatusSource.content) {
|
|
38
|
+
completionWarnings.push(`Sprint status file "${sprintStatusSource.displayPath}" could not be read. All stories were left unchecked.`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const sprintStatus = parseSprintStatus(sprintStatusSource.content);
|
|
42
|
+
completionWarnings.push(...sprintStatus.warnings);
|
|
43
|
+
if (sprintStatus.valid) {
|
|
44
|
+
completedIds = new Set(inputs.stories
|
|
45
|
+
.filter((story) => sprintStatus.storyStatusById.get(story.id) === "done")
|
|
46
|
+
.map((story) => story.id));
|
|
47
|
+
fixPlanPreserved = completedIds.size > 0;
|
|
48
|
+
for (const story of inputs.stories) {
|
|
49
|
+
if (!sprintStatus.storyStatusById.has(story.id)) {
|
|
50
|
+
completionWarnings.push(`Sprint status is missing story ${story.id} (${story.title}); leaving it unchecked.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
completedIds = new Set(existingItems.filter((item) => item.completed).map((item) => item.id));
|
|
58
|
+
fixPlanPreserved = completedIds.size > 0;
|
|
59
|
+
const newStoryIds = new Set(inputs.stories.map((story) => story.id));
|
|
60
|
+
orphanWarnings = detectOrphanedCompletedStories(existingItems, newStoryIds);
|
|
61
|
+
const completedTitles = buildCompletedTitleMap(existingItems);
|
|
62
|
+
const newTitleMap = new Map(inputs.stories.map((story) => [story.id, story.title]));
|
|
63
|
+
const preservedIds = new Set();
|
|
64
|
+
for (const [id, title] of newTitleMap) {
|
|
65
|
+
if (!completedIds.has(id) && completedTitles.has(normalizeTitle(title))) {
|
|
66
|
+
preservedIds.add(id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
renumberWarnings = detectRenumberedStories(existingItems, inputs.stories, preservedIds);
|
|
70
|
+
}
|
|
71
|
+
const completedTitles = buildCompletedTitleMap(existingItems);
|
|
72
|
+
const newTitleMap = new Map(inputs.stories.map((story) => [story.id, story.title]));
|
|
73
|
+
info(`Generating fix plan for ${inputs.stories.length} stories...`);
|
|
74
|
+
const newFixPlan = generateFixPlan(inputs.stories, undefined, inputs.planningSpecsSubpath);
|
|
75
|
+
const mergedFixPlan = mergeFixPlanProgress(newFixPlan, completedIds, useTitleBasedMerge ? newTitleMap : undefined, useTitleBasedMerge ? completedTitles : undefined);
|
|
76
|
+
await atomicWriteFile(fixPlanPath, mergedFixPlan);
|
|
77
|
+
return {
|
|
78
|
+
warnings: [...completionWarnings, ...orphanWarnings, ...renumberWarnings],
|
|
79
|
+
fixPlanPreserved,
|
|
80
|
+
generatedFile: {
|
|
81
|
+
path: ".ralph/@fix_plan.md",
|
|
82
|
+
action: fixPlanExisted ? "updated" : "created",
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function resolveSprintStatusSource(projectDir, inputs) {
|
|
87
|
+
const canonicalCandidates = [
|
|
88
|
+
"_bmad-output/implementation-artifacts/sprint-status.yaml",
|
|
89
|
+
"_bmad-output/implementation_artifacts/sprint-status.yaml",
|
|
90
|
+
];
|
|
91
|
+
for (const candidate of canonicalCandidates) {
|
|
92
|
+
const candidatePath = join(projectDir, candidate);
|
|
93
|
+
if (!(await exists(candidatePath))) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return {
|
|
98
|
+
displayPath: candidate,
|
|
99
|
+
content: await readFile(candidatePath, "utf-8"),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
displayPath: candidate,
|
|
105
|
+
content: null,
|
|
106
|
+
readError: formatError(err),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!inputs.collectedArtifacts.sprintStatusFile) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const artifactsRelativeDir = relative(projectDir, inputs.artifactsDir).replace(/\\/g, "/");
|
|
114
|
+
return {
|
|
115
|
+
displayPath: `${artifactsRelativeDir}/${inputs.collectedArtifacts.sprintStatusFile}`,
|
|
116
|
+
content: inputs.artifactContents.get(inputs.collectedArtifacts.sprintStatusFile) ?? null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=fix-plan-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fix-plan-sync.js","sourceRoot":"","sources":["../../src/transition/fix-plan-sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,372 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { debug, info, warn } from "../utils/logger.js";
|
|
4
|
-
import { isEnoent, formatError } from "../utils/errors.js";
|
|
5
|
-
import { atomicWriteFile, exists, getFilesRecursive } from "../utils/file-system.js";
|
|
6
|
-
import { readConfig } from "../utils/config.js";
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { info } from "../utils/logger.js";
|
|
7
3
|
import { readState, writeState } from "../utils/state.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { runPreflight, PreflightValidationError } from "./preflight.js";
|
|
13
|
-
import { collectTransitionArtifacts, combineArtifactContents } from "./artifact-collection.js";
|
|
14
|
-
import { compareStoryIds } from "./story-id.js";
|
|
15
|
-
import { extractProjectContext, generateProjectContextMd, generatePrompt, detectTruncation, } from "./context.js";
|
|
16
|
-
import { generateSpecsChangelog, formatChangelog } from "./specs-changelog.js";
|
|
17
|
-
import { generateSpecsIndex, formatSpecsIndexMd } from "./specs-index.js";
|
|
18
|
-
import { parseSprintStatus } from "./sprint-status.js";
|
|
19
|
-
import { prepareSpecsDirectory } from "./specs-sync.js";
|
|
20
|
-
async function swapSpecsDirectory(specsDir, specsTmpDir) {
|
|
21
|
-
const specsOldDir = `${specsDir}.old`;
|
|
22
|
-
let hasBackup = false;
|
|
23
|
-
if (await exists(specsDir)) {
|
|
24
|
-
await rm(specsOldDir, { recursive: true, force: true });
|
|
25
|
-
await rename(specsDir, specsOldDir);
|
|
26
|
-
hasBackup = true;
|
|
27
|
-
}
|
|
28
|
-
else if (await exists(specsOldDir)) {
|
|
29
|
-
hasBackup = true;
|
|
30
|
-
debug("Found existing .ralph/specs.old from previous failed transition, preserving backup");
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
debug("No existing .ralph/specs to preserve (first transition)");
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
await rename(specsTmpDir, specsDir);
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
if (hasBackup) {
|
|
40
|
-
debug(`Specs swap failed, restoring original: ${formatError(err)}`);
|
|
41
|
-
try {
|
|
42
|
-
await rename(specsOldDir, specsDir);
|
|
43
|
-
}
|
|
44
|
-
catch (restoreErr) {
|
|
45
|
-
if (!isEnoent(restoreErr)) {
|
|
46
|
-
debug(`Could not restore .ralph/specs.old: ${formatError(restoreErr)}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
throw err;
|
|
51
|
-
}
|
|
52
|
-
if (hasBackup) {
|
|
53
|
-
await rm(specsOldDir, { recursive: true, force: true });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function ensureUniqueStoryIds(stories) {
|
|
57
|
-
const sourceById = new Map();
|
|
58
|
-
for (const story of stories) {
|
|
59
|
-
const existingSource = sourceById.get(story.id);
|
|
60
|
-
if (existingSource) {
|
|
61
|
-
throw new Error(`Duplicate story ID "${story.id}" found in ${existingSource} and ${story.sourceFile}`);
|
|
62
|
-
}
|
|
63
|
-
sourceById.set(story.id, story.sourceFile);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
async function resolveSprintStatusSource(projectDir, artifactsDir, collectedArtifacts, artifactContents) {
|
|
67
|
-
const canonicalCandidates = [
|
|
68
|
-
"_bmad-output/implementation-artifacts/sprint-status.yaml",
|
|
69
|
-
"_bmad-output/implementation_artifacts/sprint-status.yaml",
|
|
70
|
-
];
|
|
71
|
-
for (const candidate of canonicalCandidates) {
|
|
72
|
-
const candidatePath = join(projectDir, candidate);
|
|
73
|
-
if (!(await exists(candidatePath))) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
return {
|
|
78
|
-
displayPath: candidate,
|
|
79
|
-
content: await readFile(candidatePath, "utf-8"),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
return {
|
|
84
|
-
displayPath: candidate,
|
|
85
|
-
content: null,
|
|
86
|
-
readError: formatError(err),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (!collectedArtifacts.sprintStatusFile) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const artifactsRelativeDir = relative(projectDir, artifactsDir).replace(/\\/g, "/");
|
|
94
|
-
return {
|
|
95
|
-
displayPath: `${artifactsRelativeDir}/${collectedArtifacts.sprintStatusFile}`,
|
|
96
|
-
content: artifactContents.get(collectedArtifacts.sprintStatusFile) ?? null,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
4
|
+
import { loadTransitionInputs } from "./artifact-loading.js";
|
|
5
|
+
import { generateContextOutputs } from "./context-output.js";
|
|
6
|
+
import { syncFixPlan } from "./fix-plan-sync.js";
|
|
7
|
+
import { prepareSpecsDirectory, syncPreparedSpecsDirectory } from "./specs-sync.js";
|
|
99
8
|
export async function runTransition(projectDir, options) {
|
|
100
|
-
|
|
101
|
-
const artifactsDir = await findArtifactsDir(projectDir);
|
|
102
|
-
if (!artifactsDir) {
|
|
103
|
-
throw new Error("No BMAD artifacts found. Run BMAD planning phases first (at minimum: Create PRD, Create Architecture, Create Epics and Stories).");
|
|
104
|
-
}
|
|
105
|
-
const files = await getFilesRecursive(artifactsDir);
|
|
106
|
-
const collectedArtifacts = collectTransitionArtifacts(files);
|
|
107
|
-
// Read artifact contents early for preflight validation and later use
|
|
108
|
-
const artifactContents = new Map();
|
|
109
|
-
for (const file of collectedArtifacts.files) {
|
|
110
|
-
if (!/\.(?:md|ya?ml)$/i.test(file)) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const content = await readFile(join(artifactsDir, file), "utf-8");
|
|
115
|
-
artifactContents.set(file, content);
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
warn(`Could not read artifact ${file}: ${formatError(err)}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (collectedArtifacts.storyFiles.length === 0) {
|
|
122
|
-
debug(`Files in artifacts dir: ${collectedArtifacts.files.join(", ")}`);
|
|
123
|
-
throw new Error(`No epics/stories file found in ${artifactsDir}. Available files: ${collectedArtifacts.files.join(", ")}. Run 'CE' (Create Epics and Stories) first.`);
|
|
124
|
-
}
|
|
125
|
-
debug(`Using stories files: ${collectedArtifacts.storyFiles.join(", ")}`);
|
|
126
|
-
info("Parsing stories...");
|
|
127
|
-
const stories = [];
|
|
128
|
-
const parseWarnings = [];
|
|
129
|
-
for (const storyFile of collectedArtifacts.storyFiles) {
|
|
130
|
-
const storiesContent = artifactContents.get(storyFile);
|
|
131
|
-
if (!storiesContent) {
|
|
132
|
-
warn(`Could not read stories artifact ${storyFile}`);
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
const parsedStories = parseStoriesWithWarnings(storiesContent, storyFile);
|
|
136
|
-
stories.push(...parsedStories.stories);
|
|
137
|
-
parseWarnings.push(...parsedStories.warnings);
|
|
138
|
-
}
|
|
139
|
-
ensureUniqueStoryIds(stories);
|
|
140
|
-
stories.sort((left, right) => compareStoryIds(left.id, right.id) ||
|
|
141
|
-
left.sourceFile.localeCompare(right.sourceFile) ||
|
|
142
|
-
left.title.localeCompare(right.title));
|
|
143
|
-
if (stories.length === 0) {
|
|
144
|
-
throw new Error("No stories parsed from the epics files. Ensure stories follow the format: ### Story N.M: Title");
|
|
145
|
-
}
|
|
146
|
-
// Pre-flight validation
|
|
147
|
-
info("Pre-flight validation...");
|
|
148
|
-
const preflightResult = runPreflight(artifactContents, collectedArtifacts.files, stories, parseWarnings);
|
|
149
|
-
const preflightIssues = options?.force
|
|
150
|
-
? preflightResult.issues.map((issue) => issue.severity === "error" ? { ...issue, severity: "warning" } : issue)
|
|
151
|
-
: preflightResult.issues;
|
|
152
|
-
if (!preflightResult.pass) {
|
|
153
|
-
if (options?.force) {
|
|
154
|
-
warn("Pre-flight validation has errors but --force was used, continuing...");
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
throw new PreflightValidationError(preflightResult.issues);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// Track generated files for summary output
|
|
9
|
+
const inputs = await loadTransitionInputs(projectDir, options);
|
|
161
10
|
const generatedFiles = [];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let existingItems = [];
|
|
165
|
-
let orphanWarnings = [];
|
|
166
|
-
let renumberWarnings = [];
|
|
167
|
-
const completionWarnings = [];
|
|
168
|
-
let useTitleBasedMerge = true;
|
|
169
|
-
let fixPlanPreserved = false;
|
|
170
|
-
const fixPlanPath = join(projectDir, ".ralph/@fix_plan.md");
|
|
171
|
-
const fixPlanExisted = await exists(fixPlanPath);
|
|
172
|
-
try {
|
|
173
|
-
const existingFixPlan = await readFile(fixPlanPath, "utf-8");
|
|
174
|
-
existingItems = parseFixPlan(existingFixPlan);
|
|
175
|
-
debug(`Found ${existingItems.filter((item) => item.completed).length} completed stories in existing fix_plan`);
|
|
176
|
-
}
|
|
177
|
-
catch (err) {
|
|
178
|
-
if (isEnoent(err)) {
|
|
179
|
-
debug("No existing fix_plan found, starting fresh");
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
warn(`Could not read existing fix_plan: ${formatError(err)}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const sprintStatusSource = await resolveSprintStatusSource(projectDir, artifactsDir, collectedArtifacts, artifactContents);
|
|
186
|
-
if (sprintStatusSource) {
|
|
187
|
-
useTitleBasedMerge = false;
|
|
188
|
-
if (sprintStatusSource.readError) {
|
|
189
|
-
completionWarnings.push(`Sprint status file "${sprintStatusSource.displayPath}" could not be read: ${sprintStatusSource.readError}. All stories were left unchecked.`);
|
|
190
|
-
}
|
|
191
|
-
else if (!sprintStatusSource.content) {
|
|
192
|
-
completionWarnings.push(`Sprint status file "${sprintStatusSource.displayPath}" could not be read. All stories were left unchecked.`);
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
const sprintStatus = parseSprintStatus(sprintStatusSource.content);
|
|
196
|
-
completionWarnings.push(...sprintStatus.warnings);
|
|
197
|
-
if (sprintStatus.valid) {
|
|
198
|
-
completedIds = new Set(stories
|
|
199
|
-
.filter((story) => sprintStatus.storyStatusById.get(story.id) === "done")
|
|
200
|
-
.map((story) => story.id));
|
|
201
|
-
fixPlanPreserved = completedIds.size > 0;
|
|
202
|
-
for (const story of stories) {
|
|
203
|
-
if (!sprintStatus.storyStatusById.has(story.id)) {
|
|
204
|
-
completionWarnings.push(`Sprint status is missing story ${story.id} (${story.title}); leaving it unchecked.`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
completedIds = new Set(existingItems.filter((item) => item.completed).map((item) => item.id));
|
|
212
|
-
fixPlanPreserved = completedIds.size > 0;
|
|
213
|
-
// Detect orphaned completed stories (Bug #2)
|
|
214
|
-
const newStoryIds = new Set(stories.map((story) => story.id));
|
|
215
|
-
orphanWarnings = detectOrphanedCompletedStories(existingItems, newStoryIds);
|
|
216
|
-
// Build title maps for title-based merge (Gap 3: renumbered story preservation)
|
|
217
|
-
const completedTitles = buildCompletedTitleMap(existingItems);
|
|
218
|
-
const newTitleMap = new Map(stories.map((story) => [story.id, story.title]));
|
|
219
|
-
// Detect which stories were preserved via title match (for renumber warning suppression)
|
|
220
|
-
const preservedIds = new Set();
|
|
221
|
-
for (const [id, title] of newTitleMap) {
|
|
222
|
-
if (!completedIds.has(id) && completedTitles.has(normalizeTitle(title))) {
|
|
223
|
-
preservedIds.add(id);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
// Detect renumbered stories (Bug #3), skipping auto-preserved ones
|
|
227
|
-
renumberWarnings = detectRenumberedStories(existingItems, stories, preservedIds);
|
|
228
|
-
}
|
|
229
|
-
const completedTitles = buildCompletedTitleMap(existingItems);
|
|
230
|
-
const newTitleMap = new Map(stories.map((story) => [story.id, story.title]));
|
|
231
|
-
// Generate new fix_plan from current stories, preserving completion status
|
|
232
|
-
info(`Generating fix plan for ${stories.length} stories...`);
|
|
233
|
-
const planningSpecsSubpath = resolvePlanningSpecsSubpath(projectDir, artifactsDir);
|
|
234
|
-
const newFixPlan = generateFixPlan(stories, undefined, planningSpecsSubpath);
|
|
235
|
-
const mergedFixPlan = mergeFixPlanProgress(newFixPlan, completedIds, useTitleBasedMerge ? newTitleMap : undefined, useTitleBasedMerge ? completedTitles : undefined);
|
|
236
|
-
await atomicWriteFile(fixPlanPath, mergedFixPlan);
|
|
237
|
-
generatedFiles.push({
|
|
238
|
-
path: ".ralph/@fix_plan.md",
|
|
239
|
-
action: fixPlanExisted ? "updated" : "created",
|
|
240
|
-
});
|
|
11
|
+
const fixPlanSync = await syncFixPlan(projectDir, inputs);
|
|
12
|
+
generatedFiles.push(fixPlanSync.generatedFile);
|
|
241
13
|
const specsDir = join(projectDir, ".ralph/specs");
|
|
242
14
|
const specsTmpDir = join(projectDir, ".ralph/specs.new");
|
|
243
15
|
info("Preparing specs tree...");
|
|
244
|
-
await prepareSpecsDirectory(projectDir, artifactsDir, collectedArtifacts.files, specsTmpDir);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
warn(`Could not generate SPECS_CHANGELOG.md: ${formatError(err)}`);
|
|
256
|
-
}
|
|
257
|
-
info("Copying specs to .ralph/specs/...");
|
|
258
|
-
await swapSpecsDirectory(specsDir, specsTmpDir);
|
|
259
|
-
generatedFiles.push({ path: ".ralph/specs/", action: "updated" });
|
|
260
|
-
// Generate SPECS_INDEX.md for intelligent spec reading
|
|
261
|
-
info("Generating SPECS_INDEX.md...");
|
|
262
|
-
const specsIndexPath = join(projectDir, ".ralph/SPECS_INDEX.md");
|
|
263
|
-
const specsIndexExisted = await exists(specsIndexPath);
|
|
264
|
-
try {
|
|
265
|
-
const specsIndex = await generateSpecsIndex(specsDir);
|
|
266
|
-
if (specsIndex.totalFiles > 0) {
|
|
267
|
-
await atomicWriteFile(specsIndexPath, formatSpecsIndexMd(specsIndex));
|
|
268
|
-
generatedFiles.push({
|
|
269
|
-
path: ".ralph/SPECS_INDEX.md",
|
|
270
|
-
action: specsIndexExisted ? "updated" : "created",
|
|
271
|
-
});
|
|
272
|
-
debug(`Generated SPECS_INDEX.md with ${specsIndex.totalFiles} files`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
catch (err) {
|
|
276
|
-
warn(`Could not generate SPECS_INDEX.md: ${formatError(err)}`);
|
|
277
|
-
}
|
|
278
|
-
// Generate PROJECT_CONTEXT.md from planning artifacts
|
|
279
|
-
let projectName = "project";
|
|
280
|
-
try {
|
|
281
|
-
const config = await readConfig(projectDir);
|
|
282
|
-
if (config?.name) {
|
|
283
|
-
projectName = config.name;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
debug(`Could not read config for project name: ${formatError(err)}`);
|
|
288
|
-
}
|
|
289
|
-
// Extract project context for both PROJECT_CONTEXT.md and PROMPT.md
|
|
290
|
-
info("Generating PROJECT_CONTEXT.md...");
|
|
291
|
-
const projectContextPath = join(projectDir, ".ralph/PROJECT_CONTEXT.md");
|
|
292
|
-
const projectContextExisted = await exists(projectContextPath);
|
|
293
|
-
let projectContext = null;
|
|
294
|
-
let truncationWarnings = [];
|
|
295
|
-
if (artifactContents.size > 0) {
|
|
296
|
-
const { context, truncated } = extractProjectContext(artifactContents);
|
|
297
|
-
projectContext = context;
|
|
298
|
-
truncationWarnings = detectTruncation(truncated);
|
|
299
|
-
const contextMd = generateProjectContextMd(projectContext, projectName);
|
|
300
|
-
await atomicWriteFile(projectContextPath, contextMd);
|
|
301
|
-
generatedFiles.push({
|
|
302
|
-
path: ".ralph/PROJECT_CONTEXT.md",
|
|
303
|
-
action: projectContextExisted ? "updated" : "created",
|
|
304
|
-
});
|
|
305
|
-
debug("Generated PROJECT_CONTEXT.md");
|
|
306
|
-
}
|
|
307
|
-
// Generate PROMPT.md with embedded context
|
|
308
|
-
info("Generating PROMPT.md...");
|
|
309
|
-
// Try to preserve rich PROMPT.md template if it has the placeholder
|
|
310
|
-
let prompt;
|
|
311
|
-
let promptExisted = false;
|
|
312
|
-
try {
|
|
313
|
-
const existingPrompt = await readFile(join(projectDir, ".ralph/PROMPT.md"), "utf-8");
|
|
314
|
-
promptExisted = true;
|
|
315
|
-
if (existingPrompt.includes("[YOUR PROJECT NAME]")) {
|
|
316
|
-
prompt = existingPrompt.replace(/\[YOUR PROJECT NAME\]/g, projectName);
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
// Pass context to embed critical specs directly in PROMPT.md
|
|
320
|
-
prompt = generatePrompt(projectName, projectContext ?? undefined);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
catch (err) {
|
|
324
|
-
if (isEnoent(err)) {
|
|
325
|
-
debug("No existing PROMPT.md found, generating from template");
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
warn(`Could not read existing PROMPT.md: ${formatError(err)}`);
|
|
329
|
-
}
|
|
330
|
-
prompt = generatePrompt(projectName, projectContext ?? undefined);
|
|
331
|
-
}
|
|
332
|
-
await atomicWriteFile(join(projectDir, ".ralph/PROMPT.md"), prompt);
|
|
333
|
-
generatedFiles.push({ path: ".ralph/PROMPT.md", action: promptExisted ? "updated" : "created" });
|
|
334
|
-
// Customize @AGENT.md based on detected tech stack from architecture
|
|
335
|
-
const combinedArchitectureContent = combineArtifactContents(collectedArtifacts.architectureFiles, artifactContents);
|
|
336
|
-
if (combinedArchitectureContent) {
|
|
337
|
-
try {
|
|
338
|
-
const stack = detectTechStack(combinedArchitectureContent);
|
|
339
|
-
if (stack) {
|
|
340
|
-
const agentPath = join(projectDir, ".ralph/@AGENT.md");
|
|
341
|
-
const agentTemplate = await readFile(agentPath, "utf-8");
|
|
342
|
-
const customized = customizeAgentMd(agentTemplate, stack);
|
|
343
|
-
await atomicWriteFile(agentPath, customized);
|
|
344
|
-
generatedFiles.push({ path: ".ralph/@AGENT.md", action: "updated" });
|
|
345
|
-
debug("Customized @AGENT.md with detected tech stack");
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
catch (err) {
|
|
349
|
-
warn(`Could not customize @AGENT.md: ${formatError(err)}`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
// Collect warnings from all sources
|
|
353
|
-
const preflightWarnings = preflightIssues
|
|
354
|
-
.filter((i) => i.severity === "warning")
|
|
355
|
-
.map((i) => i.message);
|
|
356
|
-
// Keep parse warnings not already covered by preflight.
|
|
357
|
-
const nonPreflightParseWarnings = parseWarnings.filter((w) => !/malformed story id/i.test(w) &&
|
|
358
|
-
!/has no acceptance criteria/i.test(w) &&
|
|
359
|
-
!/has no description/i.test(w) &&
|
|
360
|
-
!/not under an epic/i.test(w));
|
|
16
|
+
await prepareSpecsDirectory(projectDir, inputs.artifactsDir, inputs.collectedArtifacts.files, specsTmpDir);
|
|
17
|
+
generatedFiles.push(...(await syncPreparedSpecsDirectory(projectDir, specsDir, specsTmpDir)));
|
|
18
|
+
const contextOutput = await generateContextOutputs(projectDir, inputs);
|
|
19
|
+
generatedFiles.push(...contextOutput.generatedFiles);
|
|
20
|
+
const preflightWarnings = inputs.preflightIssues
|
|
21
|
+
.filter((issue) => issue.severity === "warning")
|
|
22
|
+
.map((issue) => issue.message);
|
|
23
|
+
const nonPreflightParseWarnings = inputs.parseWarnings.filter((warning) => !/malformed story id/i.test(warning) &&
|
|
24
|
+
!/has no acceptance criteria/i.test(warning) &&
|
|
25
|
+
!/has no description/i.test(warning) &&
|
|
26
|
+
!/not under an epic/i.test(warning));
|
|
361
27
|
const warnings = [
|
|
362
28
|
...preflightWarnings,
|
|
363
29
|
...nonPreflightParseWarnings,
|
|
364
|
-
...
|
|
365
|
-
...
|
|
366
|
-
...renumberWarnings,
|
|
367
|
-
...truncationWarnings,
|
|
30
|
+
...fixPlanSync.warnings,
|
|
31
|
+
...contextOutput.warnings,
|
|
368
32
|
];
|
|
369
|
-
// Update phase state to 4 (Implementation) - Bug #1
|
|
370
33
|
const now = new Date().toISOString();
|
|
371
34
|
const currentState = await readState(projectDir);
|
|
372
35
|
const newState = {
|
|
@@ -378,10 +41,10 @@ export async function runTransition(projectDir, options) {
|
|
|
378
41
|
await writeState(projectDir, newState);
|
|
379
42
|
info("Transition complete: phase 4 (implementing)");
|
|
380
43
|
return {
|
|
381
|
-
storiesCount: stories.length,
|
|
44
|
+
storiesCount: inputs.stories.length,
|
|
382
45
|
warnings,
|
|
383
|
-
fixPlanPreserved,
|
|
384
|
-
preflightIssues,
|
|
46
|
+
fixPlanPreserved: fixPlanSync.fixPlanPreserved,
|
|
47
|
+
preflightIssues: inputs.preflightIssues,
|
|
385
48
|
generatedFiles,
|
|
386
49
|
};
|
|
387
50
|
}
|