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.
Files changed (61) hide show
  1. package/README.md +105 -38
  2. package/dist/cli.js +19 -0
  3. package/dist/commands/doctor.d.ts +0 -11
  4. package/dist/commands/doctor.js +22 -55
  5. package/dist/commands/implement.d.ts +6 -0
  6. package/dist/commands/implement.js +82 -0
  7. package/dist/commands/init.js +4 -2
  8. package/dist/commands/reset.d.ts +7 -0
  9. package/dist/commands/reset.js +81 -0
  10. package/dist/commands/status.js +100 -11
  11. package/dist/commands/watch.d.ts +6 -0
  12. package/dist/commands/watch.js +19 -0
  13. package/dist/installer.d.ts +0 -6
  14. package/dist/installer.js +0 -10
  15. package/dist/platform/claude-code.js +0 -1
  16. package/dist/reset.d.ts +18 -0
  17. package/dist/reset.js +181 -0
  18. package/dist/transition/artifact-scan.d.ts +27 -0
  19. package/dist/transition/artifact-scan.js +91 -0
  20. package/dist/transition/artifacts.d.ts +0 -1
  21. package/dist/transition/artifacts.js +0 -26
  22. package/dist/transition/context.js +34 -0
  23. package/dist/transition/fix-plan.d.ts +8 -2
  24. package/dist/transition/fix-plan.js +33 -7
  25. package/dist/transition/index.d.ts +1 -1
  26. package/dist/transition/index.js +1 -1
  27. package/dist/transition/orchestration.d.ts +2 -2
  28. package/dist/transition/orchestration.js +120 -41
  29. package/dist/transition/preflight.d.ts +6 -0
  30. package/dist/transition/preflight.js +154 -0
  31. package/dist/transition/specs-index.d.ts +1 -1
  32. package/dist/transition/specs-index.js +24 -1
  33. package/dist/transition/types.d.ts +23 -1
  34. package/dist/utils/dryrun.d.ts +1 -1
  35. package/dist/utils/dryrun.js +22 -0
  36. package/dist/utils/state.d.ts +0 -2
  37. package/dist/utils/validate.js +3 -2
  38. package/dist/watch/dashboard.d.ts +4 -0
  39. package/dist/watch/dashboard.js +60 -0
  40. package/dist/watch/file-watcher.d.ts +9 -0
  41. package/dist/watch/file-watcher.js +27 -0
  42. package/dist/watch/renderer.d.ts +16 -0
  43. package/dist/watch/renderer.js +241 -0
  44. package/dist/watch/state-reader.d.ts +9 -0
  45. package/dist/watch/state-reader.js +190 -0
  46. package/dist/watch/types.d.ts +55 -0
  47. package/dist/watch/types.js +1 -0
  48. package/package.json +9 -4
  49. package/ralph/lib/circuit_breaker.sh +86 -59
  50. package/ralph/lib/enable_core.sh +3 -6
  51. package/ralph/lib/response_analyzer.sh +5 -29
  52. package/ralph/lib/task_sources.sh +45 -11
  53. package/ralph/lib/wizard_utils.sh +9 -0
  54. package/ralph/ralph_import.sh +7 -2
  55. package/ralph/ralph_loop.sh +44 -34
  56. package/ralph/ralph_monitor.sh +4 -0
  57. package/slash-commands/bmalph-doctor.md +16 -0
  58. package/slash-commands/bmalph-implement.md +18 -141
  59. package/slash-commands/bmalph-status.md +15 -0
  60. package/slash-commands/bmalph-upgrade.md +15 -0
  61. package/slash-commands/bmalph-watch.md +20 -0
@@ -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 phase = state?.currentPhase ?? 1;
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 (phase === 4) {
24
+ if (storedPhase === 4) {
25
25
  ralphStatus = await readRalphStatus(projectDir);
26
26
  }
27
- // Determine next action
28
- const nextAction = getNextAction(phase, status, ralphStatus);
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
- console.log(` ${chalk.cyan("Phase:")} ${phase} - ${phaseName}`);
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 (nextAction) {
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 /bmalph-implement when ready for implementation";
193
+ return "Run: bmalph implement";
108
194
  case 4:
109
195
  if (!ralphStatus || ralphStatus.status === "not_started") {
110
- return "Start Ralph loop with: bash .ralph/ralph_loop.sh";
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,6 @@
1
+ interface WatchCommandOptions {
2
+ interval?: string;
3
+ projectDir: string;
4
+ }
5
+ export declare function watchCommand(options: WatchCommandOptions): Promise<void>;
6
+ export {};
@@ -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
+ }
@@ -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
 
@@ -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,2 +1 @@
1
1
  export declare function findArtifactsDir(projectDir: string): Promise<string | null>;
2
- export declare function validateArtifacts(files: string[], artifactsDir: string): Promise<string[]>;
@@ -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")