@atlashub/smartstack-cli 4.26.0 → 4.28.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 (27) hide show
  1. package/dist/mcp-entry.mjs +33 -11
  2. package/dist/mcp-entry.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/agents/ba-writer.md +46 -46
  5. package/templates/project/appsettings.json.template +4 -6
  6. package/templates/skills/apex/SKILL.md +1 -0
  7. package/templates/skills/apex/references/challenge-questions.md +17 -0
  8. package/templates/skills/apex/references/core-seed-data.md +27 -4
  9. package/templates/skills/apex/references/post-checks.md +330 -0
  10. package/templates/skills/apex/references/smartstack-layers.md +31 -0
  11. package/templates/skills/apex/steps/step-02-plan.md +9 -0
  12. package/templates/skills/apex/steps/step-03-execute.md +102 -4
  13. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +33 -0
  14. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +17 -0
  15. package/templates/skills/business-analyse/references/spec-auto-inference.md +12 -7
  16. package/templates/skills/business-analyse/steps/step-00-init.md +19 -9
  17. package/templates/skills/business-analyse/steps/step-02-structure.md +20 -6
  18. package/templates/skills/business-analyse/steps/step-03-specify.md +7 -0
  19. package/templates/skills/business-analyse/steps/step-04-consolidate.md +2 -14
  20. package/templates/skills/controller/references/mcp-scaffold-workflow.md +20 -0
  21. package/templates/skills/derive-prd/references/handoff-file-templates.md +25 -1
  22. package/templates/skills/derive-prd/references/handoff-seeddata-generation.md +3 -1
  23. package/templates/skills/ralph-loop/references/category-completeness.md +125 -0
  24. package/templates/skills/ralph-loop/references/compact-loop.md +90 -3
  25. package/templates/skills/ralph-loop/references/module-transition.md +60 -0
  26. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -12
  27. package/templates/skills/ralph-loop/steps/step-05-report.md +205 -14
@@ -191,6 +191,129 @@ for (const [cat, check] of Object.entries(artifactChecks)) {
191
191
  }
192
192
  ```
193
193
 
194
+ ## Entity-Level File Completeness Check (BLOCKING)
195
+
196
+ > **LESSON LEARNED (audit ba-002):** Artifact verification checked "at least one file per category"
197
+ > but never reconciled against the **handoff contract** (`prd.implementation.filesToCreate`).
198
+ > Result: 4/17 API endpoints were missing but the category showed "complete" because *some* controllers existed.
199
+
200
+ ```javascript
201
+ // BLOCKING: Verify EVERY file from prd.implementation.filesToCreate exists on disk
202
+ const filesToCreate = prd.implementation?.filesToCreate;
203
+ if (filesToCreate) {
204
+ const handoffMissing = [];
205
+ const handoffPresent = [];
206
+
207
+ for (const [category, files] of Object.entries(filesToCreate)) {
208
+ for (const file of (files || [])) {
209
+ const filePath = file.path || file;
210
+ if (fileExists(filePath)) {
211
+ handoffPresent.push({ category, path: filePath });
212
+ } else {
213
+ handoffMissing.push({ category, path: filePath });
214
+ }
215
+ }
216
+ }
217
+
218
+ const totalHandoff = handoffPresent.length + handoffMissing.length;
219
+ const coveragePct = totalHandoff > 0 ? Math.round((handoffPresent.length / totalHandoff) * 100) : 100;
220
+ console.log(`Handoff file coverage: ${handoffPresent.length}/${totalHandoff} (${coveragePct}%)`);
221
+
222
+ if (handoffMissing.length > 0) {
223
+ console.error(`BLOCKING: ${handoffMissing.length} files from handoff contract missing on disk`);
224
+
225
+ // Group missing files by category for targeted remediation
226
+ const missingByCategory = {};
227
+ for (const m of handoffMissing) {
228
+ if (!missingByCategory[m.category]) missingByCategory[m.category] = [];
229
+ missingByCategory[m.category].push(m.path);
230
+ }
231
+
232
+ // Inject remediation tasks for each missing file
233
+ let maxIdNum = Math.max(...prd.tasks.map(t => {
234
+ const num = parseInt(t.id.replace(/[^0-9]/g, ''), 10);
235
+ return isNaN(num) ? 0 : num;
236
+ }), 0);
237
+ const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'HNDOFF-';
238
+
239
+ for (const [cat, paths] of Object.entries(missingByCategory)) {
240
+ for (const p of paths) {
241
+ // Skip if a remediation task already exists for this file
242
+ const alreadyExists = prd.tasks.some(t =>
243
+ t.description.includes(p) && t.description.includes('[HANDOFF-REMEDIATION]')
244
+ );
245
+ if (alreadyExists) continue;
246
+
247
+ maxIdNum++;
248
+ prd.tasks.push({
249
+ id: `${prefix}${String(maxIdNum).padStart(3, '0')}`,
250
+ description: `[HANDOFF-REMEDIATION] Create missing file: ${p}`,
251
+ status: 'pending',
252
+ category: cat,
253
+ dependencies: [],
254
+ acceptance_criteria: `File ${p} exists on disk and compiles`,
255
+ started_at: null, completed_at: null, iteration: null,
256
+ commit_hash: null, files_changed: [], validation: null, error: null
257
+ });
258
+ }
259
+ console.error(` ${cat}: ${paths.length} missing — ${paths.map(p => p.split('/').pop()).join(', ')}`);
260
+ }
261
+
262
+ writeJSON(currentPrdPath, prd);
263
+ }
264
+ } // end filesToCreate check
265
+ ```
266
+
267
+ ## Type Reference Verification (Cross-File Integrity)
268
+
269
+ > **LESSON LEARNED (audit ba-002):** Artifact verification counted files but didn't check
270
+ > that types referenced in code actually exist. `EmployeeListDto` was used in services
271
+ > but the file was missing — a "phantom reference" that artifact file-counting missed.
272
+
273
+ ```javascript
274
+ // After artifact checks, verify cross-file type references for completed categories
275
+ if (completedCats.has('application') && completedCats.has('api')) {
276
+ const serviceFiles = glob('src/**/Services/**/*Service.cs');
277
+ const dtoFiles = glob('src/**/DTOs/**/*.cs');
278
+ const entityFiles = glob('src/**/Domain/**/*.cs');
279
+
280
+ // Extract all DTO type names from actual files
281
+ const existingTypes = new Set();
282
+ for (const f of [...dtoFiles, ...entityFiles]) {
283
+ const content = readFile(f);
284
+ const typeMatches = content.matchAll(/(?:class|record|struct|enum|interface)\s+(\w+)/g);
285
+ for (const m of typeMatches) existingTypes.add(m[1]);
286
+ }
287
+
288
+ // Check service files for references to types that don't exist
289
+ const phantomRefs = [];
290
+ for (const f of serviceFiles) {
291
+ const content = readFile(f);
292
+ // Look for Dto/Entity type references in return types and method params
293
+ const typeRefs = content.matchAll(/(?:<|,\s*|\(|new\s+)(\w+(?:Dto|Entity|Exception))\b/g);
294
+ for (const m of typeRefs) {
295
+ if (!existingTypes.has(m[1]) && !m[1].startsWith('I')) {
296
+ phantomRefs.push({ file: f, type: m[1] });
297
+ }
298
+ }
299
+ }
300
+
301
+ if (phantomRefs.length > 0) {
302
+ console.error(`PHANTOM TYPE REFERENCES DETECTED (${phantomRefs.length}):`);
303
+ phantomRefs.forEach(r => console.error(` ${r.file}: references ${r.type} — TYPE DOES NOT EXIST`));
304
+
305
+ // Reset application category tasks to pending — code references non-existent types
306
+ prd.tasks.filter(t => t.category === 'application' && t.status === 'completed')
307
+ .forEach(t => {
308
+ t.status = 'pending';
309
+ t.error = `Phantom type references: ${phantomRefs.map(r=>r.type).join(', ')}`;
310
+ t.completed_at = null;
311
+ });
312
+ writeJSON(currentPrdPath, prd);
313
+ }
314
+ }
315
+ ```
316
+
194
317
  ---
195
318
 
196
319
  ## Key Rules
@@ -198,4 +321,6 @@ for (const [cat, check] of Object.entries(artifactChecks)) {
198
321
  - **Inject EVERY iteration:** Not just once during load
199
322
  - **Frontend depends on seedData:** Not just API
200
323
  - **Check artifacts:** Mark tasks pending if files don't exist
324
+ - **Check type references:** Verify types used in code actually exist (phantom reference detection)
325
+ - **Check handoff files:** Verify EVERY file from `prd.implementation.filesToCreate` exists on disk
201
326
  - **Never skip:** This is the blocker for "missing frontend/test" failures
@@ -289,6 +289,35 @@ if (pending > 0) {
289
289
  console.log(`Apex completed: ${completed}/${batchIds.length} tasks`);
290
290
  ```
291
291
 
292
+ ### B3b. Post-Batch Build Verification (BLOCKING)
293
+
294
+ > **LESSON LEARNED (audit ba-002):** Without build checks between batches, compilation
295
+ > errors from early batches propagate silently through all subsequent batches.
296
+ > The final "Build PASS" was never actually verified.
297
+
298
+ ```bash
299
+ # Quick build check after each batch — BLOCKING if fails
300
+ dotnet build --no-restore --verbosity quiet
301
+ if [ $? -ne 0 ]; then
302
+ echo "BLOCKING: Build fails after batch [${firstCategory}]"
303
+ # Inject immediate fix task — will be picked up next iteration
304
+ fi
305
+ ```
306
+
307
+ ```javascript
308
+ if (BUILD_FAILED) {
309
+ const maxId = Math.max(...updatedPrd.tasks.map(t => parseInt(t.id.replace(/\D/g,''))||0));
310
+ updatedPrd.tasks.push({
311
+ id: `FIX-${maxId+1}`,
312
+ description: `BLOCKING: Fix build regression after ${firstCategory} batch`,
313
+ status: 'pending', category: 'validation', dependencies: [],
314
+ acceptance_criteria: 'dotnet build passes with 0 errors'
315
+ });
316
+ writeJSON(currentPrdPath, updatedPrd);
317
+ // Continue to section C (commit) then D (loop) — fix task will be picked up next iteration
318
+ }
319
+ ```
320
+
292
321
  ---
293
322
 
294
323
  ## C. Commit PRD State
@@ -306,7 +335,65 @@ appendFile('.ralph/progress.txt',
306
335
  );
307
336
  ```
308
337
 
309
- ### C2. Increment Iteration
338
+ ### C2. Skipped Task Audit
339
+
340
+ After apex returns, check for newly skipped tasks:
341
+
342
+ > **LESSON LEARNED (audit ba-002):** Individual skips in critical categories (api, test, domain,
343
+ > infrastructure) allowed 4/17 endpoints and 74% stub tests to pass undetected. The old logic
344
+ > only blocked when ALL tasks in a category were skipped — individual skips slipped through.
345
+
346
+ ```javascript
347
+ const CRITICAL_CATEGORIES = ['api', 'test', 'domain', 'infrastructure'];
348
+ const MAX_SKIP_RETRIES = 3;
349
+
350
+ const newlySkipped = prdCheck.tasks.filter(t =>
351
+ batchIds.includes(t.id) && t.status === 'skipped'
352
+ );
353
+ if (newlySkipped.length > 0) {
354
+ console.warn(`⚠ ${newlySkipped.length} tasks were SKIPPED by apex:`);
355
+ newlySkipped.forEach(t => console.warn(` - ${t.id}: ${t.description} [${t.category}]`));
356
+
357
+ const skippedCategories = [...new Set(newlySkipped.map(t => t.category))];
358
+ for (const cat of skippedCategories) {
359
+ const isCritical = CRITICAL_CATEGORIES.includes(cat);
360
+ const allInCat = prdCheck.tasks.filter(t => t.category === cat);
361
+ const allSkipped = allInCat.every(t => t.status === 'skipped');
362
+
363
+ if (isCritical) {
364
+ // CRITICAL CATEGORY: Block on INDIVIDUAL skips (not just when ALL are skipped)
365
+ const skippedInCat = newlySkipped.filter(t => t.category === cat);
366
+ for (const t of skippedInCat) {
367
+ const retryCount = t._skipRetryCount || 0;
368
+ if (retryCount < MAX_SKIP_RETRIES) {
369
+ console.error(`BLOCKING: Task ${t.id} skipped in critical category "${cat}" — reset to pending (retry ${retryCount + 1}/${MAX_SKIP_RETRIES})`);
370
+ t.status = 'pending';
371
+ t._skipRetryCount = retryCount + 1;
372
+ t._retryCount = (t._retryCount || 0) + 1;
373
+ t.error = `Skipped in critical category "${cat}" — auto-retry ${retryCount + 1}/${MAX_SKIP_RETRIES}`;
374
+ } else {
375
+ console.error(`FAILED: Task ${t.id} skipped ${MAX_SKIP_RETRIES} times in critical category "${cat}" — marking failed`);
376
+ t.status = 'failed';
377
+ t.error = `Skipped ${MAX_SKIP_RETRIES} times in critical category "${cat}" — max retries exhausted`;
378
+ }
379
+ }
380
+ } else {
381
+ // NON-CRITICAL CATEGORY: Block only when ALL tasks in category are skipped (existing behavior)
382
+ if (allSkipped) {
383
+ console.error(`BLOCKING: ALL tasks in category "${cat}" were skipped — investigate root cause`);
384
+ allInCat.forEach(t => {
385
+ t.status = 'pending';
386
+ t._retryCount = (t._retryCount || 0) + 1;
387
+ t.error = `All tasks in category "${cat}" skipped — auto-retry`;
388
+ });
389
+ }
390
+ }
391
+ }
392
+ writeJSON(currentPrdPath, prdCheck);
393
+ }
394
+ ```
395
+
396
+ ### C3. Increment Iteration
310
397
 
311
398
  ```javascript
312
399
  prdCheck.config.current_iteration++;
@@ -314,7 +401,7 @@ prdCheck.updated_at = new Date().toISOString();
314
401
  writeJSON(currentPrdPath, prdCheck);
315
402
  ```
316
403
 
317
- ### C3. Git Commit (PRD state only — apex already committed code)
404
+ ### C4. Git Commit (PRD state only — apex already committed code)
318
405
 
319
406
  ```bash
320
407
  git add {currentPrdPath} .ralph/progress.txt
@@ -331,7 +418,7 @@ EOF
331
418
  )"
332
419
  ```
333
420
 
334
- ### C4. PRD Sync Verification (HARD CHECK)
421
+ ### C5. PRD Sync Verification (HARD CHECK)
335
422
 
336
423
  > **Note:** `prdCheck` (read in C1) is the authoritative post-apex snapshot.
337
424
  > `completed` (from B3) was computed on a DIFFERENT read — do NOT mix the two.
@@ -27,6 +27,27 @@ if (fileExists(queuePath)) {
27
27
 
28
28
  // All categories present — module is complete
29
29
  // Continue below...
30
+
31
+ // HANDOFF FILE GATE (double-security — step-04 §1.8 is primary, this is defense-in-depth)
32
+ // The build gate does NOT detect missing files if nothing references them (no compile error).
33
+ const ftc = prd.implementation?.filesToCreate;
34
+ if (ftc) {
35
+ let ftcMissing = 0;
36
+ const ftcDetails = [];
37
+ for (const [cat, files] of Object.entries(ftc)) {
38
+ for (const file of (files || [])) {
39
+ if (!fileExists(file.path || file)) {
40
+ ftcMissing++;
41
+ ftcDetails.push(`${cat}: ${(file.path || file).split('/').pop()}`);
42
+ }
43
+ }
44
+ }
45
+ if (ftcMissing > 0) {
46
+ console.error(`BLOCKING: ${ftcMissing} handoff files missing — cannot advance module`);
47
+ ftcDetails.forEach(d => console.error(` ${d}`));
48
+ return; // Back to compact loop — remediation tasks already injected by category-completeness
49
+ }
50
+ }
30
51
  }
31
52
  ```
32
53
 
@@ -36,6 +57,36 @@ if (fileExists(queuePath)) {
36
57
 
37
58
  ```javascript
38
59
  if (missing.length === 0) {
60
+ // MANDATORY BUILD+TEST GATE before advancing module
61
+ // (audit ba-002: module marked "100% complete" despite build errors and security flaws)
62
+ const buildOk = execSync('dotnet build --no-restore --verbosity quiet').exitCode === 0;
63
+ if (!buildOk) {
64
+ console.error(`BLOCKING: Module ${currentModule.code} build fails — cannot advance to next module`);
65
+ const maxId = Math.max(...prd.tasks.map(t => parseInt(t.id.replace(/\D/g,''))||0));
66
+ prd.tasks.push({
67
+ id: `GATE-${maxId+1}`, description: `BLOCKING: Fix build for module ${currentModule.code}`,
68
+ status: 'pending', category: 'validation', dependencies: [],
69
+ acceptance_criteria: 'dotnet build passes with 0 errors'
70
+ });
71
+ writeJSON(currentPrdPath, prd);
72
+ return; // Back to compact loop — do NOT advance
73
+ }
74
+
75
+ const testOk = execSync('dotnet test --no-build --verbosity quiet').exitCode === 0;
76
+ if (!testOk) {
77
+ console.error(`BLOCKING: Module ${currentModule.code} tests fail — cannot advance to next module`);
78
+ const maxId = Math.max(...prd.tasks.map(t => parseInt(t.id.replace(/\D/g,''))||0));
79
+ prd.tasks.push({
80
+ id: `GATE-${maxId+1}`, description: `BLOCKING: Fix tests for module ${currentModule.code}`,
81
+ status: 'pending', category: 'validation', dependencies: [],
82
+ acceptance_criteria: 'dotnet test passes with 0 failures'
83
+ });
84
+ writeJSON(currentPrdPath, prd);
85
+ return; // Back to compact loop — do NOT advance
86
+ }
87
+
88
+ console.log(`MODULE GATE PASSED: ${currentModule.code} — build OK, tests OK`);
89
+
39
90
  // Mark current module done
40
91
  currentModule.status = 'completed';
41
92
  queue.completedModules++;
@@ -68,6 +119,15 @@ if (missing.length === 0) {
68
119
 
69
120
  console.log(`MODULE COMPLETE: ${currentModule.code} → NEXT: ${queue.modules[nextIndex].code}`);
70
121
 
122
+ // CONTEXT REFRESH: Force /compact between modules to prevent context degradation
123
+ // (audit ba-002: quality degraded progressively — module 2 had more errors than module 1
124
+ // because patterns from module 1 were lost during context compaction)
125
+ console.log('Forcing /compact for fresh context before next module...');
126
+ // The /compact command clears accumulated tool results and intermediate reasoning,
127
+ // preserving only key decisions and the current state. This ensures the next module
128
+ // starts with a clean context window instead of degraded fragments from the previous module.
129
+ // INVOKE /compact
130
+
71
131
  // Return to step-01 to load next module's tasks
72
132
  // (step-01 will detect module-changed.json and load next PRD)
73
133
  return GO_TO_STEP_01;
@@ -33,24 +33,80 @@ const tasksTotal = prd.tasks.length;
33
33
  const allDone = (tasksCompleted + tasksSkipped) === tasksTotal;
34
34
  ```
35
35
 
36
- ### 1.5. Lightweight Sanity Check
36
+ ### 1.5. Build & Test Gate (BLOCKING)
37
37
 
38
- > **Note:** Full build verification, POST-CHECKs, and test runs are handled by apex.
39
- > Ralph only does a lightweight sanity check to detect regressions between apex invocations.
38
+ > **LESSON LEARNED (audit ba-002):** A non-blocking sanity build allowed "92/92 COMPLETE" with
39
+ > code that had compilation errors and security flaws. The build gate MUST be BLOCKING.
40
40
 
41
41
  ```bash
42
- # Quick build check (quiet mode)
42
+ # STEP 1: Build gate (BLOCKING — must pass before ANY further iteration)
43
43
  dotnet build --no-restore --verbosity quiet
44
44
  BUILD_RC=$?
45
45
  # Note: WSL bin\Debug cleanup handled by PostToolUse hook (wsl-dotnet-cleanup.sh)
46
46
  if [ $BUILD_RC -ne 0 ]; then
47
- echo "BUILD REGRESSION detected between apex invocations"
48
- # Inject fix task — apex will handle the actual fix
49
- prd.tasks.push({ id: maxId+1, description: "Fix build regression",
47
+ echo "BLOCKING: BUILD FAILED cannot advance until fixed"
48
+ # Inject fix task with HIGH priority — apex will handle the actual fix
49
+ prd.tasks.push({ id: maxId+1, description: "BLOCKING: Fix build regression — dotnet build fails",
50
50
  status: "pending", category: "validation", dependencies: [],
51
- acceptance_criteria: "dotnet build passes" });
51
+ acceptance_criteria: "dotnet build passes with 0 errors" });
52
52
  writeJSON(currentPrdPath, prd);
53
+ # DO NOT mark module complete — force compact loop to fix first
53
54
  fi
55
+
56
+ # STEP 2: Test gate (BLOCKING — runs only if build passes)
57
+ if [ $BUILD_RC -eq 0 ]; then
58
+ dotnet test --no-build --verbosity quiet
59
+ TEST_RC=$?
60
+ if [ $TEST_RC -ne 0 ]; then
61
+ echo "BLOCKING: TESTS FAILED — cannot advance until fixed"
62
+ prd.tasks.push({ id: maxId+2, description: "BLOCKING: Fix test regression — dotnet test fails",
63
+ status: "pending", category: "validation", dependencies: [],
64
+ acceptance_criteria: "dotnet test passes with 0 failures" });
65
+ writeJSON(currentPrdPath, prd);
66
+ fi
67
+ fi
68
+ ```
69
+
70
+ ### 1.6. MCP Security Gate (post-module validation)
71
+
72
+ > **LESSON LEARNED (audit ba-002):** Apex POST-CHECKs validate structure (TenantId present,
73
+ > [RequirePermission] present) but miss SEMANTIC errors (Read permission on write endpoints,
74
+ > self-approval, cross-tenant FK references). This gate catches those.
75
+
76
+ ```javascript
77
+ // Run ONLY when all tasks for current module are complete (avoid wasting tokens mid-loop)
78
+ const moduleTasksDone = prd.tasks.filter(t => t.status === 'completed').length === prd.tasks.length;
79
+
80
+ if (moduleTasksDone && BUILD_RC === 0) {
81
+ // 1. Validate security (catches tenant isolation gaps, permission issues)
82
+ const securityResult = await mcp__smartstack__validate_security();
83
+
84
+ // 2. Review code quality (catches logic errors, exception inconsistencies)
85
+ const reviewResult = await mcp__smartstack__review_code();
86
+
87
+ // 3. SEMANTIC PERMISSION CHECK: write endpoints must not use Read permissions
88
+ // This check catches the ba-002 bug where Cancel (POST) used Permissions.*.Read
89
+ const permissionIssues = [];
90
+ for (const ctrlFile of glob('src/**/Controllers/**/*Controller.cs')) {
91
+ const content = readFile(ctrlFile);
92
+ // Find POST/PUT/DELETE endpoints with Read permission
93
+ const writeEndpoints = content.matchAll(/\[(HttpPost|HttpPut|HttpDelete|HttpPatch)[^\]]*\][^[]*\[RequirePermission\(([^\)]+)\)\]/gs);
94
+ for (const match of writeEndpoints) {
95
+ if (match[2].includes('.Read')) {
96
+ permissionIssues.push(`${ctrlFile}: ${match[1]} endpoint uses Read permission — should be Update/Delete/Create`);
97
+ }
98
+ }
99
+ }
100
+
101
+ if (permissionIssues.length > 0) {
102
+ console.error('BLOCKING: Write endpoints with Read permissions detected:');
103
+ permissionIssues.forEach(i => console.error(` ${i}`));
104
+ prd.tasks.push({ id: maxId+3, description: "BLOCKING: Fix permission mismatch — write endpoints using Read permission",
105
+ status: "pending", category: "validation", dependencies: [],
106
+ acceptance_criteria: "All POST/PUT/DELETE endpoints use appropriate write permissions (Create/Update/Delete)" });
107
+ writeJSON(currentPrdPath, prd);
108
+ }
109
+ }
54
110
  ```
55
111
 
56
112
  ### 1.7. Category Completeness Check (RUNS EVERY ITERATION)
@@ -71,6 +127,125 @@ if (guardrailsNeeded.length > 0) {
71
127
  // Artifact verification is handled inside checkCategoryCompleteness() — see reference file.
72
128
  ```
73
129
 
130
+ ### 1.8. Handoff File Gate (BLOCKING)
131
+
132
+ > **LESSON LEARNED (audit ba-002):** Category completeness checked "at least one file per category"
133
+ > but 4/17 API endpoints were missing because individual files were never reconciled against the handoff contract.
134
+
135
+ ```javascript
136
+ // BLOCKING: Reconcile prd.implementation.filesToCreate against disk
137
+ const filesToCreate = prd.implementation?.filesToCreate;
138
+ if (filesToCreate) {
139
+ let handoffDeclared = 0, handoffPresent = 0;
140
+ const missingByCategory = {};
141
+
142
+ for (const [category, files] of Object.entries(filesToCreate)) {
143
+ for (const file of (files || [])) {
144
+ handoffDeclared++;
145
+ const filePath = file.path || file;
146
+ if (fileExists(filePath)) {
147
+ handoffPresent++;
148
+ } else {
149
+ if (!missingByCategory[category]) missingByCategory[category] = [];
150
+ missingByCategory[category].push(filePath);
151
+ }
152
+ }
153
+ }
154
+
155
+ const coveragePct = handoffDeclared > 0 ? Math.round((handoffPresent / handoffDeclared) * 100) : 100;
156
+ console.log(`Handoff file coverage: ${handoffPresent}/${handoffDeclared} (${coveragePct}%)`);
157
+
158
+ if (coveragePct < 100) {
159
+ console.error(`BLOCKING: Handoff coverage ${coveragePct}% — ${handoffDeclared - handoffPresent} files missing`);
160
+ for (const [cat, paths] of Object.entries(missingByCategory)) {
161
+ console.error(` ${cat}: ${paths.length} missing`);
162
+ }
163
+ // category-completeness.md injects remediation tasks — do NOT advance module
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### 1.9. Business Rule Coverage Gate (BLOCKING)
169
+
170
+ > **LESSON LEARNED (audit ba-002):** 5/22 business rules were not implemented despite all tasks
171
+ > being marked COMPLETE. Task-level tracking ("file created?") missed rule-level coverage
172
+ > ("does the code actually implement BR-007?").
173
+
174
+ ```javascript
175
+ // BLOCKING: Verify each BR from brToCodeMapping has an implementation
176
+ const brMapping = prd.implementation?.brToCodeMapping;
177
+ if (brMapping) {
178
+ const brMissing = [];
179
+ const brNotTested = [];
180
+
181
+ for (const br of brMapping) {
182
+ // Check if the component file exists
183
+ const componentFile = br.component || br.file;
184
+ const methodName = br.method || br.function;
185
+
186
+ if (!componentFile || !fileExists(componentFile)) {
187
+ brMissing.push({ id: br.id, rule: br.rule, component: componentFile, reason: 'file not found' });
188
+ continue;
189
+ }
190
+
191
+ // Check if the method exists in the file
192
+ if (methodName) {
193
+ const content = readFile(componentFile);
194
+ if (!content.includes(methodName)) {
195
+ brMissing.push({ id: br.id, rule: br.rule, component: componentFile, reason: `method ${methodName} not found` });
196
+ continue;
197
+ }
198
+ }
199
+
200
+ // Check test coverage (WARNING only — test category handles enforcement)
201
+ if (br.testFile && !fileExists(br.testFile)) {
202
+ brNotTested.push({ id: br.id, rule: br.rule, testFile: br.testFile });
203
+ }
204
+ }
205
+
206
+ if (brMissing.length > 0) {
207
+ console.error(`BLOCKING: ${brMissing.length}/${brMapping.length} business rules NOT implemented:`);
208
+ for (const br of brMissing) {
209
+ console.error(` ${br.id}: ${br.rule} — ${br.reason}`);
210
+ }
211
+
212
+ // Inject remediation tasks for missing BRs
213
+ let maxIdNum = Math.max(...prd.tasks.map(t => {
214
+ const num = parseInt(t.id.replace(/[^0-9]/g, ''), 10);
215
+ return isNaN(num) ? 0 : num;
216
+ }), 0);
217
+ const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'BR-';
218
+
219
+ for (const br of brMissing) {
220
+ const alreadyExists = prd.tasks.some(t =>
221
+ t.description.includes('[BR-REMEDIATION]') && t.description.includes(br.id)
222
+ );
223
+ if (alreadyExists) continue;
224
+
225
+ maxIdNum++;
226
+ prd.tasks.push({
227
+ id: `${prefix}${String(maxIdNum).padStart(3, '0')}`,
228
+ description: `[BR-REMEDIATION] Implement ${br.id}: ${br.rule}`,
229
+ status: 'pending',
230
+ category: 'application',
231
+ dependencies: [],
232
+ acceptance_criteria: `Business rule ${br.id} implemented in ${br.component} with method ${br.method || 'TBD'}`,
233
+ started_at: null, completed_at: null, iteration: null,
234
+ commit_hash: null, files_changed: [], validation: null, error: null
235
+ });
236
+ }
237
+ writeJSON(currentPrdPath, prd);
238
+ }
239
+
240
+ if (brNotTested.length > 0) {
241
+ console.warn(`WARNING: ${brNotTested.length}/${brMapping.length} business rules have no test file:`);
242
+ for (const br of brNotTested) {
243
+ console.warn(` ${br.id}: ${br.rule} — expected test: ${br.testFile}`);
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
74
249
  ## 2. Check Iteration Limit
75
250
 
76
251
  If `current_iteration > max_iterations`:
@@ -123,10 +298,30 @@ if (fileExists(queuePath)) {
123
298
  console.log(`Module ${currentModule.code}: missing categories ${missing.join(', ')} — continuing loop`);
124
299
  // Fall through to section 5 (compact loop)
125
300
  } else {
126
- // All categories complete — advance to next module
127
- // (detailed logic in references/module-transition.md)
128
- // MANDATORY: If this was the LAST module, ALWAYS proceed to step-05-report.md.
129
- // NEVER stop without generating the final report.
301
+ // All categories complete — verify handoff files before advancing
302
+ const ftc = prd.implementation?.filesToCreate;
303
+ if (ftc) {
304
+ let ftcMissing = 0;
305
+ for (const [cat, files] of Object.entries(ftc)) {
306
+ for (const file of (files || [])) {
307
+ if (!fileExists(file.path || file)) ftcMissing++;
308
+ }
309
+ }
310
+ if (ftcMissing > 0) {
311
+ console.error(`BLOCKING: ${ftcMissing} handoff files still missing — cannot advance module`);
312
+ // Fall through to section 5 (compact loop) — remediation tasks injected by 1.8
313
+ } else {
314
+ // Handoff 100% + all categories complete — advance to next module
315
+ // (detailed logic in references/module-transition.md)
316
+ // MANDATORY: If this was the LAST module, ALWAYS proceed to step-05-report.md.
317
+ // NEVER stop without generating the final report.
318
+ }
319
+ } else {
320
+ // No handoff contract — advance to next module (manual mode)
321
+ // (detailed logic in references/module-transition.md)
322
+ // MANDATORY: If this was the LAST module, ALWAYS proceed to step-05-report.md.
323
+ // NEVER stop without generating the final report.
324
+ }
130
325
  }
131
326
  }
132
327
  ```