pi-bmad-flow 0.1.1 → 0.1.3

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.
@@ -82,6 +82,15 @@ export function registerBmadCommands(pi: ExtensionAPI, cwd: string): void {
82
82
  ctx.ui.notify(`Delivery: ${deliverySummary}`, "info");
83
83
  ctx.ui.notify(`Design: ${designSummary}`, "info");
84
84
  ctx.ui.notify(`TEA: ${teaSummary}`, "info");
85
+ if (state.activeSprintStatusPath) {
86
+ ctx.ui.notify(`Status file: ${state.activeSprintStatusPath}`, "info");
87
+ }
88
+ if (state.activeStoryLocation) {
89
+ ctx.ui.notify(`Story location: ${state.activeStoryLocation}`, "info");
90
+ }
91
+ if (state.inventory.epicFiles.length > 0) {
92
+ ctx.ui.notify(`Epic file: ${state.inventory.epicFiles[0]}`, "info");
93
+ }
85
94
  ctx.ui.notify(`Next: ${next.command} - ${next.summary}`, "info");
86
95
 
87
96
  refreshBmadUi(ctx, state, next);
@@ -96,9 +105,15 @@ export function registerBmadCommands(pi: ExtensionAPI, cwd: string): void {
96
105
  const next = decideNextAction(state);
97
106
  ctx.ui.notify(`Current phase: ${state.phase}`, "info");
98
107
  ctx.ui.notify(
99
- `Artifacts: PRD ${state.inventory.prdFiles.length}, UX ${state.inventory.uxFiles.length}, Architecture ${state.inventory.architectureFiles.length}, Stories ${state.inventory.storyFiles.length}`,
108
+ `Artifacts: PRD ${state.inventory.prdFiles.length}, UX ${state.inventory.uxFiles.length}, Architecture ${state.inventory.architectureFiles.length}, Epics ${state.inventory.epicFiles.length}, Stories ${state.inventory.storyFiles.length}`,
100
109
  "info",
101
110
  );
111
+ if (state.activeSprintStatusPath) {
112
+ ctx.ui.notify(`Status file: ${state.activeSprintStatusPath}`, "info");
113
+ }
114
+ if (state.activeStoryLocation) {
115
+ ctx.ui.notify(`Story location: ${state.activeStoryLocation}`, "info");
116
+ }
102
117
  ctx.ui.notify(`Recommended: ${next.command} (${next.reason})`, "info");
103
118
  refreshBmadUi(ctx, state, next);
104
119
  appendBmadState(pi, { event: "status", nextCommand: next.command });
@@ -16,14 +16,35 @@ function collectStructuredFiles(dirPath: string): string[] {
16
16
  return unique([...collectMarkdownFiles(dirPath), ...collectYamlFiles(dirPath)]);
17
17
  }
18
18
 
19
+ function detectStoryFiles(storyLocationFiles: string[]): string[] {
20
+ return storyLocationFiles.filter((file) => {
21
+ const lower = file.toLowerCase();
22
+ const base = lower.split("/").pop() ?? lower;
23
+ if (!base.endsWith(".md")) return false;
24
+ if (["readme.md", "index.md", "deferred-work.md"].includes(base)) return false;
25
+ if (/(checklist|traceability|report|spec-|specification|validation)/.test(base)) return false;
26
+ return /^(story-.*|\d+-\d+-.*|rb\d+-\d+-.*)\.md$/.test(base);
27
+ });
28
+ }
29
+
30
+ function detectEpicFiles(planningFiles: string[]): string[] {
31
+ const canonical = planningFiles.filter((file) => file.toLowerCase().endsWith("/epics.md"));
32
+ if (canonical.length > 0) return canonical;
33
+
34
+ return planningFiles.filter((file) => {
35
+ const lower = file.toLowerCase();
36
+ const base = lower.split("/").pop() ?? lower;
37
+ return /(^|.*-)epics\.md$/.test(base) || /^epic-.*\.md$/.test(base);
38
+ });
39
+ }
40
+
19
41
  function detectInventory(paths: BmadPaths): ArtifactInventory {
20
42
  const resolvedSprintStatusPath = resolveSprintStatusPath(paths.sprintStatusPath);
21
43
  const sprint = loadSprintStatus(resolvedSprintStatusPath);
22
44
  const storyLocationDir = resolveStoryLocation(resolvedSprintStatusPath, sprint);
23
45
  const planningFiles = collectMarkdownFiles(paths.planningArtifactsDir);
24
46
  const implementationFiles = collectMarkdownFiles(paths.implementationArtifactsDir);
25
- const storyLocationFiles =
26
- storyLocationDir !== paths.implementationArtifactsDir ? collectMarkdownFiles(storyLocationDir) : implementationFiles;
47
+ const storyLocationFiles = collectMarkdownFiles(storyLocationDir);
27
48
  const designFiles = collectStructuredFiles(paths.designArtifactsDir);
28
49
  const docsFiles = collectStructuredFiles(paths.docsDir);
29
50
  const teaFiles = unique([
@@ -65,8 +86,8 @@ function detectInventory(paths: BmadPaths): ArtifactInventory {
65
86
  /(ux|wds-|wireframe|storyboard|page-spec|page-specification|design-delivery|scenario)/,
66
87
  ),
67
88
  architectureFiles: matches(allContextFiles, /architecture|adr/),
68
- epicFiles: matches(allContextFiles, /(^|\/)epics?\/|epic-/),
69
- storyFiles: matches(allContextFiles, /(^|\/)story-.*\.md$/),
89
+ epicFiles: detectEpicFiles(planningFiles),
90
+ storyFiles: detectStoryFiles(storyLocationFiles),
70
91
  reviewFiles: matches(allContextFiles, /review|retrospective|readiness-report|validation-report/),
71
92
  projectContextFiles: matches(allContextFiles, /project-context/),
72
93
  wdsScenarioFiles,
@@ -154,6 +175,7 @@ export function detectBmadProjectState(cwd: string): BmadProjectState {
154
175
  const nextBacklogStory = sprint?.storyOrder.find((story) => sprint.developmentStatus[story] === "backlog");
155
176
  const nextReadyStory = sprint?.storyOrder.find((story) => sprint.developmentStatus[story] === "ready-for-dev");
156
177
  const activeStory = sprint?.storyOrder.find((story) => sprint.developmentStatus[story] === "in-progress");
178
+ const activeStoryLocation = sprint ? resolveStoryLocation(resolvedSprintStatusPath, sprint) : undefined;
157
179
 
158
180
  return {
159
181
  phase,
@@ -163,6 +185,8 @@ export function detectBmadProjectState(cwd: string): BmadProjectState {
163
185
  hasWdsArtifacts,
164
186
  hasTeaArtifacts,
165
187
  inventory,
188
+ activeSprintStatusPath: hasSprintStatus ? resolvedSprintStatusPath : undefined,
189
+ activeStoryLocation,
166
190
  nextBacklogStory,
167
191
  nextReadyStory,
168
192
  activeStory,
@@ -47,6 +47,14 @@ export function decideNextAction(state: BmadProjectState): NextAction {
47
47
  };
48
48
  }
49
49
 
50
+ if (!state.activeStoryLocation) {
51
+ return {
52
+ summary: "Sprint tracking exists, but story_location is missing or could not be resolved.",
53
+ command: "bmad-sprint-planning",
54
+ reason: "The active story directory should come from sprint-status.yaml before story execution starts.",
55
+ };
56
+ }
57
+
50
58
  if (state.activeStory) {
51
59
  return {
52
60
  summary: `Story ${state.activeStory} is already in progress.`,
@@ -3,8 +3,7 @@ import { dirname, resolve } from "node:path";
3
3
  import type { SprintStatus } from "./types.js";
4
4
 
5
5
  const STATUS_FILE_CANDIDATES = ["sprint-status.yaml", "story-status.yaml"];
6
-
7
- const STORY_KEY_PATTERN = /^\d+-\d+-/;
6
+ const STORY_KEY_PATTERN = /^(?:\d+-\d+-|rb\d+-\d+-)/;
8
7
 
9
8
  function parseStatusLine(line: string): { key: string; value: string } | undefined {
10
9
  const match = line.match(/^\s{2}([a-zA-Z0-9._-]+):\s*([a-zA-Z0-9._-]+)\s*$/);
@@ -57,6 +56,18 @@ export function loadSprintStatus(statusPath: string): SprintStatus | undefined {
57
56
  if (STORY_KEY_PATTERN.test(parsed.key)) storyOrder.push(parsed.key);
58
57
  }
59
58
 
59
+ if (storyOrder.length === 0) {
60
+ for (const key of Object.keys(developmentStatus)) {
61
+ if (STORY_KEY_PATTERN.test(key)) storyOrder.push(key);
62
+ }
63
+ }
64
+
65
+ if (storyOrder.length === 0) {
66
+ for (const key of Object.keys(developmentStatus)) {
67
+ if (!key.startsWith("epic-") && !key.endsWith("-retrospective")) storyOrder.push(key);
68
+ }
69
+ }
70
+
60
71
  return { developmentStatus, storyOrder, storyLocation };
61
72
  }
62
73
 
@@ -94,5 +105,13 @@ export function resolveStoryLocation(statusPath: string, sprint?: SprintStatus):
94
105
  if (!location) return fallbackDir;
95
106
  if (location.startsWith("{")) return fallbackDir;
96
107
  if (location.startsWith("/")) return location;
97
- return resolve(dirname(statusPath), location);
108
+
109
+ const relativeToStatusDir = resolve(dirname(statusPath), location);
110
+ if (existsSync(relativeToStatusDir)) return relativeToStatusDir;
111
+
112
+ const projectRoot = resolve(dirname(statusPath), "..", "..");
113
+ const relativeToProjectRoot = resolve(projectRoot, location);
114
+ if (existsSync(relativeToProjectRoot)) return relativeToProjectRoot;
115
+
116
+ return relativeToStatusDir;
98
117
  }
@@ -65,6 +65,8 @@ export interface BmadProjectState {
65
65
  hasWdsArtifacts: boolean;
66
66
  hasTeaArtifacts: boolean;
67
67
  inventory: ArtifactInventory;
68
+ activeSprintStatusPath?: string;
69
+ activeStoryLocation?: string;
68
70
  nextBacklogStory?: string;
69
71
  nextReadyStory?: string;
70
72
  activeStory?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bmad-flow",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Pi-native orchestration overlay for BMAD workflows",
6
6
  "keywords": [
@@ -1,5 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
- import { resolve, join, relative } from "node:path";
2
+ import { resolve, join, relative, dirname } from "node:path";
3
+
4
+ const STORY_KEY_PATTERN = /^(?:\d+-\d+-|rb\d+-\d+-)/;
3
5
 
4
6
  function parseArgs(argv) {
5
7
  const options = {
@@ -48,13 +50,10 @@ function readJson(path) {
48
50
  function resolveSprintStatusPath(path) {
49
51
  if (existsSync(path)) return path;
50
52
 
51
- const candidates = [
52
- path.replace(/sprint-status\.yaml$/, "story-status.yaml"),
53
- path.replace(/story-status\.yaml$/, "sprint-status.yaml"),
54
- ];
55
-
56
- for (const candidate of candidates) {
57
- if (candidate !== path && existsSync(candidate)) return candidate;
53
+ const directory = dirname(path);
54
+ for (const candidate of ["sprint-status.yaml", "story-status.yaml"]) {
55
+ const candidatePath = resolve(directory, candidate);
56
+ if (existsSync(candidatePath)) return candidatePath;
58
57
  }
59
58
 
60
59
  return path;
@@ -73,76 +72,93 @@ function parseSprintStatus(path) {
73
72
  storyLocation: undefined,
74
73
  };
75
74
 
76
- let section = "";
75
+ let inDevelopmentStatus = false;
77
76
  for (const rawLine of lines) {
78
- const line = rawLine.replace(/\t/g, " ");
79
- const trimmed = line.trim();
80
- if (!trimmed || trimmed.startsWith("#")) continue;
81
-
82
- if (/^story_order:\s*$/.test(trimmed)) {
83
- section = "story_order";
84
- continue;
85
- }
86
- if (/^development_status:\s*$/.test(trimmed)) {
87
- section = "development_status";
88
- continue;
89
- }
90
- if (/^story_location:\s*/.test(trimmed)) {
91
- state.storyLocation = trimmed.split(":").slice(1).join(":").trim().replace(/^['"]|['"]$/g, "");
92
- section = "";
93
- continue;
77
+ const line = rawLine.trimEnd();
78
+ if (!inDevelopmentStatus && !state.storyLocation) {
79
+ const storyLocationMatch = line.match(/^story_location:\s*["']?(.+?)["']?\s*$/);
80
+ if (storyLocationMatch) state.storyLocation = storyLocationMatch[1];
94
81
  }
95
82
 
96
- if (section === "story_order") {
97
- const match = trimmed.match(/^-\s+(.+)$/);
98
- if (match) state.storyOrder.push(match[1].trim().replace(/^['"]|['"]$/g, ""));
83
+ if (!inDevelopmentStatus) {
84
+ if (line === "development_status:") inDevelopmentStatus = true;
99
85
  continue;
100
86
  }
101
87
 
102
- if (section === "development_status") {
103
- const match = trimmed.match(/^([^:]+):\s*(.+)$/);
104
- if (match) {
105
- const key = match[1].trim().replace(/^['"]|['"]$/g, "");
106
- const value = match[2].trim().replace(/^['"]|['"]$/g, "");
107
- state.developmentStatus[key] = value;
108
- }
88
+ if (!line.startsWith(" ") && line.length > 0 && !line.startsWith("#")) break;
89
+ const match = line.match(/^\s{2}([a-zA-Z0-9._-]+):\s*([a-zA-Z0-9._-]+)\s*$/);
90
+ if (!match) continue;
91
+
92
+ const key = match[1];
93
+ const value = match[2];
94
+ state.developmentStatus[key] = value;
95
+ if (STORY_KEY_PATTERN.test(key)) state.storyOrder.push(key);
96
+ }
97
+
98
+ if (state.storyOrder.length === 0) {
99
+ for (const key of Object.keys(state.developmentStatus)) {
100
+ if (STORY_KEY_PATTERN.test(key)) state.storyOrder.push(key);
109
101
  }
110
102
  }
111
103
 
112
104
  return state;
113
105
  }
114
106
 
115
- function decideNextAction(inventory, sprintStatusExists, sprint) {
116
- if (inventory.prdFiles.length === 0) {
117
- return "bmad-create-prd";
118
- }
119
- if (inventory.uxFiles.length === 0) {
120
- return "bmad-create-ux-design";
121
- }
122
- if (inventory.architectureFiles.length === 0) {
123
- return "bmad-create-architecture";
124
- }
125
- if (inventory.epicFiles.length === 0) {
126
- return "bmad-create-epics-and-stories";
127
- }
128
- if (!sprintStatusExists) {
129
- return "bmad-sprint-planning";
130
- }
107
+ function resolveStoryLocation(statusPath, sprint) {
108
+ const fallbackDir = dirname(statusPath);
109
+ const location = sprint.storyLocation?.trim();
110
+ if (!location) return fallbackDir;
111
+ if (location.startsWith("{")) return fallbackDir;
112
+ if (location.startsWith("/")) return location;
113
+
114
+ const relativeToStatusDir = resolve(dirname(statusPath), location);
115
+ if (existsSync(relativeToStatusDir)) return relativeToStatusDir;
116
+
117
+ const projectRoot = resolve(dirname(statusPath), "..", "..");
118
+ const relativeToProjectRoot = resolve(projectRoot, location);
119
+ if (existsSync(relativeToProjectRoot)) return relativeToProjectRoot;
120
+
121
+ return relativeToStatusDir;
122
+ }
123
+
124
+ function detectStoryFiles(storyLocationFiles) {
125
+ return storyLocationFiles.filter((file) => {
126
+ const lower = file.toLowerCase();
127
+ const base = lower.split("/").pop() ?? lower;
128
+ if (!base.endsWith(".md")) return false;
129
+ if (["readme.md", "index.md", "deferred-work.md"].includes(base)) return false;
130
+ if (/(checklist|traceability|report|spec-|specification|validation)/.test(base)) return false;
131
+ return /^(story-.*|\d+-\d+-.*|rb\d+-\d+-.*)\.md$/.test(base);
132
+ });
133
+ }
134
+
135
+ function detectEpicFiles(planningFiles) {
136
+ const canonical = planningFiles.filter((file) => file.toLowerCase().endsWith("/epics.md"));
137
+ if (canonical.length > 0) return canonical;
138
+
139
+ return planningFiles.filter((file) => {
140
+ const lower = file.toLowerCase();
141
+ const base = lower.split("/").pop() ?? lower;
142
+ return /(^|.*-)epics\.md$/.test(base) || /^epic-.*\.md$/.test(base);
143
+ });
144
+ }
145
+
146
+ function decideNextAction(inventory, sprintStatusExists, sprint, activeStoryLocation) {
147
+ if (inventory.prdFiles.length === 0) return "bmad-create-prd";
148
+ if (inventory.uxFiles.length === 0) return "bmad-create-ux-design";
149
+ if (inventory.architectureFiles.length === 0) return "bmad-create-architecture";
150
+ if (inventory.epicFiles.length === 0) return "bmad-create-epics-and-stories";
151
+ if (!sprintStatusExists) return "bmad-sprint-planning";
152
+ if (!activeStoryLocation) return "bmad-sprint-planning";
131
153
 
132
154
  const activeStory = sprint.storyOrder.find((story) => sprint.developmentStatus[story] === "in-progress");
133
- if (activeStory) {
134
- return `bmad-dev-story ${activeStory}`;
135
- }
155
+ if (activeStory) return `bmad-dev-story ${activeStory}`;
136
156
 
137
157
  const nextReadyStory = sprint.storyOrder.find((story) => sprint.developmentStatus[story] === "ready-for-dev");
138
- if (nextReadyStory) {
139
- return `/bmad-start (${nextReadyStory})`;
140
- }
158
+ if (nextReadyStory) return `/bmad-start (${nextReadyStory})`;
141
159
 
142
160
  const nextBacklogStory = sprint.storyOrder.find((story) => sprint.developmentStatus[story] === "backlog");
143
- if (nextBacklogStory) {
144
- return `bmad-create-story (${nextBacklogStory})`;
145
- }
161
+ if (nextBacklogStory) return `bmad-create-story (${nextBacklogStory})`;
146
162
 
147
163
  return "bmad-correct-course";
148
164
  }
@@ -205,12 +221,18 @@ function main() {
205
221
  const testFiles = collectStructuredFiles(paths.testArtifactsDir);
206
222
  const allContextFiles = [...planningFiles, ...implementationFiles, ...docsFiles, ...designFiles, ...testFiles];
207
223
 
224
+ const resolvedSprintStatusPath = resolveSprintStatusPath(paths.sprintStatusPath);
225
+ const sprint = parseSprintStatus(resolvedSprintStatusPath);
226
+ const sprintStatusExists = existsSync(resolvedSprintStatusPath);
227
+ const activeStoryLocation = sprintStatusExists ? resolveStoryLocation(resolvedSprintStatusPath, sprint) : undefined;
228
+ const activeStoryFiles = activeStoryLocation ? collectStructuredFiles(activeStoryLocation) : [];
229
+
208
230
  const inventory = {
209
231
  prdFiles: matches(allContextFiles, /(^|\/)(prd|.*prd.*)\.md$/),
210
232
  uxFiles: matches(allContextFiles, /(ux|wds-|wireframe|storyboard|page-spec|page-specification|design-delivery|scenario)/),
211
233
  architectureFiles: matches(allContextFiles, /architecture|adr/),
212
- epicFiles: matches(allContextFiles, /(^|\/)epics?\/|epic-/),
213
- storyFiles: matches(allContextFiles, /(^|\/)story-.*\.md$/),
234
+ epicFiles: detectEpicFiles(planningFiles),
235
+ storyFiles: detectStoryFiles(activeStoryFiles),
214
236
  projectContextFiles: matches(allContextFiles, /project-context/),
215
237
  wdsDeliveryFiles: matches(allContextFiles, /(design-deliver(y|ies)|\/deliveries\/|dd-\d+)/),
216
238
  wdsPageSpecFiles: matches(allContextFiles, /\/c-ux-scenarios\/.*(specifications\.md|page-spec|page-specification|\/frontend\/.*\.md)/),
@@ -219,10 +241,7 @@ function main() {
219
241
  teaNfrFiles: matches(allContextFiles, /nfr-assessment|nfr-report/),
220
242
  };
221
243
 
222
- const resolvedSprintStatusPath = resolveSprintStatusPath(paths.sprintStatusPath);
223
- const sprint = parseSprintStatus(resolvedSprintStatusPath);
224
- const sprintStatusExists = existsSync(resolvedSprintStatusPath);
225
- const nextAction = decideNextAction(inventory, sprintStatusExists, sprint);
244
+ const nextAction = decideNextAction(inventory, sprintStatusExists, sprint, activeStoryLocation);
226
245
 
227
246
  console.log("pi-bmad-flow project audit");
228
247
  console.log(`project: ${projectDir}`);
@@ -259,6 +278,7 @@ function main() {
259
278
  console.log("");
260
279
  console.log("sprint:");
261
280
  console.log(`sprint status: ${sprintStatusExists ? "present" : "missing"}`);
281
+ console.log(`status file: ${resolvedSprintStatusPath}`);
262
282
  console.log(`story order entries: ${sprint.storyOrder.length}`);
263
283
  console.log(`ready stories: ${sprint.storyOrder.filter((story) => sprint.developmentStatus[story] === "ready-for-dev").length}`);
264
284
  console.log(`active stories: ${sprint.storyOrder.filter((story) => sprint.developmentStatus[story] === "in-progress").length}`);
@@ -266,6 +286,9 @@ function main() {
266
286
  if (sprint.storyLocation) {
267
287
  console.log(`story location: ${sprint.storyLocation}`);
268
288
  }
289
+ if (activeStoryLocation) {
290
+ console.log(`resolved story location: ${activeStoryLocation}`);
291
+ }
269
292
 
270
293
  console.log("");
271
294
  console.log(`next action: ${nextAction}`);