bmalph 2.3.0 → 2.5.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 +105 -38
- package/dist/cli.js +19 -0
- package/dist/commands/doctor.d.ts +0 -11
- package/dist/commands/doctor.js +22 -55
- package/dist/commands/implement.d.ts +6 -0
- package/dist/commands/implement.js +82 -0
- package/dist/commands/init.js +4 -2
- package/dist/commands/reset.d.ts +7 -0
- package/dist/commands/reset.js +81 -0
- package/dist/commands/status.js +100 -11
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +19 -0
- package/dist/installer.d.ts +0 -6
- package/dist/installer.js +0 -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 +0 -1
- package/dist/transition/artifacts.js +0 -26
- 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/index.d.ts +1 -1
- package/dist/transition/index.js +1 -1
- 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/state.d.ts +0 -2
- package/dist/utils/validate.js +3 -2
- package/dist/watch/dashboard.d.ts +4 -0
- package/dist/watch/dashboard.js +60 -0
- package/dist/watch/file-watcher.d.ts +9 -0
- package/dist/watch/file-watcher.js +27 -0
- package/dist/watch/renderer.d.ts +16 -0
- package/dist/watch/renderer.js +241 -0
- package/dist/watch/state-reader.d.ts +9 -0
- package/dist/watch/state-reader.js +190 -0
- package/dist/watch/types.d.ts +55 -0
- package/dist/watch/types.js +1 -0
- package/package.json +9 -4
- package/ralph/lib/circuit_breaker.sh +86 -59
- package/ralph/lib/enable_core.sh +3 -6
- package/ralph/lib/response_analyzer.sh +5 -29
- package/ralph/lib/task_sources.sh +45 -11
- package/ralph/lib/wizard_utils.sh +9 -0
- package/ralph/ralph_import.sh +7 -2
- package/ralph/ralph_loop.sh +44 -34
- package/ralph/ralph_monitor.sh +4 -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
- package/slash-commands/bmalph-watch.md +20 -0
package/dist/commands/status.js
CHANGED
|
@@ -2,6 +2,8 @@ import chalk from "chalk";
|
|
|
2
2
|
import { readConfig } from "../utils/config.js";
|
|
3
3
|
import { readState, readRalphStatus, getPhaseLabel, getPhaseInfo } from "../utils/state.js";
|
|
4
4
|
import { withErrorHandling } from "../utils/errors.js";
|
|
5
|
+
import { resolveProjectPlatform } from "../platform/resolve.js";
|
|
6
|
+
import { scanProjectArtifacts } from "../transition/artifact-scan.js";
|
|
5
7
|
export async function statusCommand(options) {
|
|
6
8
|
await withErrorHandling(() => runStatus(options));
|
|
7
9
|
}
|
|
@@ -15,17 +17,37 @@ export async function runStatus(options) {
|
|
|
15
17
|
}
|
|
16
18
|
// Read current state
|
|
17
19
|
const state = await readState(projectDir);
|
|
18
|
-
const
|
|
20
|
+
const storedPhase = state?.currentPhase ?? 1;
|
|
19
21
|
const status = state?.status ?? "planning";
|
|
20
|
-
const phaseName = getPhaseLabel(phase);
|
|
21
|
-
const phaseInfo = getPhaseInfo(phase);
|
|
22
22
|
// Read Ralph status if in implementation phase
|
|
23
23
|
let ralphStatus = null;
|
|
24
|
-
if (
|
|
24
|
+
if (storedPhase === 4) {
|
|
25
25
|
ralphStatus = await readRalphStatus(projectDir);
|
|
26
26
|
}
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// Scan artifacts for phases 1-3 to detect actual progress
|
|
28
|
+
let artifactScan = null;
|
|
29
|
+
let phase = storedPhase;
|
|
30
|
+
let phaseDetected = false;
|
|
31
|
+
if (phase < 4) {
|
|
32
|
+
artifactScan = await scanProjectArtifacts(projectDir);
|
|
33
|
+
if (artifactScan && artifactScan.detectedPhase > phase) {
|
|
34
|
+
phase = artifactScan.detectedPhase;
|
|
35
|
+
phaseDetected = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const phaseName = getPhaseLabel(phase);
|
|
39
|
+
const phaseInfo = getPhaseInfo(phase);
|
|
40
|
+
// Resolve platform for next action hints
|
|
41
|
+
const platform = await resolveProjectPlatform(projectDir);
|
|
42
|
+
// Determine next action — use artifact-based suggestion when available
|
|
43
|
+
const nextAction = artifactScan && phaseDetected
|
|
44
|
+
? artifactScan.nextAction
|
|
45
|
+
: getNextAction(phase, status, ralphStatus, platform);
|
|
46
|
+
// Detect when Ralph completed but bmalph state hasn't caught up
|
|
47
|
+
const completionMismatch = phase === 4 &&
|
|
48
|
+
status === "implementing" &&
|
|
49
|
+
ralphStatus !== null &&
|
|
50
|
+
ralphStatus.status === "completed";
|
|
29
51
|
if (options.json) {
|
|
30
52
|
const output = {
|
|
31
53
|
phase,
|
|
@@ -40,17 +62,37 @@ export async function runStatus(options) {
|
|
|
40
62
|
tasksTotal: ralphStatus.tasksTotal,
|
|
41
63
|
};
|
|
42
64
|
}
|
|
65
|
+
if (artifactScan) {
|
|
66
|
+
output.artifacts = {
|
|
67
|
+
directory: artifactScan.directory,
|
|
68
|
+
found: artifactScan.found,
|
|
69
|
+
detectedPhase: artifactScan.detectedPhase,
|
|
70
|
+
missing: artifactScan.missing,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
43
73
|
if (nextAction) {
|
|
44
74
|
output.nextAction = nextAction;
|
|
45
75
|
}
|
|
76
|
+
if (completionMismatch) {
|
|
77
|
+
output.completionMismatch = true;
|
|
78
|
+
}
|
|
46
79
|
console.log(JSON.stringify(output, null, 2));
|
|
47
80
|
return;
|
|
48
81
|
}
|
|
49
82
|
// Human-readable output
|
|
50
83
|
console.log(chalk.bold("bmalph status\n"));
|
|
51
|
-
|
|
84
|
+
const phaseLabel = phaseDetected
|
|
85
|
+
? `${phase} - ${phaseName} (detected from artifacts)`
|
|
86
|
+
: `${phase} - ${phaseName}`;
|
|
87
|
+
console.log(` ${chalk.cyan("Phase:")} ${phaseLabel}`);
|
|
52
88
|
console.log(` ${chalk.cyan("Agent:")} ${phaseInfo.agent}`);
|
|
53
89
|
console.log(` ${chalk.cyan("Status:")} ${formatStatus(status)}`);
|
|
90
|
+
// Show artifact checklist for phases 1-3
|
|
91
|
+
if (artifactScan) {
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log(chalk.bold(` Artifacts (${artifactScan.directory})`));
|
|
94
|
+
printArtifactChecklist(artifactScan);
|
|
95
|
+
}
|
|
54
96
|
if (phase === 4 && ralphStatus) {
|
|
55
97
|
console.log("");
|
|
56
98
|
console.log(chalk.bold(" Ralph Loop"));
|
|
@@ -63,11 +105,55 @@ export async function runStatus(options) {
|
|
|
63
105
|
console.log(chalk.bold(" Ralph Loop"));
|
|
64
106
|
console.log(` ${chalk.cyan("Status:")} ${chalk.dim("not started")}`);
|
|
65
107
|
}
|
|
66
|
-
if (
|
|
108
|
+
if (completionMismatch) {
|
|
109
|
+
console.log("");
|
|
110
|
+
console.log(chalk.green(" Ralph has completed all tasks."));
|
|
111
|
+
console.log(` ${chalk.cyan("Next:")} Review changes and update project phase`);
|
|
112
|
+
}
|
|
113
|
+
else if (nextAction) {
|
|
67
114
|
console.log("");
|
|
68
115
|
console.log(` ${chalk.cyan("Next:")} ${nextAction}`);
|
|
69
116
|
}
|
|
70
117
|
}
|
|
118
|
+
const ARTIFACT_DEFINITIONS = [
|
|
119
|
+
{ phase: 1, name: "Product Brief", required: false },
|
|
120
|
+
{ phase: 1, name: "Market Research", required: false },
|
|
121
|
+
{ phase: 1, name: "Domain Research", required: false },
|
|
122
|
+
{ phase: 1, name: "Technical Research", required: false },
|
|
123
|
+
{ phase: 2, name: "PRD", required: true },
|
|
124
|
+
{ phase: 2, name: "UX Design", required: false },
|
|
125
|
+
{ phase: 3, name: "Architecture", required: true },
|
|
126
|
+
{ phase: 3, name: "Epics & Stories", required: true },
|
|
127
|
+
{ phase: 3, name: "Readiness Report", required: true },
|
|
128
|
+
];
|
|
129
|
+
const PHASE_LABELS = {
|
|
130
|
+
1: "Phase 1 - Analysis",
|
|
131
|
+
2: "Phase 2 - Planning",
|
|
132
|
+
3: "Phase 3 - Solutioning",
|
|
133
|
+
};
|
|
134
|
+
function printArtifactChecklist(scan) {
|
|
135
|
+
const foundByName = new Map();
|
|
136
|
+
for (const artifacts of [scan.phases[1], scan.phases[2], scan.phases[3]]) {
|
|
137
|
+
for (const artifact of artifacts) {
|
|
138
|
+
foundByName.set(artifact.name, artifact);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let currentPhase = 0;
|
|
142
|
+
for (const def of ARTIFACT_DEFINITIONS) {
|
|
143
|
+
if (def.phase !== currentPhase) {
|
|
144
|
+
currentPhase = def.phase;
|
|
145
|
+
console.log(` ${PHASE_LABELS[currentPhase]}`);
|
|
146
|
+
}
|
|
147
|
+
const found = foundByName.get(def.name);
|
|
148
|
+
if (found) {
|
|
149
|
+
console.log(` ${chalk.green("*")} ${def.name} (${found.filename})`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const suffix = def.required ? " (required)" : "";
|
|
153
|
+
console.log(` ${chalk.dim("-")} ${def.name}${suffix}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
71
157
|
function formatStatus(status) {
|
|
72
158
|
switch (status) {
|
|
73
159
|
case "planning":
|
|
@@ -94,7 +180,7 @@ function formatRalphStatus(status) {
|
|
|
94
180
|
return status;
|
|
95
181
|
}
|
|
96
182
|
}
|
|
97
|
-
function getNextAction(phase, status, ralphStatus) {
|
|
183
|
+
function getNextAction(phase, status, ralphStatus, platform) {
|
|
98
184
|
if (status === "completed") {
|
|
99
185
|
return null;
|
|
100
186
|
}
|
|
@@ -104,10 +190,13 @@ function getNextAction(phase, status, ralphStatus) {
|
|
|
104
190
|
case 2:
|
|
105
191
|
return "Run /pm to create PRD";
|
|
106
192
|
case 3:
|
|
107
|
-
return "Run
|
|
193
|
+
return "Run: bmalph implement";
|
|
108
194
|
case 4:
|
|
109
195
|
if (!ralphStatus || ralphStatus.status === "not_started") {
|
|
110
|
-
|
|
196
|
+
if (platform.tier === "full") {
|
|
197
|
+
return `Start Ralph loop with: bash .ralph/drivers/${platform.id}.sh`;
|
|
198
|
+
}
|
|
199
|
+
return "Ralph requires a full-tier platform (Claude Code or Codex)";
|
|
111
200
|
}
|
|
112
201
|
if (ralphStatus.status === "blocked") {
|
|
113
202
|
return "Review Ralph logs: bmalph doctor";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readConfig } from "../utils/config.js";
|
|
2
|
+
import { withErrorHandling } from "../utils/errors.js";
|
|
3
|
+
import { startDashboard } from "../watch/dashboard.js";
|
|
4
|
+
const DEFAULT_INTERVAL_MS = 2000;
|
|
5
|
+
export async function watchCommand(options) {
|
|
6
|
+
await withErrorHandling(() => runWatch(options));
|
|
7
|
+
}
|
|
8
|
+
async function runWatch(options) {
|
|
9
|
+
const projectDir = options.projectDir;
|
|
10
|
+
const config = await readConfig(projectDir);
|
|
11
|
+
if (!config) {
|
|
12
|
+
throw new Error("Project not initialized. Run: bmalph init");
|
|
13
|
+
}
|
|
14
|
+
const interval = options.interval ? parseInt(options.interval, 10) : DEFAULT_INTERVAL_MS;
|
|
15
|
+
if (isNaN(interval) || interval < 500) {
|
|
16
|
+
throw new Error("Interval must be a number >= 500 (milliseconds)");
|
|
17
|
+
}
|
|
18
|
+
await startDashboard({ projectDir, interval });
|
|
19
|
+
}
|
package/dist/installer.d.ts
CHANGED
|
@@ -28,12 +28,6 @@ export declare function generateManifests(projectDir: string): Promise<void>;
|
|
|
28
28
|
* Creates the file if it doesn't exist, replaces an existing BMAD section on upgrade.
|
|
29
29
|
*/
|
|
30
30
|
export declare function mergeInstructionsFile(projectDir: string, platform?: Platform): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
* @deprecated Use `mergeInstructionsFile(projectDir)` instead.
|
|
33
|
-
* Kept for backward compatibility during migration.
|
|
34
|
-
*/
|
|
35
|
-
export declare function mergeClaudeMd(projectDir: string): Promise<void>;
|
|
36
31
|
export declare function isInitialized(projectDir: string): Promise<boolean>;
|
|
37
|
-
export declare function hasExistingBmadDir(projectDir: string): Promise<boolean>;
|
|
38
32
|
export declare function previewInstall(projectDir: string, platform?: Platform): Promise<PreviewInstallResult>;
|
|
39
33
|
export declare function previewUpgrade(projectDir: string, platform?: Platform): Promise<PreviewUpgradeResult>;
|
package/dist/installer.js
CHANGED
|
@@ -424,19 +424,9 @@ export async function mergeInstructionsFile(projectDir, platform) {
|
|
|
424
424
|
}
|
|
425
425
|
await atomicWriteFile(instructionsPath, existing + snippet);
|
|
426
426
|
}
|
|
427
|
-
/**
|
|
428
|
-
* @deprecated Use `mergeInstructionsFile(projectDir)` instead.
|
|
429
|
-
* Kept for backward compatibility during migration.
|
|
430
|
-
*/
|
|
431
|
-
export async function mergeClaudeMd(projectDir) {
|
|
432
|
-
return mergeInstructionsFile(projectDir);
|
|
433
|
-
}
|
|
434
427
|
export async function isInitialized(projectDir) {
|
|
435
428
|
return exists(join(projectDir, CONFIG_FILE));
|
|
436
429
|
}
|
|
437
|
-
export async function hasExistingBmadDir(projectDir) {
|
|
438
|
-
return exists(join(projectDir, "_bmad"));
|
|
439
|
-
}
|
|
440
430
|
export async function previewInstall(projectDir, platform) {
|
|
441
431
|
const p = platform ?? (await getDefaultPlatform());
|
|
442
432
|
const wouldCreate = [];
|
|
@@ -36,7 +36,6 @@ Use \`/bmalph\` to navigate phases. Use \`/bmad-help\` to discover all commands.
|
|
|
36
36
|
| \`/bmalph-implement\` | Transition planning artifacts → prepare Ralph loop |
|
|
37
37
|
| \`/bmalph-upgrade\` | Update bundled assets to match current bmalph version |
|
|
38
38
|
| \`/bmalph-doctor\` | Check project health and report issues |
|
|
39
|
-
| \`/bmalph-reset\` | Reset state (soft or hard reset with confirmation) |
|
|
40
39
|
|
|
41
40
|
### Available Agents
|
|
42
41
|
|
package/dist/reset.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Platform } from "./platform/types.js";
|
|
2
|
+
import type { DryRunAction } from "./utils/dryrun.js";
|
|
3
|
+
export interface ResetPlan {
|
|
4
|
+
directories: string[];
|
|
5
|
+
commandFiles: string[];
|
|
6
|
+
instructionsCleanup: {
|
|
7
|
+
path: string;
|
|
8
|
+
sectionsToRemove: string[];
|
|
9
|
+
} | null;
|
|
10
|
+
gitignoreLines: string[];
|
|
11
|
+
warnings: Array<{
|
|
12
|
+
path: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildResetPlan(projectDir: string, platform: Platform): Promise<ResetPlan>;
|
|
17
|
+
export declare function executeResetPlan(projectDir: string, plan: ResetPlan): Promise<void>;
|
|
18
|
+
export declare function planToDryRunActions(plan: ResetPlan): DryRunAction[];
|
package/dist/reset.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readdir, readFile, rm } from "fs/promises";
|
|
2
|
+
import { join, posix } from "path";
|
|
3
|
+
import { getSlashCommandsDir } from "./installer.js";
|
|
4
|
+
import { exists, atomicWriteFile } from "./utils/file-system.js";
|
|
5
|
+
import { isEnoent } from "./utils/errors.js";
|
|
6
|
+
import { BMAD_DIR, RALPH_DIR, BMALPH_DIR, BMAD_OUTPUT_DIR } from "./utils/constants.js";
|
|
7
|
+
export async function buildResetPlan(projectDir, platform) {
|
|
8
|
+
const plan = {
|
|
9
|
+
directories: [],
|
|
10
|
+
commandFiles: [],
|
|
11
|
+
instructionsCleanup: null,
|
|
12
|
+
gitignoreLines: [],
|
|
13
|
+
warnings: [],
|
|
14
|
+
};
|
|
15
|
+
// Check which managed directories exist
|
|
16
|
+
for (const dir of [BMAD_DIR, RALPH_DIR, BMALPH_DIR]) {
|
|
17
|
+
if (await exists(join(projectDir, dir))) {
|
|
18
|
+
plan.directories.push(dir);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Check for slash commands to remove (directory delivery only)
|
|
22
|
+
if (platform.commandDelivery.kind === "directory") {
|
|
23
|
+
const commandsDir = join(projectDir, platform.commandDelivery.dir);
|
|
24
|
+
if (await exists(commandsDir)) {
|
|
25
|
+
const bundledNames = await getBundledCommandNames();
|
|
26
|
+
try {
|
|
27
|
+
const existingFiles = await readdir(commandsDir);
|
|
28
|
+
for (const file of existingFiles) {
|
|
29
|
+
if (file.endsWith(".md") && bundledNames.has(file)) {
|
|
30
|
+
plan.commandFiles.push(posix.join(platform.commandDelivery.dir, file));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (!isEnoent(err))
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Check instructions file for BMAD sections
|
|
41
|
+
try {
|
|
42
|
+
const content = await readFile(join(projectDir, platform.instructionsFile), "utf-8");
|
|
43
|
+
const sectionsToRemove = [];
|
|
44
|
+
if (content.includes(platform.instructionsSectionMarker)) {
|
|
45
|
+
sectionsToRemove.push(platform.instructionsSectionMarker);
|
|
46
|
+
}
|
|
47
|
+
// Codex (inline) also has a BMAD Commands section
|
|
48
|
+
if (platform.commandDelivery.kind === "inline" && content.includes("## BMAD Commands")) {
|
|
49
|
+
sectionsToRemove.push("## BMAD Commands");
|
|
50
|
+
}
|
|
51
|
+
if (sectionsToRemove.length > 0) {
|
|
52
|
+
plan.instructionsCleanup = {
|
|
53
|
+
path: platform.instructionsFile,
|
|
54
|
+
sectionsToRemove,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (!isEnoent(err))
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
// Check .gitignore for bmalph entries
|
|
63
|
+
try {
|
|
64
|
+
const content = await readFile(join(projectDir, ".gitignore"), "utf-8");
|
|
65
|
+
const existingLines = new Set(content
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.filter(Boolean));
|
|
69
|
+
const bmalpEntries = [".ralph/logs/", "_bmad-output/"];
|
|
70
|
+
for (const entry of bmalpEntries) {
|
|
71
|
+
if (existingLines.has(entry)) {
|
|
72
|
+
plan.gitignoreLines.push(entry);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (!isEnoent(err))
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
// Warn about _bmad-output/
|
|
81
|
+
if (await exists(join(projectDir, BMAD_OUTPUT_DIR))) {
|
|
82
|
+
plan.warnings.push({
|
|
83
|
+
path: `${BMAD_OUTPUT_DIR}/`,
|
|
84
|
+
message: "Contains user planning artifacts — not removed by reset",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return plan;
|
|
88
|
+
}
|
|
89
|
+
async function getBundledCommandNames() {
|
|
90
|
+
const slashCommandsDir = getSlashCommandsDir();
|
|
91
|
+
try {
|
|
92
|
+
const files = await readdir(slashCommandsDir);
|
|
93
|
+
return new Set(files.filter((f) => f.endsWith(".md")));
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (!isEnoent(err))
|
|
97
|
+
throw err;
|
|
98
|
+
return new Set();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export async function executeResetPlan(projectDir, plan) {
|
|
102
|
+
// Delete managed directories
|
|
103
|
+
for (const dir of plan.directories) {
|
|
104
|
+
await rm(join(projectDir, dir), { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
// Delete slash command files
|
|
107
|
+
for (const file of plan.commandFiles) {
|
|
108
|
+
await rm(join(projectDir, file), { force: true });
|
|
109
|
+
}
|
|
110
|
+
// Clean instructions file
|
|
111
|
+
if (plan.instructionsCleanup) {
|
|
112
|
+
const filePath = join(projectDir, plan.instructionsCleanup.path);
|
|
113
|
+
try {
|
|
114
|
+
let content = await readFile(filePath, "utf-8");
|
|
115
|
+
for (const marker of plan.instructionsCleanup.sectionsToRemove) {
|
|
116
|
+
content = removeSection(content, marker);
|
|
117
|
+
}
|
|
118
|
+
content = content.trim();
|
|
119
|
+
if (content.length === 0) {
|
|
120
|
+
await rm(filePath, { force: true });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
await atomicWriteFile(filePath, content + "\n");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (!isEnoent(err))
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Clean .gitignore
|
|
132
|
+
if (plan.gitignoreLines.length > 0) {
|
|
133
|
+
const filePath = join(projectDir, ".gitignore");
|
|
134
|
+
try {
|
|
135
|
+
const content = await readFile(filePath, "utf-8");
|
|
136
|
+
const cleaned = removeGitignoreLines(content, plan.gitignoreLines);
|
|
137
|
+
await atomicWriteFile(filePath, cleaned);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (!isEnoent(err))
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function removeSection(content, marker) {
|
|
146
|
+
if (!content.includes(marker))
|
|
147
|
+
return content;
|
|
148
|
+
const sectionStart = content.indexOf(marker);
|
|
149
|
+
const before = content.slice(0, sectionStart);
|
|
150
|
+
const afterSection = content.slice(sectionStart);
|
|
151
|
+
// Find next level-2 heading that doesn't match this section's heading
|
|
152
|
+
const markerEscaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
153
|
+
const nextHeadingMatch = afterSection.match(new RegExp(`\\n## (?!${markerEscaped.slice(3)})`));
|
|
154
|
+
const after = nextHeadingMatch ? afterSection.slice(nextHeadingMatch.index) : "";
|
|
155
|
+
return before.trimEnd() + after;
|
|
156
|
+
}
|
|
157
|
+
function removeGitignoreLines(content, linesToRemove) {
|
|
158
|
+
const removeSet = new Set(linesToRemove);
|
|
159
|
+
const lines = content.split(/\r?\n/);
|
|
160
|
+
const filtered = lines.filter((line) => !removeSet.has(line.trim()));
|
|
161
|
+
return filtered.join("\n");
|
|
162
|
+
}
|
|
163
|
+
export function planToDryRunActions(plan) {
|
|
164
|
+
const actions = [];
|
|
165
|
+
for (const dir of plan.directories) {
|
|
166
|
+
actions.push({ type: "delete", path: `${dir}/` });
|
|
167
|
+
}
|
|
168
|
+
for (const file of plan.commandFiles) {
|
|
169
|
+
actions.push({ type: "delete", path: file });
|
|
170
|
+
}
|
|
171
|
+
if (plan.instructionsCleanup) {
|
|
172
|
+
actions.push({ type: "modify", path: plan.instructionsCleanup.path });
|
|
173
|
+
}
|
|
174
|
+
if (plan.gitignoreLines.length > 0) {
|
|
175
|
+
actions.push({ type: "modify", path: ".gitignore" });
|
|
176
|
+
}
|
|
177
|
+
for (const warning of plan.warnings) {
|
|
178
|
+
actions.push({ type: "warn", path: warning.path, reason: warning.message });
|
|
179
|
+
}
|
|
180
|
+
return actions;
|
|
181
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ArtifactClassification {
|
|
2
|
+
phase: number;
|
|
3
|
+
name: string;
|
|
4
|
+
required: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ScannedArtifact extends ArtifactClassification {
|
|
7
|
+
filename: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PhaseArtifacts {
|
|
10
|
+
1: ScannedArtifact[];
|
|
11
|
+
2: ScannedArtifact[];
|
|
12
|
+
3: ScannedArtifact[];
|
|
13
|
+
}
|
|
14
|
+
export interface ProjectArtifactScan {
|
|
15
|
+
directory: string;
|
|
16
|
+
found: string[];
|
|
17
|
+
detectedPhase: number;
|
|
18
|
+
missing: string[];
|
|
19
|
+
phases: PhaseArtifacts;
|
|
20
|
+
nextAction: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function classifyArtifact(filename: string): ArtifactClassification | null;
|
|
23
|
+
export declare function scanArtifacts(files: string[]): PhaseArtifacts;
|
|
24
|
+
export declare function detectPhase(phases: PhaseArtifacts): number;
|
|
25
|
+
export declare function getMissing(phases: PhaseArtifacts): string[];
|
|
26
|
+
export declare function suggestNext(phases: PhaseArtifacts, detectedPhase: number): string;
|
|
27
|
+
export declare function scanProjectArtifacts(projectDir: string): Promise<ProjectArtifactScan | null>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readdir } from "fs/promises";
|
|
2
|
+
import { relative } from "path";
|
|
3
|
+
import { findArtifactsDir } from "./artifacts.js";
|
|
4
|
+
const ARTIFACT_RULES = [
|
|
5
|
+
{ pattern: /brief/i, phase: 1, name: "Product Brief", required: false },
|
|
6
|
+
{ pattern: /market/i, phase: 1, name: "Market Research", required: false },
|
|
7
|
+
{ pattern: /domain/i, phase: 1, name: "Domain Research", required: false },
|
|
8
|
+
{ pattern: /tech.*research/i, phase: 1, name: "Technical Research", required: false },
|
|
9
|
+
{ pattern: /prd/i, phase: 2, name: "PRD", required: true },
|
|
10
|
+
{ pattern: /ux/i, phase: 2, name: "UX Design", required: false },
|
|
11
|
+
{ pattern: /architect/i, phase: 3, name: "Architecture", required: true },
|
|
12
|
+
{ pattern: /epic|stor/i, phase: 3, name: "Epics & Stories", required: true },
|
|
13
|
+
{ pattern: /readiness/i, phase: 3, name: "Readiness Report", required: true },
|
|
14
|
+
];
|
|
15
|
+
export function classifyArtifact(filename) {
|
|
16
|
+
for (const rule of ARTIFACT_RULES) {
|
|
17
|
+
if (rule.pattern.test(filename)) {
|
|
18
|
+
return { phase: rule.phase, name: rule.name, required: rule.required };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function scanArtifacts(files) {
|
|
24
|
+
const phases = { 1: [], 2: [], 3: [] };
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const classification = classifyArtifact(file);
|
|
27
|
+
if (classification) {
|
|
28
|
+
const phaseKey = classification.phase;
|
|
29
|
+
phases[phaseKey].push({ ...classification, filename: file });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return phases;
|
|
33
|
+
}
|
|
34
|
+
export function detectPhase(phases) {
|
|
35
|
+
for (const phase of [3, 2, 1]) {
|
|
36
|
+
if (phases[phase].length > 0) {
|
|
37
|
+
return phase;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
export function getMissing(phases) {
|
|
43
|
+
const missing = [];
|
|
44
|
+
const foundNames = new Set([...phases[1], ...phases[2], ...phases[3]].map((a) => a.name));
|
|
45
|
+
for (const rule of ARTIFACT_RULES) {
|
|
46
|
+
if (rule.required && !foundNames.has(rule.name)) {
|
|
47
|
+
missing.push(rule.name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return missing;
|
|
51
|
+
}
|
|
52
|
+
export function suggestNext(phases, detectedPhase) {
|
|
53
|
+
const foundNames = new Set([...phases[1], ...phases[2], ...phases[3]].map((a) => a.name));
|
|
54
|
+
if (detectedPhase <= 1 && phases[1].length === 0) {
|
|
55
|
+
return "Run /analyst to start analysis";
|
|
56
|
+
}
|
|
57
|
+
if (!foundNames.has("PRD")) {
|
|
58
|
+
return "Run /create-prd to create the PRD";
|
|
59
|
+
}
|
|
60
|
+
if (!foundNames.has("Architecture")) {
|
|
61
|
+
return "Run /architect to create architecture";
|
|
62
|
+
}
|
|
63
|
+
if (!foundNames.has("Epics & Stories")) {
|
|
64
|
+
return "Run /create-epics-stories to define epics and stories";
|
|
65
|
+
}
|
|
66
|
+
if (!foundNames.has("Readiness Report")) {
|
|
67
|
+
return "Run /architect to generate readiness report";
|
|
68
|
+
}
|
|
69
|
+
return "Run: bmalph implement";
|
|
70
|
+
}
|
|
71
|
+
export async function scanProjectArtifacts(projectDir) {
|
|
72
|
+
const artifactsDir = await findArtifactsDir(projectDir);
|
|
73
|
+
if (!artifactsDir) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const files = await readdir(artifactsDir);
|
|
77
|
+
const phases = scanArtifacts(files);
|
|
78
|
+
const detectedPhase = detectPhase(phases);
|
|
79
|
+
const missing = getMissing(phases);
|
|
80
|
+
const nextAction = suggestNext(phases, detectedPhase);
|
|
81
|
+
const relativeDir = relative(projectDir, artifactsDir).replace(/\\/g, "/");
|
|
82
|
+
const found = files.filter((f) => classifyArtifact(f) !== null);
|
|
83
|
+
return {
|
|
84
|
+
directory: relativeDir,
|
|
85
|
+
found,
|
|
86
|
+
detectedPhase,
|
|
87
|
+
missing,
|
|
88
|
+
phases,
|
|
89
|
+
nextAction,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
2
1
|
import { join } from "path";
|
|
3
2
|
import { debug } from "../utils/logger.js";
|
|
4
3
|
import { exists } from "../utils/file-system.js";
|
|
@@ -19,28 +18,3 @@ export async function findArtifactsDir(projectDir) {
|
|
|
19
18
|
debug(`No artifacts found. Checked: ${candidates.join(", ")}`);
|
|
20
19
|
return null;
|
|
21
20
|
}
|
|
22
|
-
export async function validateArtifacts(files, artifactsDir) {
|
|
23
|
-
const warnings = [];
|
|
24
|
-
const hasPrd = files.some((f) => /prd/i.test(f));
|
|
25
|
-
if (!hasPrd) {
|
|
26
|
-
warnings.push("No PRD document found in planning artifacts");
|
|
27
|
-
}
|
|
28
|
-
const hasArchitecture = files.some((f) => /architect/i.test(f));
|
|
29
|
-
if (!hasArchitecture) {
|
|
30
|
-
warnings.push("No architecture document found in planning artifacts");
|
|
31
|
-
}
|
|
32
|
-
// Check readiness report for NO-GO
|
|
33
|
-
const readinessFile = files.find((f) => /readiness/i.test(f));
|
|
34
|
-
if (readinessFile) {
|
|
35
|
-
try {
|
|
36
|
-
const content = await readFile(join(artifactsDir, readinessFile), "utf-8");
|
|
37
|
-
if (/NO[-\s]?GO/i.test(content)) {
|
|
38
|
-
warnings.push("Readiness report indicates NO-GO status");
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
warnings.push("Could not read readiness report — NO-GO status unverified");
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return warnings;
|
|
46
|
-
}
|
|
@@ -37,6 +37,8 @@ export function extractProjectContext(artifacts) {
|
|
|
37
37
|
// Combine all content, keyed by likely role
|
|
38
38
|
let prdContent = "";
|
|
39
39
|
let archContent = "";
|
|
40
|
+
let uxContent = "";
|
|
41
|
+
let researchContent = "";
|
|
40
42
|
for (const [filename, content] of artifacts) {
|
|
41
43
|
if (/prd/i.test(filename))
|
|
42
44
|
prdContent += "\n" + content;
|
|
@@ -44,6 +46,10 @@ export function extractProjectContext(artifacts) {
|
|
|
44
46
|
archContent += "\n" + content;
|
|
45
47
|
if (/readiness/i.test(filename))
|
|
46
48
|
archContent += "\n" + content;
|
|
49
|
+
if (/ux/i.test(filename))
|
|
50
|
+
uxContent += "\n" + content;
|
|
51
|
+
if (/research|market|domain|brief/i.test(filename))
|
|
52
|
+
researchContent += "\n" + content;
|
|
47
53
|
}
|
|
48
54
|
const allContent = prdContent + "\n" + archContent;
|
|
49
55
|
const truncated = [];
|
|
@@ -98,6 +104,28 @@ export function extractProjectContext(artifacts) {
|
|
|
98
104
|
/^##\s+Quality Attributes/m,
|
|
99
105
|
],
|
|
100
106
|
},
|
|
107
|
+
{
|
|
108
|
+
field: "designGuidelines",
|
|
109
|
+
source: uxContent,
|
|
110
|
+
patterns: [
|
|
111
|
+
/^##\s+Design Principles/m,
|
|
112
|
+
/^##\s+Design System/m,
|
|
113
|
+
/^##\s+Core Experience/m,
|
|
114
|
+
/^##\s+User Flows/m,
|
|
115
|
+
/^##\s+Visual Foundation/m,
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
field: "researchInsights",
|
|
120
|
+
source: researchContent,
|
|
121
|
+
patterns: [
|
|
122
|
+
/^##\s+Key Findings/m,
|
|
123
|
+
/^##\s+Recommendations/m,
|
|
124
|
+
/^##\s+Market Analysis/m,
|
|
125
|
+
/^##\s+Domain Insights/m,
|
|
126
|
+
/^##\s+Summary/m,
|
|
127
|
+
],
|
|
128
|
+
},
|
|
101
129
|
];
|
|
102
130
|
const context = {
|
|
103
131
|
projectGoals: "",
|
|
@@ -107,6 +135,8 @@ export function extractProjectContext(artifacts) {
|
|
|
107
135
|
scopeBoundaries: "",
|
|
108
136
|
targetUsers: "",
|
|
109
137
|
nonFunctionalRequirements: "",
|
|
138
|
+
designGuidelines: "",
|
|
139
|
+
researchInsights: "",
|
|
110
140
|
};
|
|
111
141
|
for (const { field, source, patterns } of fields) {
|
|
112
142
|
const result = extractFromPatternsWithInfo(source, patterns);
|
|
@@ -140,6 +170,8 @@ export function generateProjectContextMd(context, projectName) {
|
|
|
140
170
|
{ heading: "Scope Boundaries", content: context.scopeBoundaries },
|
|
141
171
|
{ heading: "Target Users", content: context.targetUsers },
|
|
142
172
|
{ heading: "Non-Functional Requirements", content: context.nonFunctionalRequirements },
|
|
173
|
+
{ heading: "Design Guidelines", content: context.designGuidelines },
|
|
174
|
+
{ heading: "Research Insights", content: context.researchInsights },
|
|
143
175
|
];
|
|
144
176
|
for (const { heading, content } of sections) {
|
|
145
177
|
if (content) {
|
|
@@ -161,6 +193,8 @@ export function generatePrompt(projectName, context) {
|
|
|
161
193
|
context.targetUsers && `### Target Users\n${context.targetUsers}`,
|
|
162
194
|
context.nonFunctionalRequirements &&
|
|
163
195
|
`### Non-Functional Requirements\n${context.nonFunctionalRequirements}`,
|
|
196
|
+
context.designGuidelines && `### Design Guidelines\n${context.designGuidelines}`,
|
|
197
|
+
context.researchInsights && `### Research Insights\n${context.researchInsights}`,
|
|
164
198
|
]
|
|
165
199
|
.filter(Boolean)
|
|
166
200
|
.join("\n\n")
|