@atlashub/smartstack-cli 4.24.0 → 4.26.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 (23) hide show
  1. package/dist/index.js +765 -517
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/agents/ba-writer.md +7 -3
  5. package/templates/skills/ba-generate-html/references/data-build.md +22 -13
  6. package/templates/skills/ba-generate-html/references/data-mapping.md +33 -24
  7. package/templates/skills/ba-generate-html/steps/step-01-collect.md +15 -6
  8. package/templates/skills/ba-generate-html/steps/step-02-build-data.md +37 -22
  9. package/templates/skills/business-analyse/steps/step-00-init.md +9 -11
  10. package/templates/skills/business-analyse/steps/step-04-consolidate.md +15 -0
  11. package/templates/skills/derive-prd/steps/step-01-transform.md +6 -2
  12. package/templates/skills/derive-prd/steps/step-02-export.md +12 -0
  13. package/templates/skills/ralph-loop/references/category-completeness.md +3 -3
  14. package/templates/skills/ralph-loop/references/compact-loop.md +47 -11
  15. package/templates/skills/ralph-loop/references/init-resume-recovery.md +1 -1
  16. package/templates/skills/ralph-loop/references/module-transition.md +30 -5
  17. package/templates/skills/ralph-loop/references/multi-module-queue.md +4 -4
  18. package/templates/skills/ralph-loop/references/section-splitting.md +6 -6
  19. package/templates/skills/ralph-loop/steps/step-01-task.md +14 -4
  20. package/templates/skills/ralph-loop/steps/step-02-execute.md +14 -6
  21. package/templates/skills/ralph-loop/steps/step-03-commit.md +15 -4
  22. package/templates/skills/ralph-loop/steps/step-04-check.md +19 -5
  23. package/templates/skills/ralph-loop/steps/step-05-report.md +35 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.24.0",
3
+ "version": "4.26.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -63,8 +63,11 @@ Create initial index.json and empty thematic files with metadata and draft statu
63
63
  - For application scope: modules: []
64
64
  - For module scope: (no modules array)
65
65
  5. Create empty thematic files appropriate for scope:
66
- - Project/application: cadrage.json, entities.json, rules.json, usecases.json, permissions.json, screens.json, validation.json, handoff.json, consolidation.json
67
- - Module: discovery.json, analysis.json, specification.json, validation.json, handoff.json
66
+ - Project/application: cadrage.json, validation.json, handoff.json, consolidation.json
67
+ > **NOTE:** Do NOT create entities.json, rules.json, usecases.json, permissions.json, screens.json at project/application level.
68
+ > These thematic files contain module-specific data and are created ONLY at module level by step-03 (specify).
69
+ > Creating empty versions at app level causes downstream tools (derive-prd) to read empty arrays instead of module data.
70
+ - Module: entities.json, rules.json, usecases.json, permissions.json, screens.json, validation.json, handoff.json
68
71
  6. Update `.business-analyse/config.json` with new lastFeatureId
69
72
  7. IF scope = "module" AND applicationRef provided AND moduleCode provided:
70
73
  a. Read master index.json (via applicationRef FEAT-NNN)
@@ -107,7 +110,8 @@ Create a project-level index.json for multi-application analysis. Only used when
107
110
  - fileHashes: {}
108
111
  - applications: []
109
112
  - applicationDependencyGraph: {}
110
- 5. Create thematic files (cadrage.json, entities.json, rules.json, usecases.json, permissions.json, screens.json, validation.json, consolidation.json)
113
+ 5. Create thematic files (cadrage.json, validation.json, consolidation.json)
114
+ > Do NOT create entities/rules/usecases/permissions/screens at project level — these exist only at module level.
111
115
  6. Update `.business-analyse/config.json` with new lastProjectId
112
116
  7. Deploy schemas to `docs/business-analyse/schemas/` (including project-schema.json)
113
117
  8. Return project ID (PROJ-NNN) and path
@@ -63,11 +63,13 @@ const FEATURE_DATA = {
63
63
  ],
64
64
  moduleSpecs: {
65
65
  // CRITICAL: Must have ONE entry per module with ALL module data
66
+ // FLAT-FILE: Data comes from collected_data.modules[moduleCode] (flat files), NOT module index.json
66
67
  "{moduleCode}": {
67
- useCases: module.specification.useCases || [],
68
- businessRules: module.analysis.businessRules || [],
68
+ // const mod = collected_data.modules[moduleCode];
69
+ useCases: mod.usecases?.useCases || [],
70
+ businessRules: mod.rules?.rules || [],
69
71
  // ENTITY SAFETY NET: map fields[] → attributes[] if agent deviated from canonical format
70
- entities: (module.analysis.entities || []).map(ent => ({
72
+ entities: (mod.entities?.entities || []).map(ent => ({
71
73
  name: ent.name,
72
74
  description: ent.description || "",
73
75
  attributes: (ent.attributes || []).length > 0
@@ -79,8 +81,8 @@ const FEATURE_DATA = {
79
81
  : (ent.fields || []).filter(f => f.foreignKey)
80
82
  .map(f => `${f.foreignKey.table} (N:1) - ${f.description || f.name}`)
81
83
  })),
82
- permissions: module.specification.permissionMatrix?.permissions || [],
83
- apiEndpoints: module.specification.apiEndpoints || []
84
+ permissions: mod.permissions?.matrix || mod.permissions?.permissions || [],
85
+ apiEndpoints: mod.usecases?.apiEndpoints || []
84
86
  }
85
87
  },
86
88
  consolidation: {
@@ -102,10 +104,15 @@ const FEATURE_DATA = {
102
104
  ### Build Process
103
105
 
104
106
  1. Extract metadata from master index.json
105
- 2. Extract cadrage from master index.json (CONVERT scope keys)
106
- 3. Extract stakeholders from master.stakeholders
107
+ 2. Extract cadrage from master cadrage.json (CONVERT scope keys)
108
+ 3. Extract stakeholders from master cadrage.stakeholders
107
109
  4. Iterate ALL modules and populate moduleSpecs (THIS IS CRITICAL — empty moduleSpecs = BUG)
108
- 5. For EACH module, extract: useCases, businessRules, entities, permissions, apiEndpoints
110
+ 5. For EACH module, read FLAT FILES from `collected_data.modules[moduleCode]`:
111
+ - `entities.json` → entities
112
+ - `rules.json` → businessRules
113
+ - `usecases.json` → useCases
114
+ - `permissions.json` → permissions
115
+ - `screens.json` → wireframes
109
116
  6. Extract consolidation data (integrations, shared entities, E2E flows)
110
117
  7. Extract handoff section (complexity, strategy, module order, file counts)
111
118
 
@@ -119,10 +126,12 @@ const FEATURE_DATA = {
119
126
  const EMBEDDED_ARTIFACTS = {
120
127
  wireframes: {
121
128
  // PER-MODULE keyed object (NOT a flat array)
122
- // FOR EACH module: extract from specification.uiWireframes[] OR specification.wireframes[] (SAFETY NET)
123
- // IMPORTANT: The agent may write wireframes under either key name — always check BOTH
124
- // Read from module JSON files
125
- [moduleCode]: (moduleFeature.specification.uiWireframes || moduleFeature.specification.wireframes || []).map(wf => ({
129
+ // FLAT-FILE: Read from screens.json in each module directory (collected in step-01)
130
+ // const mod = collected_data.modules[moduleCode];
131
+ // const screens = mod.screens?.screens || [];
132
+ [moduleCode]: screens
133
+ .filter(s => s.wireframe || s.mockup || s.mockupFormat)
134
+ .map(wf => ({
126
135
  screen: wf.screen || wf.name || wf.title || wf.id || "", // SAFETY NET: fallback name/title/id → screen
127
136
  section: wf.section || "", // e.g. "list"
128
137
  format: wf.mockupFormat || "ascii", // RENAME: mockupFormat → format
@@ -160,7 +169,7 @@ const EMBEDDED_ARTIFACTS = {
160
169
 
161
170
  ### Artifact Gathering
162
171
 
163
- 1. For EACH module: read `specification.uiWireframes[]` OR `specification.wireframes[]` (check BOTH keys agent may use either), **rename fields** (`mockupFormat`→`format`, `mockup`/`ascii`/`content`→`content`, `screen`/`name`/`title`→`screen`), store under `wireframes[moduleCode]`
172
+ 1. For EACH module: read `screens.json` flat file from `collected_data.modules[moduleCode].screens`, filter screens with wireframe data, **rename fields** (`mockupFormat`→`format`, `mockup`/`ascii`/`content`→`content`, `screen`/`name`/`title`→`screen`), store under `wireframes[moduleCode]`
164
173
  2. Read master's `consolidation.e2eFlows[]` and build e2eFlows array with diagram generation
165
174
  3. Read master's `dependencyGraph` and build nodes/edges
166
175
  4. Serialize as JSON with 2-space indentation
@@ -93,29 +93,32 @@ Build a JSON object following this **exact mapping** from index.json to the HTML
93
93
  }
94
94
  ```
95
95
 
96
- ### Module Specs Mapping
96
+ ### Module Specs Mapping (FLAT-FILE ARCHITECTURE)
97
97
 
98
98
  For EACH module in `master.modules[]`:
99
99
 
100
- 1. Read the module index.json at `master.modules[i].featureJsonPath`
101
- 2. Map to `moduleSpecs[moduleCode]`:
100
+ 1. Use flat file data from `collected_data.modules[moduleCode]` (loaded in step-01)
101
+ 2. Data sources: `entities.json`, `rules.json`, `usecases.json`, `permissions.json`, `screens.json`
102
+ 3. Do NOT read from `moduleIndex.specification` or `moduleIndex.analysis` — these don't exist in flat-file format
102
103
 
103
104
  ```javascript
105
+ const mod = collected_data.modules[moduleCode];
106
+
104
107
  moduleSpecs[moduleCode] = {
105
- useCases: (moduleFeature.specification?.useCases || []).map(uc => ({
108
+ useCases: (mod.usecases?.useCases || []).map(uc => ({
106
109
  name: uc.name,
107
110
  actor: uc.primaryActor,
108
111
  steps: (uc.mainScenario || []).join("\n"), // array → newline-separated string
109
112
  alternative: (uc.alternativeScenarios || [])
110
113
  .map(a => a.name + ": " + (a.steps || []).join(", ")).join("\n")
111
114
  })),
112
- businessRules: (moduleFeature.analysis?.businessRules || []).map(br => ({
115
+ businessRules: (mod.rules?.rules || []).map(br => ({
113
116
  name: br.name,
114
117
  category: br.category, // "validation"|"calculation"|"workflow"|"security"|"data"
115
118
  statement: br.statement,
116
119
  example: (br.examples || []).map(e => e.input + " → " + e.expected).join("; ")
117
120
  })),
118
- entities: (moduleFeature.analysis?.entities || []).map(ent => ({
121
+ entities: (mod.entities?.entities || []).map(ent => ({
119
122
  name: ent.name,
120
123
  description: ent.description || "",
121
124
  attributes: (ent.attributes || []).map(a => ({
@@ -128,7 +131,7 @@ moduleSpecs[moduleCode] = {
128
131
  r.target + " (" + r.type + ") - " + (r.description || "")
129
132
  )
130
133
  })),
131
- permissions: buildPermissionKeys(moduleFeature), // see below
134
+ permissions: buildPermissionKeys(mod.permissions), // see below
132
135
  notes: "",
133
136
  mockupNotes: "" // Deprecated: wireframes now embedded separately in EMBEDDED_ARTIFACTS
134
137
  }
@@ -139,9 +142,10 @@ moduleSpecs[moduleCode] = {
139
142
  The HTML uses `"Role|Action"` format (e.g. `"RH Admin|Consulter"`):
140
143
 
141
144
  ```javascript
142
- function buildPermissionKeys(moduleFeature) {
145
+ // Input: permissions object from flat file (mod.permissions), NOT moduleFeature
146
+ function buildPermissionKeys(permissionsData) {
143
147
  const keys = [];
144
- const matrix = moduleFeature.specification?.permissionMatrix;
148
+ const matrix = permissionsData?.matrix || permissionsData?.permissionMatrix;
145
149
  if (!matrix) return keys;
146
150
  const actionMap = { read: "Consulter", create: "Creer", update: "Modifier",
147
151
  delete: "Supprimer", validate: "Valider", export: "Exporter",
@@ -207,25 +211,30 @@ Build a JSON object containing ALL visual artifacts from module JSON files (inde
207
211
  }
208
212
  ```
209
213
 
210
- ### Wireframes Mapping
214
+ ### Wireframes Mapping (FLAT-FILE: from screens.json)
211
215
 
212
216
  For EACH module in `master.modules[]`:
213
217
 
214
218
  ```javascript
215
- // Read the module index.json file
216
- const moduleFeature = readModuleFeature(moduleCode);
217
- const wireframes = (moduleFeature.specification?.uiWireframes || []).map(wf => ({
218
- screen: wf.screen, // e.g. "UM-list", "UM-form"
219
- section: wf.section, // e.g. "list", "form"
220
- format: wf.mockupFormat || "ascii", // "ascii" | "svg"
221
- content: wf.mockup, // ASCII art or SVG markup
222
- description: wf.description || "",
223
- elements: wf.elements || [], // ["DataGrid", "FilterBar", ...]
224
- actions: wf.actions || [], // ["filter", "sort", "create", ...]
225
- componentMapping: wf.componentMapping || [], // [{ wireframeElement, reactComponent }]
226
- layout: wf.layout || null, // { type, regions: [...] }
227
- permissionsRequired: wf.permissionsRequired || []
228
- }));
219
+ // Use flat file data from collected_data (loaded in step-01)
220
+ const mod = collected_data.modules[moduleCode];
221
+ const screens = mod.screens?.screens || [];
222
+
223
+ // Extract wireframes from screens that have mockup/wireframe data
224
+ const wireframes = screens
225
+ .filter(s => s.wireframe || s.mockup || s.mockupFormat)
226
+ .map(wf => ({
227
+ screen: wf.screen || wf.name || wf.id || "", // e.g. "UM-list", "UM-form"
228
+ section: wf.section || "", // e.g. "list", "form"
229
+ format: wf.mockupFormat || "ascii", // "ascii" | "svg"
230
+ content: wf.mockup || wf.ascii || wf.content || "", // ASCII art or SVG markup
231
+ description: wf.description || "",
232
+ elements: wf.elements || [], // ["DataGrid", "FilterBar", ...]
233
+ actions: wf.actions || [], // ["filter", "sort", "create", ...]
234
+ componentMapping: wf.componentMapping || [], // [{ wireframeElement, reactComponent }]
235
+ layout: wf.layout || null, // { type, regions: [...] }
236
+ permissionsRequired: wf.permissionsRequired || []
237
+ }));
229
238
 
230
239
  // Store in artifacts object
231
240
  EMBEDDED_ARTIFACTS.wireframes[moduleCode] = wireframes;
@@ -51,7 +51,11 @@ ELSE:
51
51
  BLOCKING ERROR: "Feature status: {feature.status}, expected consolidated"
52
52
  ```
53
53
 
54
- ### 4. Read All JSON Files
54
+ ### 4. Read All JSON Files (FLAT-FILE ARCHITECTURE)
55
+
56
+ > **CRITICAL:** Module data lives in separate flat files (entities.json, usecases.json, etc.),
57
+ > NOT inside index.json. The module index.json only contains metadata and file pointers.
58
+ > You MUST read each flat file individually.
55
59
 
56
60
  ```
57
61
  collected_data = {
@@ -63,13 +67,18 @@ collected_data = {
63
67
  }
64
68
 
65
69
  FOR each module in master.index.modules[]:
66
- moduleIndexPath = module.featureJsonPath || "{module.code}/business-analyse/v{version}/index.json"
67
- moduleIndex = READ(moduleIndexPath)
70
+ // Resolve module directory path
71
+ moduleDir = "{app}/{module.code}/business-analyse/v{version}/"
72
+ moduleIndex = READ(moduleDir + "index.json")
68
73
 
74
+ // Read ALL flat files from module directory
69
75
  collected_data.modules[module.code] = {
70
76
  index: moduleIndex,
71
- specification: moduleIndex.specification || {},
72
- analysis: moduleIndex.analysis || {}
77
+ entities: READ(moduleDir + "entities.json") || { entities: [] },
78
+ rules: READ(moduleDir + "rules.json") || { rules: [] },
79
+ usecases: READ(moduleDir + "usecases.json") || { useCases: [] },
80
+ permissions: READ(moduleDir + "permissions.json") || { roles: [], permissions: [], matrix: [] },
81
+ screens: READ(moduleDir + "screens.json") || { screens: [] }
73
82
  }
74
83
  ```
75
84
 
@@ -78,7 +87,7 @@ FOR each module in master.index.modules[]:
78
87
  ```
79
88
  JSON collected:
80
89
  Master: index.json + cadrage.json
81
- Modules: {module_count} module index files
90
+ Modules: {module_count} modules (flat files: entities, rules, usecases, permissions, screens)
82
91
  → Proceeding to data build...
83
92
  ```
84
93
 
@@ -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
 
@@ -358,13 +358,12 @@ IF workflow_mode = "project":
358
358
 
359
359
  Output path: docs/business-analyse/v{version}/index.json
360
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
361
+ - cadrage.json
366
362
  - validation.json
367
- - handoff.json
363
+ - consolidation.json
364
+ // NOTE: entities, rules, usecases, permissions, screens are MODULE-LEVEL only.
365
+ // They are created by step-03 (specify) in each module directory.
366
+ // Do NOT create them at project/app level — empty files mislead downstream tools.
368
367
 
369
368
  Store:
370
369
  project_id: string // PROJ-NNN
@@ -396,13 +395,12 @@ ELSE:
396
395
 
397
396
  Output path: docs/{app}/business-analyse/v{version}/index.json
398
397
  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
398
+ - cadrage.json
404
399
  - validation.json
405
400
  - handoff.json
401
+ // NOTE: entities, rules, usecases, permissions, screens are MODULE-LEVEL only.
402
+ // They are created by step-03 (specify) in each module directory.
403
+ // Do NOT create them at app level — empty files mislead derive-prd and ralph-loop.
406
404
  ```
407
405
 
408
406
  > **Note:** In project mode, per-application index.json files are created later in step-01-cadrage.
@@ -423,6 +423,21 @@ ba-writer.enrichSection({
423
423
  // Update status
424
424
  ba-writer.updateStatus({feature_id}, "consolidated");
425
425
 
426
+ // FIX H3: Clean up app-level index.json files entries
427
+ // Only keep entries for files that actually have content at app level.
428
+ // Module-level data lives in module directories — do NOT declare empty app-level files.
429
+ ba-writer.enrichSection({
430
+ featureId: {feature_id},
431
+ section: "files",
432
+ data: {
433
+ // Keep only files that exist and have content at app level
434
+ cadrage: { path: "cadrage.json", hash: "framed", lastModified: now() },
435
+ validation: { path: "validation.json", hash: "consolidated", lastModified: now() }
436
+ // REMOVED: entities, rules, usecases, permissions, screens, handoff
437
+ // These exist only at module level (flat-file architecture)
438
+ }
439
+ });
440
+
426
441
  // Save workflow state for resume support
427
442
  ba-writer.enrichSection({
428
443
  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
 
@@ -275,13 +311,13 @@ appendFile('.ralph/progress.txt',
275
311
  ```javascript
276
312
  prdCheck.config.current_iteration++;
277
313
  prdCheck.updated_at = new Date().toISOString();
278
- writeJSON('.ralph/prd.json', prdCheck);
314
+ writeJSON(currentPrdPath, prdCheck);
279
315
  ```
280
316
 
281
317
  ### C3. Git Commit (PRD state only — apex already committed code)
282
318
 
283
319
  ```bash
284
- git add .ralph/prd.json .ralph/progress.txt
320
+ git add {currentPrdPath} .ralph/progress.txt
285
321
  [ -f .ralph/modules-queue.json ] && git add .ralph/modules-queue.json
286
322
 
287
323
  git commit -m "$(cat <<'EOF'
@@ -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;