@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.
- package/dist/mcp-entry.mjs +33 -11
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/agents/ba-writer.md +46 -46
- package/templates/project/appsettings.json.template +4 -6
- package/templates/skills/apex/SKILL.md +1 -0
- package/templates/skills/apex/references/challenge-questions.md +17 -0
- package/templates/skills/apex/references/core-seed-data.md +27 -4
- package/templates/skills/apex/references/post-checks.md +330 -0
- package/templates/skills/apex/references/smartstack-layers.md +31 -0
- package/templates/skills/apex/steps/step-02-plan.md +9 -0
- package/templates/skills/apex/steps/step-03-execute.md +102 -4
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +33 -0
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +17 -0
- package/templates/skills/business-analyse/references/spec-auto-inference.md +12 -7
- package/templates/skills/business-analyse/steps/step-00-init.md +19 -9
- package/templates/skills/business-analyse/steps/step-02-structure.md +20 -6
- package/templates/skills/business-analyse/steps/step-03-specify.md +7 -0
- package/templates/skills/business-analyse/steps/step-04-consolidate.md +2 -14
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +20 -0
- package/templates/skills/derive-prd/references/handoff-file-templates.md +25 -1
- package/templates/skills/derive-prd/references/handoff-seeddata-generation.md +3 -1
- package/templates/skills/ralph-loop/references/category-completeness.md +125 -0
- package/templates/skills/ralph-loop/references/compact-loop.md +90 -3
- package/templates/skills/ralph-loop/references/module-transition.md +60 -0
- package/templates/skills/ralph-loop/steps/step-04-check.md +207 -12
- 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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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.
|
|
36
|
+
### 1.5. Build & Test Gate (BLOCKING)
|
|
37
37
|
|
|
38
|
-
> **
|
|
39
|
-
>
|
|
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
|
-
#
|
|
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
|
|
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 —
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
```
|