@atlashub/smartstack-cli 4.25.0 → 4.27.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 (30) hide show
  1. package/dist/index.js +765 -517
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +33 -11
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/agents/ba-writer.md +46 -42
  7. package/templates/project/appsettings.json.template +4 -6
  8. package/templates/skills/apex/SKILL.md +1 -0
  9. package/templates/skills/apex/references/challenge-questions.md +17 -0
  10. package/templates/skills/apex/references/post-checks.md +48 -0
  11. package/templates/skills/apex/steps/step-03-execute.md +63 -2
  12. package/templates/skills/ba-generate-html/references/data-build.md +22 -13
  13. package/templates/skills/ba-generate-html/references/data-mapping.md +33 -24
  14. package/templates/skills/ba-generate-html/steps/step-01-collect.md +15 -6
  15. package/templates/skills/ba-generate-html/steps/step-02-build-data.md +37 -22
  16. package/templates/skills/business-analyse/steps/step-00-init.md +22 -14
  17. package/templates/skills/business-analyse/steps/step-04-consolidate.md +3 -0
  18. package/templates/skills/derive-prd/steps/step-01-transform.md +6 -2
  19. package/templates/skills/derive-prd/steps/step-02-export.md +12 -0
  20. package/templates/skills/ralph-loop/references/category-completeness.md +3 -3
  21. package/templates/skills/ralph-loop/references/compact-loop.md +81 -14
  22. package/templates/skills/ralph-loop/references/init-resume-recovery.md +1 -1
  23. package/templates/skills/ralph-loop/references/module-transition.md +30 -5
  24. package/templates/skills/ralph-loop/references/multi-module-queue.md +4 -4
  25. package/templates/skills/ralph-loop/references/section-splitting.md +6 -6
  26. package/templates/skills/ralph-loop/steps/step-01-task.md +14 -4
  27. package/templates/skills/ralph-loop/steps/step-02-execute.md +14 -6
  28. package/templates/skills/ralph-loop/steps/step-03-commit.md +15 -4
  29. package/templates/skills/ralph-loop/steps/step-04-check.md +19 -5
  30. package/templates/skills/ralph-loop/steps/step-05-report.md +35 -3
@@ -89,24 +89,29 @@ modules: master.modules.map(m => ({
89
89
 
90
90
  **moduleSpecs{} — ONE entry per module (CRITICAL):**
91
91
 
92
- For EACH module, read the module index.json and map:
92
+ > **FLAT-FILE ARCHITECTURE:** Module data comes from separate JSON files collected in step-01,
93
+ > NOT from index.json fields. Use `collected_data.modules[moduleCode].entities`, `.usecases`, etc.
94
+
95
+ For EACH module, use the flat file data from `collected_data.modules[moduleCode]`:
93
96
 
94
97
  ```javascript
98
+ const mod = collected_data.modules[moduleCode];
99
+
95
100
  moduleSpecs[moduleCode] = {
96
- useCases: (moduleFeature.specification?.useCases || []).map(uc => ({
101
+ useCases: (mod.usecases?.useCases || []).map(uc => ({
97
102
  name: uc.name,
98
103
  actor: uc.primaryActor,
99
104
  steps: (uc.mainScenario || []).join("\n"),
100
105
  alternative: (uc.alternativeScenarios || []).map(a => a.name + ": " + (a.steps || []).join(", ")).join("\n")
101
106
  })),
102
- businessRules: (moduleFeature.analysis?.businessRules || []).map(br => ({
107
+ businessRules: (mod.rules?.rules || []).map(br => ({
103
108
  name: br.name,
104
109
  category: br.category,
105
110
  statement: br.statement,
106
111
  example: (br.examples || []).map(e => e.input + " → " + e.expected).join("; ")
107
112
  })),
108
113
  // ENTITY SAFETY NET: map fields[] → attributes[] if agent deviated
109
- entities: (moduleFeature.analysis?.entities || []).map(ent => ({
114
+ entities: (mod.entities?.entities || []).map(ent => ({
110
115
  name: ent.name,
111
116
  description: ent.description || "",
112
117
  attributes: (ent.attributes || []).length > 0
@@ -115,8 +120,8 @@ moduleSpecs[moduleCode] = {
115
120
  relationships: (ent.relationships || []).map(r =>
116
121
  typeof r === 'string' ? r : r.target + " (" + r.type + ") - " + (r.description || ""))
117
122
  })),
118
- permissions: buildPermissionKeys(moduleFeature),
119
- apiEndpoints: moduleFeature.specification?.apiEndpoints || []
123
+ permissions: buildPermissionKeys(mod.permissions),
124
+ apiEndpoints: mod.usecases?.apiEndpoints || []
120
125
  }
121
126
  ```
122
127
 
@@ -158,24 +163,34 @@ handoff: {
158
163
  > **FIELD RENAME WARNING:** Module JSON uses `mockupFormat`/`mockup`. HTML reads `format`/`content`. You MUST rename.
159
164
 
160
165
  **wireframes{} — per moduleCode:**
166
+
167
+ > **FLAT-FILE:** Wireframes come from `screens.json` in each module directory (collected in step-01),
168
+ > NOT from `moduleFeature.specification.uiWireframes`.
169
+
161
170
  ```javascript
171
+ const mod = collected_data.modules[moduleCode];
172
+ const screens = mod.screens?.screens || [];
173
+
174
+ // Extract wireframes from screens that have mockup/wireframe data
162
175
  wireframes: {
163
- [moduleCode]: (moduleFeature.specification.uiWireframes || moduleFeature.specification.wireframes || []).map(wf => ({
164
- screen: wf.screen || wf.name || wf.title || wf.id || "",
165
- section: wf.section || "",
166
- format: wf.mockupFormat || "ascii", // RENAME: mockupFormat format
167
- content: wf.mockup || wf.ascii || wf.content || "", // RENAME: mockup → content
168
- svgContent: null,
169
- description: wf.description || "",
170
- elements: wf.elements || [],
171
- actions: wf.actions || [],
172
- componentMapping: Array.isArray(wf.componentMapping) ? wf.componentMapping
173
- : typeof wf.componentMapping === 'object' && wf.componentMapping !== null
174
- ? Object.entries(wf.componentMapping).map(([k, v]) => ({ wireframeElement: k, reactComponent: v }))
175
- : [],
176
- layout: typeof wf.layout === 'object' ? wf.layout : null,
177
- permissionsRequired: wf.permissionsRequired || []
178
- }))
176
+ [moduleCode]: screens
177
+ .filter(s => s.wireframe || s.mockup || s.mockupFormat)
178
+ .map(wf => ({
179
+ screen: wf.screen || wf.name || wf.title || wf.id || "",
180
+ section: wf.section || "",
181
+ format: wf.mockupFormat || "ascii", // RENAME: mockupFormat → format
182
+ content: wf.mockup || wf.ascii || wf.content || "", // RENAME: mockup → content
183
+ svgContent: null,
184
+ description: wf.description || "",
185
+ elements: wf.elements || [],
186
+ actions: wf.actions || [],
187
+ componentMapping: Array.isArray(wf.componentMapping) ? wf.componentMapping
188
+ : typeof wf.componentMapping === 'object' && wf.componentMapping !== null
189
+ ? Object.entries(wf.componentMapping).map(([k, v]) => ({ wireframeElement: k, reactComponent: v }))
190
+ : [],
191
+ layout: typeof wf.layout === 'object' ? wf.layout : null,
192
+ permissionsRequired: wf.permissionsRequired || []
193
+ }))
179
194
  }
180
195
  ```
181
196
 
@@ -357,14 +357,12 @@ IF workflow_mode = "project":
357
357
  })
358
358
 
359
359
  Output path: docs/business-analyse/v{version}/index.json
360
- Thematic files (empty, initialized): docs/business-analyse/v{version}/*.json
361
- - entities.json
362
- - rules.json
363
- - usecases.json
364
- - permissions.json
365
- - screens.json
360
+ Thematic files — EXACTLY these 3, NO OTHERS:
361
+ - cadrage.json
366
362
  - validation.json
367
- - handoff.json
363
+ - consolidation.json
364
+ FORBIDDEN: entities.json, rules.json, usecases.json, permissions.json, screens.json, handoff.json
365
+ Creating any FORBIDDEN file at project level is a BLOCKING ERROR.
368
366
 
369
367
  Store:
370
368
  project_id: string // PROJ-NNN
@@ -395,19 +393,29 @@ ELSE:
395
393
  })
396
394
 
397
395
  Output path: docs/{app}/business-analyse/v{version}/index.json
398
- Thematic files (empty, initialized): docs/{app}/business-analyse/v{version}/*.json
399
- - entities.json
400
- - rules.json
401
- - usecases.json
402
- - permissions.json
403
- - screens.json
396
+ Thematic files — EXACTLY these 3, NO OTHERS:
397
+ - cadrage.json
404
398
  - validation.json
405
- - handoff.json
399
+ - consolidation.json
400
+ FORBIDDEN: entities.json, rules.json, usecases.json, permissions.json, screens.json, handoff.json
401
+ Creating any FORBIDDEN file at application level is a BLOCKING ERROR.
406
402
  ```
407
403
 
408
404
  > **Note:** In project mode, per-application index.json files are created later in step-01-cadrage.
409
405
  > In single-app mode, step-02 (structure) determines if it's single or multi-module.
410
406
 
407
+ ### POST-CREATE VERIFICATION (MANDATORY)
408
+
409
+ After creating thematic files, verify no forbidden files were created:
410
+
411
+ ```
412
+ List all *.json files in {docs_dir}/
413
+ Expected (project/application): [index.json, cadrage.json, validation.json, consolidation.json]
414
+ If any file NOT in expected list is found → DELETE it immediately + log WARNING
415
+ ```
416
+
417
+ This guard catches any accidental creation of module-only files at the wrong scope.
418
+
411
419
  ## Step 10: Update Config
412
420
 
413
421
  Update `.business-analyse/config.json` with new feature information.
@@ -423,6 +423,9 @@ ba-writer.enrichSection({
423
423
  // Update status
424
424
  ba-writer.updateStatus({feature_id}, "consolidated");
425
425
 
426
+ // Clean up any forbidden files that may have been created at app level
427
+ ba-writer.cleanupAppLevelFiles({ featureId: {feature_id} });
428
+
426
429
  // Save workflow state for resume support
427
430
  ba-writer.enrichSection({
428
431
  featureId: {feature_id},
@@ -201,8 +201,12 @@ FOR i = 0 to modules.length-1:
201
201
  - totalFiles: count from filesToCreate
202
202
  - totalTasks: estimated from complexity
203
203
  - handedOffAt: ISO timestamp
204
- 3. Write via ba-writer.enrichModuleHandoff({ moduleFeatureId, handoffData })
205
- 4. Display: "handoff {i+1}/{N}: {moduleCode} ({fileCount} files)"
204
+ 3. Propagate featureDescription from app-level index.json:
205
+ ```javascript
206
+ handoffPayload.featureDescription = appIndex.metadata.featureDescription || null;
207
+ ```
208
+ 4. Write via ba-writer.enrichModuleHandoff({ moduleFeatureId, handoffData })
209
+ 5. Display: "handoff {i+1}/{N}: {moduleCode} ({fileCount} files)"
206
210
  ```
207
211
 
208
212
  ### 10. POST-CHECK Per Module (BLOCKING)
@@ -29,6 +29,18 @@ For each module, generate PRD files via CLI, verify integrity, create progress t
29
29
 
30
30
  For EACH module:
31
31
 
32
+ **Option A — Flat-file BA (recommended):**
33
+
34
+ ```bash
35
+ # Single module:
36
+ ss derive-prd --ba-dir {moduleBaDir} --output .ralph/prd-{moduleCode}.json
37
+
38
+ # All modules from app-level index.json:
39
+ ss derive-prd --ba-app {appIndexPath}
40
+ ```
41
+
42
+ **Option B — Legacy feature.json:**
43
+
32
44
  ```bash
33
45
  ss derive-prd --feature {moduleFeaturePath} --output .ralph/prd-{moduleCode}.json
34
46
  ```
@@ -130,7 +130,7 @@ if (guardrailsNeeded.length > 0) {
130
130
  }
131
131
  }
132
132
 
133
- writeJSON('.ralph/prd.json', prd);
133
+ writeJSON(currentPrdPath, prd);
134
134
  }
135
135
  ```
136
136
 
@@ -142,7 +142,7 @@ const { missing, guardrailsNeeded } = checkCategoryCompleteness(prd);
142
142
 
143
143
  if (guardrailsNeeded.length > 0) {
144
144
  prd.tasks.push(...guardrailsNeeded);
145
- writeJSON('.ralph/prd.json', prd);
145
+ writeJSON(currentPrdPath, prd);
146
146
 
147
147
  const newPending = prd.tasks.filter(t => t.status === 'pending').length;
148
148
  console.log(`PRD updated: +${guardrailsNeeded.length} guardrails, ${newPending} pending tasks`);
@@ -186,7 +186,7 @@ for (const [cat, check] of Object.entries(artifactChecks)) {
186
186
  t.error = 'Artifacts missing — re-execute';
187
187
  t.completed_at = null;
188
188
  });
189
- writeJSON('.ralph/prd.json', prd);
189
+ writeJSON(currentPrdPath, prd);
190
190
  }
191
191
  }
192
192
  ```
@@ -16,6 +16,16 @@
16
16
 
17
17
  ## Loop Entry
18
18
 
19
+ ```javascript
20
+ // PRD PATH RESOLUTION (MANDATORY - runs at loop entry)
21
+ const queuePath = '.ralph/modules-queue.json';
22
+ let currentPrdPath = '.ralph/prd.json';
23
+ if (fileExists(queuePath)) {
24
+ const queue = readJSON(queuePath);
25
+ currentPrdPath = queue.queue[queue.currentIndex].prdFile;
26
+ }
27
+ ```
28
+
19
29
  Display compact progress:
20
30
  ```
21
31
  [{iteration}/{max}] {completed}/{total} done | Finding eligible tasks...
@@ -26,7 +36,7 @@ Display compact progress:
26
36
  ## A0. Section-Split Mode (checked BEFORE standard batching)
27
37
 
28
38
  ```javascript
29
- const prd = readJSON('.ralph/prd.json');
39
+ const prd = readJSON(currentPrdPath);
30
40
 
31
41
  if (prd._sectionSplit?.enabled) {
32
42
  // SECTION-SPLIT: Delegate per-phase instead of per-category batch
@@ -96,7 +106,7 @@ Apex sees a focused PRD for this phase only. After apex returns, continue:
96
106
  }
97
107
 
98
108
  prd._sectionSplit.currentPhase = nextPhase.phase;
99
- writeJSON('.ralph/prd.json', prd);
109
+ writeJSON(currentPrdPath, prd);
100
110
 
101
111
  // → Skip to section C (commit PRD state), then D (loop back)
102
112
  // → SKIP sections A and B. Jump directly to section C (COMMIT_PRD) below.
@@ -130,7 +140,7 @@ if (isDeadEnd) {
130
140
  // Then fall through to `eligible.length === 0` below which returns to step-04.
131
141
  console.log(`DEAD-END: ${prd.tasks.filter(t=>t.status==='completed').length} done, ${failedBeforeRetry.length} failed (retries exhausted), ${prd.tasks.filter(t=>t.status==='blocked').length} blocked`);
132
142
  prd.status = 'failed';
133
- writeJSON('.ralph/prd.json', prd);
143
+ writeJSON(currentPrdPath, prd);
134
144
  // DO NOT reset retries below — skip to eligible search (will find 0 tasks → CHECK_COMPLETION)
135
145
  }
136
146
 
@@ -172,7 +182,7 @@ if (!isDeadEnd) {
172
182
  if (depsBlocked) { task.status = 'blocked'; task.error = 'Blocked by failed dependency'; }
173
183
  }
174
184
 
175
- writeJSON('.ralph/prd.json', prd);
185
+ writeJSON(currentPrdPath, prd);
176
186
  } // end if (!isDeadEnd)
177
187
 
178
188
  // Find ALL eligible tasks (dependencies met)
@@ -213,12 +223,12 @@ for (const task of batch) {
213
223
  task.status = 'in_progress';
214
224
  task.started_at = new Date().toISOString();
215
225
  }
216
- writeJSON('.ralph/prd.json', prd);
226
+ writeJSON(currentPrdPath, prd);
217
227
  ```
218
228
 
219
229
  ### B2. Invoke /apex
220
230
 
221
- **INVOKE `/apex -d .ralph/prd.json`**
231
+ **INVOKE `/apex -d {currentPrdPath}`**
222
232
 
223
233
  Apex handles everything for the current module:
224
234
  - Reads PRD, extracts context (app_name, module_code, entities, sections)
@@ -230,11 +240,37 @@ Apex handles everything for the current module:
230
240
 
231
241
  > **FLAGS:** `-d` implies `-a` (auto, no user confirmation) and `-e` (economy, no nested teams).
232
242
 
233
- ### B3. Verify Post-Apex Results
243
+ ### B2b. MANDATORY CHECKPOINT: Verify prd.json tasks updated
234
244
 
235
245
  ```javascript
236
246
  // Re-read PRD after apex execution
237
- const updatedPrd = readJSON('.ralph/prd.json');
247
+ const updatedPrd = readJSON(currentPrdPath);
248
+ const batchIds = batch.map(t => t.id);
249
+
250
+ // BLOCKING CHECK: Verify apex actually updated task statuses
251
+ const stillPending = updatedPrd.tasks.filter(t => batchIds.includes(t.id) && t.status === 'pending').length;
252
+ if (stillPending === batchIds.length) {
253
+ console.error(`BLOCKING ERROR: apex did not update any task statuses — all ${stillPending} tasks still pending`);
254
+ console.error('Possible causes: apex crashed, PRD path mismatch, or apex did not receive the correct PRD');
255
+ // Mark tasks as failed so retry logic can handle them
256
+ for (const task of updatedPrd.tasks) {
257
+ if (batchIds.includes(task.id) && task.status === 'pending') {
258
+ task.status = 'failed';
259
+ task.error = 'apex did not update task status';
260
+ }
261
+ }
262
+ writeJSON(currentPrdPath, updatedPrd);
263
+ }
264
+ ```
265
+
266
+ > **NEVER write progress.txt manually. ALWAYS update prd.json tasks FIRST,
267
+ > then derive progress.txt from prd.json state.**
268
+
269
+ ### B3. Verify Post-Apex Results
270
+
271
+ ```javascript
272
+ // Re-read PRD after apex execution (use currentPrdPath)
273
+ const updatedPrd = readJSON(currentPrdPath);
238
274
  const batchIds = batch.map(t => t.id);
239
275
 
240
276
  const completed = updatedPrd.tasks.filter(t => batchIds.includes(t.id) && t.status === 'completed').length;
@@ -260,7 +296,7 @@ console.log(`Apex completed: ${completed}/${batchIds.length} tasks`);
260
296
  ### C1. Update Progress File (MANDATORY)
261
297
 
262
298
  ```javascript
263
- const prdCheck = readJSON('.ralph/prd.json');
299
+ const prdCheck = readJSON(currentPrdPath);
264
300
  const totalCompleted = prdCheck.tasks.filter(t => t.status === 'completed').length;
265
301
  const total = prdCheck.tasks.length;
266
302
 
@@ -270,18 +306,49 @@ appendFile('.ralph/progress.txt',
270
306
  );
271
307
  ```
272
308
 
273
- ### C2. Increment Iteration
309
+ ### C2. Skipped Task Audit
310
+
311
+ After apex returns, check for newly skipped tasks:
312
+
313
+ ```javascript
314
+ const newlySkipped = prdCheck.tasks.filter(t =>
315
+ batchIds.includes(t.id) && t.status === 'skipped'
316
+ );
317
+ if (newlySkipped.length > 0) {
318
+ console.warn(`⚠ ${newlySkipped.length} tasks were SKIPPED by apex:`);
319
+ newlySkipped.forEach(t => console.warn(` - ${t.id}: ${t.description}`));
320
+
321
+ // If ALL tasks in a category were skipped → BLOCKING, reset for retry
322
+ const skippedCategories = [...new Set(newlySkipped.map(t => t.category))];
323
+ for (const cat of skippedCategories) {
324
+ const allInCat = prdCheck.tasks.filter(t => t.category === cat);
325
+ const allSkipped = allInCat.every(t => t.status === 'skipped');
326
+ if (allSkipped) {
327
+ console.error(`BLOCKING: ALL tasks in category "${cat}" were skipped — investigate root cause`);
328
+ // Reset to pending for retry
329
+ allInCat.forEach(t => {
330
+ t.status = 'pending';
331
+ t._retryCount = (t._retryCount || 0) + 1;
332
+ t.error = `All tasks in category "${cat}" skipped — auto-retry`;
333
+ });
334
+ }
335
+ }
336
+ writeJSON(currentPrdPath, prdCheck);
337
+ }
338
+ ```
339
+
340
+ ### C3. Increment Iteration
274
341
 
275
342
  ```javascript
276
343
  prdCheck.config.current_iteration++;
277
344
  prdCheck.updated_at = new Date().toISOString();
278
- writeJSON('.ralph/prd.json', prdCheck);
345
+ writeJSON(currentPrdPath, prdCheck);
279
346
  ```
280
347
 
281
- ### C3. Git Commit (PRD state only — apex already committed code)
348
+ ### C4. Git Commit (PRD state only — apex already committed code)
282
349
 
283
350
  ```bash
284
- git add .ralph/prd.json .ralph/progress.txt
351
+ git add {currentPrdPath} .ralph/progress.txt
285
352
  [ -f .ralph/modules-queue.json ] && git add .ralph/modules-queue.json
286
353
 
287
354
  git commit -m "$(cat <<'EOF'
@@ -295,7 +362,7 @@ EOF
295
362
  )"
296
363
  ```
297
364
 
298
- ### C4. PRD Sync Verification (HARD CHECK)
365
+ ### C5. PRD Sync Verification (HARD CHECK)
299
366
 
300
367
  > **Note:** `prdCheck` (read in C1) is the authoritative post-apex snapshot.
301
368
  > `completed` (from B3) was computed on a DIFFERENT read — do NOT mix the two.
@@ -16,7 +16,7 @@ if (!fileExists('.ralph/prd.json')) {
16
16
  STOP;
17
17
  }
18
18
 
19
- const prd = readJSON('.ralph/prd.json');
19
+ const prd = readJSON(currentPrdPath);
20
20
 
21
21
  // Detect format
22
22
  let resumeValid = false;
@@ -54,15 +54,17 @@ if (missing.length === 0) {
54
54
  timestamp: new Date().toISOString()
55
55
  });
56
56
 
57
- // Mark current PRD as complete
57
+ // Mark current PRD as complete (write to module-specific file, NOT .ralph/prd.json)
58
58
  prd.status = 'completed';
59
- writeJSON('.ralph/prd.json', prd);
59
+ writeJSON(currentModule.prdFile, prd);
60
60
 
61
- // Load next module's PRD
61
+ // Prepare next module's PRD (work directly with prd-{moduleCode}.json)
62
62
  const nextPrd = readJSON(queue.modules[nextIndex].prdFile);
63
63
  nextPrd.config.current_iteration = 1;
64
64
  nextPrd.config.max_iterations = prd.config.max_iterations;
65
- writeJSON('.ralph/prd.json', nextPrd);
65
+ writeJSON(queue.modules[nextIndex].prdFile, nextPrd);
66
+ // NOTE: Ralph works directly with prd-{moduleCode}.json via currentPrdPath resolution.
67
+ // No need to copy to .ralph/prd.json — this avoids overwriting between modules.
66
68
 
67
69
  console.log(`MODULE COMPLETE: ${currentModule.code} → NEXT: ${queue.modules[nextIndex].code}`);
68
70
 
@@ -76,6 +78,29 @@ if (missing.length === 0) {
76
78
  }
77
79
  ```
78
80
 
81
+ ### MANDATORY POST-CHECK: modules-queue.json integrity
82
+
83
+ ```javascript
84
+ // VERIFY: modules-queue.json updated correctly after advancement
85
+ const queueCheck = readJSON(queuePath);
86
+ const actualCompleted = queueCheck.modules.filter(m => m.status === 'completed').length;
87
+ if (queueCheck.completedModules !== actualCompleted) {
88
+ console.error(`QUEUE SYNC ERROR: completedModules=${queueCheck.completedModules} but actual=${actualCompleted}`);
89
+ queueCheck.completedModules = actualCompleted;
90
+ writeJSON(queuePath, queueCheck);
91
+ console.log('FIXED: completedModules synced to actual count');
92
+ }
93
+
94
+ if (nextIndex < queueCheck.totalModules) {
95
+ if (queueCheck.currentIndex !== nextIndex) {
96
+ console.error(`QUEUE SYNC ERROR: currentIndex=${queueCheck.currentIndex} but expected=${nextIndex}`);
97
+ queueCheck.currentIndex = nextIndex;
98
+ writeJSON(queuePath, queueCheck);
99
+ console.log('FIXED: currentIndex synced');
100
+ }
101
+ }
102
+ ```
103
+
79
104
  ---
80
105
 
81
106
  ## Check: All Modules Complete
@@ -121,7 +146,7 @@ if (fileExists(moduleChangePath)) {
121
146
  ├─ step-04: Check completion
122
147
  │ └─ If all categories complete:
123
148
  │ ├─ Write module-changed.json
124
- │ ├─ Write next PRD to .ralph/prd.json
149
+ │ ├─ Write next PRD to prd-{moduleCode}.json (via currentPrdPath)
125
150
  │ └─ Return to step-01 ✓
126
151
 
127
152
  └─ [Phase B: Module 2]
@@ -123,7 +123,7 @@ if (fileExists(queuePath)) {
123
123
  const queue = readJSON(queuePath);
124
124
  const currentModule = queue.modules[queue.currentIndex];
125
125
 
126
- const currentPrd = readJSON('.ralph/prd.json');
126
+ const currentPrd = readJSON(currentPrdPath);
127
127
  if (currentPrd.metadata?.moduleCode !== currentModule.code) {
128
128
  // Module has changed — load new PRD
129
129
  const modulePrd = readJSON(currentModule.prdFile);
@@ -143,16 +143,16 @@ if (fileExists(queuePath)) {
143
143
  modulePrd.metadata.project_path = modulePrd.metadata.project_path || process.cwd();
144
144
  modulePrd.metadata.mcp_servers = modulePrd.metadata.mcp_servers || { smartstack: true, context7: true };
145
145
  modulePrd.history = modulePrd.history || [];
146
- writeJSON('.ralph/prd.json', modulePrd);
146
+ writeJSON(currentModule.prdFile, modulePrd);
147
147
  }
148
148
  // LEGACY: FORMAT A
149
149
  else if (modulePrd.project && modulePrd.requirements && !modulePrd.$version) {
150
150
  console.warn('⚠ DEPRECATED: FORMAT A prd.json detected. Re-run `ss derive-prd` to generate v3 format.');
151
- writeJSON('.ralph/prd.json', transformPrdJsonToRalphV2(modulePrd, currentModule.code));
151
+ writeJSON(currentModule.prdFile, transformPrdJsonToRalphV2(modulePrd, currentModule.code));
152
152
  }
153
153
  // v2 legacy
154
154
  else {
155
- writeJSON('.ralph/prd.json', modulePrd);
155
+ writeJSON(currentModule.prdFile, modulePrd);
156
156
  }
157
157
  }
158
158
 
@@ -50,7 +50,7 @@ EF Core creates a single ModelSnapshot per DbContext. If Phase 1 creates a migra
50
50
  ## 1. Detection
51
51
 
52
52
  ```javascript
53
- const prd = readJSON('.ralph/prd.json');
53
+ const prd = readJSON(currentPrdPath);
54
54
  const domainTasks = prd.tasks.filter(t => t.category === 'domain');
55
55
  const sections = prd.architecture?.sections ?? [];
56
56
 
@@ -313,7 +313,7 @@ prd._sectionSplit = {
313
313
  entityToSection
314
314
  };
315
315
 
316
- writeJSON('.ralph/prd.json', prd);
316
+ writeJSON(currentPrdPath, prd);
317
317
  ```
318
318
 
319
319
  ---
@@ -344,7 +344,7 @@ if (!depsOk) {
344
344
  // After apex returns:
345
345
  nextPhase.status = 'completed';
346
346
  prd._sectionSplit.currentPhase = nextPhase.phase;
347
- writeJSON('.ralph/prd.json', prd);
347
+ writeJSON(currentPrdPath, prd);
348
348
  ```
349
349
 
350
350
  ---
@@ -369,7 +369,7 @@ for (const phaseTask of phasePrd.tasks) {
369
369
  }
370
370
  }
371
371
 
372
- writeJSON('.ralph/prd.json', prd);
372
+ writeJSON(currentPrdPath, prd);
373
373
  ```
374
374
 
375
375
  ---
@@ -413,13 +413,13 @@ After all phases complete or on final report:
413
413
  if (prd._sectionSplit?.enabled) {
414
414
  // Delete temporary phase PRD files
415
415
  for (const phase of prd._sectionSplit.phases) {
416
- if (fileExists(phase.prdFile) && phase.prdFile !== '.ralph/prd.json') {
416
+ if (fileExists(phase.prdFile) && phase.prdFile !== currentPrdPath) {
417
417
  deleteFile(phase.prdFile);
418
418
  }
419
419
  }
420
420
  // Remove split state from master PRD
421
421
  delete prd._sectionSplit;
422
- writeJSON('.ralph/prd.json', prd);
422
+ writeJSON(currentPrdPath, prd);
423
423
  }
424
424
  ```
425
425
 
@@ -46,17 +46,27 @@ See [references/task-transform-legacy.md](../references/task-transform-legacy.md
46
46
 
47
47
  ## 1. Check for Existing prd.json
48
48
 
49
- If `.ralph/prd.json` exists:
49
+ ```javascript
50
+ // PRD PATH RESOLUTION (MANDATORY - runs before any PRD access)
51
+ const queuePath = '.ralph/modules-queue.json';
52
+ let currentPrdPath = '.ralph/prd.json';
53
+ if (fileExists(queuePath)) {
54
+ const queue = readJSON(queuePath);
55
+ currentPrdPath = queue.queue[queue.currentIndex].prdFile;
56
+ }
57
+ ```
58
+
59
+ If `{currentPrdPath}` exists:
50
60
  1. Read the PRD
51
61
  2. **v3 FAST PATH:** If `$version === "3.0.0"` AND `tasks[]` exists:
52
62
  - Inject runtime fields if missing (status, config, feature, created, updated_at, history)
53
- - Write back to `.ralph/prd.json`
63
+ - Write back to `{currentPrdPath}`
54
64
  - **Run CATEGORY COMPLETENESS CHECK (section 4b) before proceeding**
55
65
  - Skip directly to section 5 (find next task)
56
66
  3. **v2 legacy:** If `$version === "2.0.0"` → find next eligible task (section 5)
57
67
  4. **FORMAT A (deprecated):** If `.project && .requirements && !.$version` → run `transformPrdJsonToRalphV2()` → section 5
58
68
 
59
- If `.ralph/prd.json` does not exist: continue to section 1b.
69
+ If `{currentPrdPath}` does not exist: continue to section 1b.
60
70
 
61
71
  ### 1b. BA Handoff Quality Gate (BLOCKING when BA artifacts present)
62
72
 
@@ -110,7 +120,7 @@ Generate **3-30 subtasks** by category (domain → infrastructure → applicatio
110
120
 
111
121
  ## 3. Create prd.json
112
122
 
113
- Write `.ralph/prd.json` with all tasks (every task MUST have: id, description, status, category, dependencies, acceptance_criteria, started_at, completed_at, iteration, commit_hash, files_changed, validation, error).
123
+ Write `{currentPrdPath}` with all tasks (every task MUST have: id, description, status, category, dependencies, acceptance_criteria, started_at, completed_at, iteration, commit_hash, files_changed, validation, error).
114
124
 
115
125
  Initialize `.ralph/progress.txt`:
116
126
  ```markdown
@@ -30,13 +30,21 @@ Delegate the current module's tasks to `/apex -d` for code generation. Ralph han
30
30
  ### 1. Verify Dependencies
31
31
 
32
32
  ```javascript
33
- const prd = readJSON('.ralph/prd.json');
33
+ // PRD PATH RESOLUTION
34
+ const queuePath = '.ralph/modules-queue.json';
35
+ let currentPrdPath = '.ralph/prd.json';
36
+ if (fileExists(queuePath)) {
37
+ const queue = readJSON(queuePath);
38
+ currentPrdPath = queue.queue[queue.currentIndex].prdFile;
39
+ }
40
+
41
+ const prd = readJSON(currentPrdPath);
34
42
  const task = prd.tasks.find(t => t.id === {current_task_id});
35
43
  for (const depId of task.dependencies) {
36
44
  const dep = prd.tasks.find(t => t.id === depId);
37
45
  if (!dep || dep.status !== 'completed') {
38
46
  task.status = 'blocked'; task.error = `Dependency ${depId} not completed`;
39
- writeJSON('.ralph/prd.json', prd);
47
+ writeJSON(currentPrdPath, prd);
40
48
  STOP — return to step-01
41
49
  }
42
50
  }
@@ -47,7 +55,7 @@ for (const depId of task.dependencies) {
47
55
  ```javascript
48
56
  task.status = 'in_progress';
49
57
  task.started_at = new Date().toISOString();
50
- writeJSON('.ralph/prd.json', prd);
58
+ writeJSON(currentPrdPath, prd);
51
59
  ```
52
60
 
53
61
  ### 3. Delegate to /apex
@@ -106,7 +114,7 @@ Read `references/section-splitting.md` sections 7-9 for execution, merging, and
106
114
 
107
115
  nextPhase.status = 'completed';
108
116
  prd._sectionSplit.currentPhase = nextPhase.phase;
109
- writeJSON('.ralph/prd.json', prd);
117
+ writeJSON(currentPrdPath, prd);
110
118
 
111
119
  // Continue to step-03 (commit), then step-04 loops back for next phase
112
120
  }
@@ -116,7 +124,7 @@ Read `references/section-splitting.md` sections 7-9 for execution, merging, and
116
124
 
117
125
  #### 3c. Standard Mode (no split, no parallel)
118
126
 
119
- **INVOKE `/apex -d .ralph/prd.json`**
127
+ **INVOKE `/apex -d {currentPrdPath}`**
120
128
 
121
129
  This delegates ALL remaining tasks for the current module to apex:
122
130
 
@@ -134,7 +142,7 @@ This delegates ALL remaining tasks for the current module to apex:
134
142
 
135
143
  ```javascript
136
144
  // Re-read PRD after apex execution
137
- const updatedPrd = readJSON('.ralph/prd.json');
145
+ const updatedPrd = readJSON(currentPrdPath);
138
146
  const moduleTasks = updatedPrd.tasks.filter(t => t.module === {current_module});
139
147
 
140
148
  const completed = moduleTasks.filter(t => t.status === 'completed').length;