---
name: step-04-check
description: Check completion criteria with status-based logic
next_step: steps/step-05-report.md OR steps/step-01-task.md
---

# Step 4: Check Completion

> **STATE RECOVERY (executable):** If context was compressed:
```javascript
if (fileExists('.ralph/ralph-state.json')) {
  const state = readJSON('.ralph/ralph-state.json');
  if (state.currentStep && state.currentStep !== 'step-04-check' && state.currentStep !== 'compact-loop') {
    console.warn(`RECOVERY: ralph-state.json says currentStep="${state.currentStep}" but we are in step-04.`);
  }
  // Restore module context if lost
  if (state.currentModule) { {current_module} = state.currentModule; }
  if (state.iteration) { {current_iteration} = state.iteration; }
}
```

## YOUR TASK:

Check if all tasks are complete and decide: output completion promise, advance module, or continue loop.

---

## 1. Read Current State

```javascript
// PRD PATH RESOLUTION
const queuePath = '.ralph/modules-queue.json';
let currentPrdPath = '.ralph/prd.json';
if (fileExists(queuePath)) {
  const queue = readJSON(queuePath);
  currentPrdPath = queue.modules[queue.currentIndex].prdFile;
}

const prd = readJSON(currentPrdPath);
const tasksCompleted = prd.tasks.filter(t => t.status === 'completed').length;
const tasksSkipped = prd.tasks.filter(t => t.status === 'skipped').length;
const tasksFailed = prd.tasks.filter(t => t.status === 'failed').length;
const tasksBlocked = prd.tasks.filter(t => t.status === 'blocked').length;
const tasksPending = prd.tasks.filter(t => t.status === 'pending').length;
const tasksTotal = prd.tasks.length;
const allDone = (tasksCompleted + tasksSkipped) === tasksTotal;
```

### 1.5. Build & Test Gate (BLOCKING)

> **LESSON LEARNED (audit ba-002):** A non-blocking sanity build allowed "92/92 COMPLETE" with
> code that had compilation errors and security flaws. The build gate MUST be BLOCKING.

```bash
# STEP 1: Build gate (BLOCKING — must pass before ANY further iteration)
dotnet build --no-restore --verbosity quiet
BUILD_RC=$?
# Note: WSL bin\Debug cleanup handled by PostToolUse hook (wsl-dotnet-cleanup.sh)
if [ $BUILD_RC -ne 0 ]; then
  echo "BLOCKING: BUILD FAILED — cannot advance until fixed"
  # Inject fix task with HIGH priority — apex will handle the actual fix
  prd.tasks.push({ id: maxId+1, description: "BLOCKING: Fix build regression — dotnet build fails",
    status: "pending", category: "validation", dependencies: [],
    acceptance_criteria: "dotnet build passes with 0 errors" });
  writeJSON(currentPrdPath, prd);
  # DO NOT mark module complete — force compact loop to fix first
fi

# STEP 2: Test gate (BLOCKING — runs only if build passes)
if [ $BUILD_RC -eq 0 ]; then
  dotnet test --no-build --verbosity quiet
  TEST_RC=$?
  if [ $TEST_RC -ne 0 ]; then
    echo "BLOCKING: TESTS FAILED — cannot advance until fixed"
    prd.tasks.push({ id: maxId+2, description: "BLOCKING: Fix test regression — dotnet test fails",
      status: "pending", category: "validation", dependencies: [],
      acceptance_criteria: "dotnet test passes with 0 failures" });
    writeJSON(currentPrdPath, prd);
  fi
fi
```

### 1.6. MCP Security Gate (post-module validation)

> **LESSON LEARNED (audit ba-002):** Apex POST-CHECKs validate structure (TenantId present,
> [RequirePermission] present) but miss SEMANTIC errors (Read permission on write endpoints,
> self-approval, cross-tenant FK references). This gate catches those.

```javascript
// Run ONLY when all tasks for current module are complete (avoid wasting tokens mid-loop)
const moduleTasksDone = prd.tasks.filter(t => t.status === 'completed').length === prd.tasks.length;

if (moduleTasksDone && BUILD_RC === 0) {
  // 1. Validate security (catches tenant isolation gaps, permission issues)
  const securityResult = await mcp__smartstack__validate_security();

  // 2. Review code quality (catches logic errors, exception inconsistencies)
  const reviewResult = await mcp__smartstack__review_code();

  // 3. SEMANTIC PERMISSION CHECK: write endpoints must not use Read permissions
  // This check catches the ba-002 bug where Cancel (POST) used Permissions.*.Read
  const permissionIssues = [];
  for (const ctrlFile of glob('src/**/Controllers/**/*Controller.cs')) {
    const content = readFile(ctrlFile);
    // Find POST/PUT/DELETE endpoints with Read permission
    const writeEndpoints = content.matchAll(/\[(HttpPost|HttpPut|HttpDelete|HttpPatch)[^\]]*\][^[]*\[RequirePermission\(([^\)]+)\)\]/gs);
    for (const match of writeEndpoints) {
      if (match[2].includes('.Read')) {
        permissionIssues.push(`${ctrlFile}: ${match[1]} endpoint uses Read permission — should be Update/Delete/Create`);
      }
    }
  }

  if (permissionIssues.length > 0) {
    console.error('BLOCKING: Write endpoints with Read permissions detected:');
    permissionIssues.forEach(i => console.error(`  ${i}`));
    prd.tasks.push({ id: maxId+3, description: "BLOCKING: Fix permission mismatch — write endpoints using Read permission",
      status: "pending", category: "validation", dependencies: [],
      acceptance_criteria: "All POST/PUT/DELETE endpoints use appropriate write permissions (Create/Update/Delete)" });
    writeJSON(currentPrdPath, prd);
  }
}
```

### 1.7. Category Completeness Check (RUNS EVERY ITERATION)

> **Implementation:** See `references/category-completeness.md` for the full `checkCategoryCompleteness()` function and artifact verification logic.
> Do NOT duplicate the implementation here — load and execute the reference.

```javascript
// Load references/category-completeness.md — execute checkCategoryCompleteness() as defined there
// (includes guardrail injection AND artifact verification)
const { missing, guardrailsNeeded } = checkCategoryCompleteness(prd);

if (guardrailsNeeded.length > 0) {
  prd.tasks.push(...guardrailsNeeded);
  writeJSON(currentPrdPath, prd);
  console.log(`PRD updated: +${guardrailsNeeded.length} guardrails, ${prd.tasks.filter(t => t.status === 'pending').length} pending tasks`);
}
// Artifact verification is handled inside checkCategoryCompleteness() — see reference file.
```

### 1.8. Handoff File Gate (BLOCKING)

> **LESSON LEARNED (audit ba-002):** Category completeness checked "at least one file per category"
> but 4/17 API endpoints were missing because individual files were never reconciled against the handoff contract.

```javascript
// BLOCKING: Reconcile prd.implementation.filesToCreate against disk
const filesToCreate = prd.implementation?.filesToCreate;
if (filesToCreate) {
  let handoffDeclared = 0, handoffPresent = 0;
  const missingByCategory = {};

  for (const [category, files] of Object.entries(filesToCreate)) {
    for (const file of (files || [])) {
      handoffDeclared++;
      const filePath = file.path || file;
      if (fileExists(filePath)) {
        handoffPresent++;
      } else {
        if (!missingByCategory[category]) missingByCategory[category] = [];
        missingByCategory[category].push(filePath);
      }
    }
  }

  const coveragePct = handoffDeclared > 0 ? Math.round((handoffPresent / handoffDeclared) * 100) : 100;
  console.log(`Handoff file coverage: ${handoffPresent}/${handoffDeclared} (${coveragePct}%)`);

  if (coveragePct < 100) {
    console.error(`BLOCKING: Handoff coverage ${coveragePct}% — ${handoffDeclared - handoffPresent} files missing`);
    for (const [cat, paths] of Object.entries(missingByCategory)) {
      console.error(`  ${cat}: ${paths.length} missing`);
    }
    // category-completeness.md injects remediation tasks — do NOT advance module
  }
}
```

### 1.9. Business Rule Coverage Gate (BLOCKING)

> **LESSON LEARNED (audit ba-002):** 5/22 business rules were not implemented despite all tasks
> being marked COMPLETE. Task-level tracking ("file created?") missed rule-level coverage
> ("does the code actually implement BR-007?").

```javascript
// BLOCKING: Verify each BR from brToCodeMapping has an implementation
const brMapping = prd.implementation?.brToCodeMapping;
if (brMapping) {
  const brMissing = [];
  const brNotTested = [];

  for (const br of brMapping) {
    // Check if the component file exists
    const componentFile = br.component || br.file;
    const methodName = br.method || br.function;

    if (!componentFile || !fileExists(componentFile)) {
      brMissing.push({ id: br.id, rule: br.rule, component: componentFile, reason: 'file not found' });
      continue;
    }

    // Check if the method exists in the file
    if (methodName) {
      const content = readFile(componentFile);
      if (!content.includes(methodName)) {
        brMissing.push({ id: br.id, rule: br.rule, component: componentFile, reason: `method ${methodName} not found` });
        continue;
      }
    }

    // Check test coverage (WARNING only — test category handles enforcement)
    if (br.testFile && !fileExists(br.testFile)) {
      brNotTested.push({ id: br.id, rule: br.rule, testFile: br.testFile });
    }
  }

  if (brMissing.length > 0) {
    console.error(`BLOCKING: ${brMissing.length}/${brMapping.length} business rules NOT implemented:`);
    for (const br of brMissing) {
      console.error(`  ${br.id}: ${br.rule} — ${br.reason}`);
    }

    // Inject remediation tasks for missing BRs
    let maxIdNum = Math.max(...prd.tasks.map(t => {
      const num = parseInt(t.id.replace(/[^0-9]/g, ''), 10);
      return isNaN(num) ? 0 : num;
    }), 0);
    const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'BR-';

    for (const br of brMissing) {
      const alreadyExists = prd.tasks.some(t =>
        t.description.includes('[BR-REMEDIATION]') && t.description.includes(br.id)
      );
      if (alreadyExists) continue;

      maxIdNum++;
      prd.tasks.push({
        id: `${prefix}${String(maxIdNum).padStart(3, '0')}`,
        description: `[BR-REMEDIATION] Implement ${br.id}: ${br.rule}`,
        status: 'pending',
        category: 'application',
        dependencies: [],
        acceptance_criteria: `Business rule ${br.id} implemented in ${br.component} with method ${br.method || 'TBD'}`,
        started_at: null, completed_at: null, iteration: null,
        commit_hash: null, files_changed: [], validation: null, error: null
      });
    }
    writeJSON(currentPrdPath, prd);
  }

  if (brNotTested.length > 0) {
    console.warn(`WARNING: ${brNotTested.length}/${brMapping.length} business rules have no test file:`);
    for (const br of brNotTested) {
      console.warn(`  ${br.id}: ${br.rule} — expected test: ${br.testFile}`);
    }
  }
}
```

## 2. Check Iteration Limit

If `current_iteration > max_iterations`:
```
MAX ITERATIONS REACHED: {iteration}/{max} | {completed}/{total} done
-> Generating partial report...
```
Set `prd.status = 'partial'` → step-05.

## 3. Check All Tasks Complete

**Re-read state after section 1.7 (categories may have been injected):**
```javascript
const tasksCompletedNow = prd.tasks.filter(t => t.status === 'completed').length;
const tasksTotalNow = prd.tasks.length;
const allDone = (tasksCompletedNow + prd.tasks.filter(t => t.status === 'skipped').length) === tasksTotalNow;
```

If `allDone`:

### 3a. Team Mode (multi-module with Agent Teams)

```javascript
if ({team_active}) {
  SendMessage({
    type: "message", recipient: "team-lead",
    content: `MODULE_COMPLETE:${current_module}`,
    summary: `${current_module} complete`
  });
  return;
}
```

### 3b. Sequential Multi-Module (modules-queue.json)

See `references/module-transition.md` for complete module advancement protocol.

```javascript
const queuePath = '.ralph/modules-queue.json';
if (fileExists(queuePath)) {
  const queue = readJSON(queuePath);
  const currentModule = queue.modules[queue.currentIndex];

  // MODULE COMPLETENESS CHECK
  const completedCats = new Set(prd.tasks.filter(t => t.status === 'completed').map(t => t.category));
  const expected = ['domain', 'infrastructure', 'application', 'api', 'seedData', 'frontend', 'test'];
  const missing = expected.filter(c => !completedCats.has(c));

  if (missing.length > 0) {
    console.log(`Module ${currentModule.code}: missing categories ${missing.join(', ')} — continuing loop`);
    // Fall through to section 5 (compact loop)
  } else {
    // All categories complete — verify handoff files before advancing
    const ftc = prd.implementation?.filesToCreate;
    if (ftc) {
      let ftcMissing = 0;
      for (const [cat, files] of Object.entries(ftc)) {
        for (const file of (files || [])) {
          if (!fileExists(file.path || file)) ftcMissing++;
        }
      }
      if (ftcMissing > 0) {
        console.error(`BLOCKING: ${ftcMissing} handoff files still missing — cannot advance module`);
        // Fall through to section 5 (compact loop) — remediation tasks injected by 1.8
      } else {
        // Handoff 100% + all categories complete — advance to next module
        // (detailed logic in references/module-transition.md)
        // MANDATORY: If this was the LAST module, ALWAYS proceed to step-05-report.md.
        // NEVER stop without generating the final report.
      }
    } else {
      // No handoff contract — advance to next module (manual mode)
      // (detailed logic in references/module-transition.md)
      // MANDATORY: If this was the LAST module, ALWAYS proceed to step-05-report.md.
      // NEVER stop without generating the final report.
    }
  }
}
```

### 3c. Completion Output

```
ALL TASKS COMPLETE | Iterations: {n} | Tasks: {completed}/{total}
<promise>{completion_promise}</promise>
```

Set `prd.status = 'completed'`.

**MANDATORY: After displaying completion, ALWAYS load step-05-report.md and generate the report.**
**NEVER stop after displaying "ALL TASKS COMPLETE" without generating the report.**
→ step-05.

## 4. Dead-End Check

```javascript
const hasPending = prd.tasks.some(t => t.status === 'pending');
const MAX_RETRIES = 3;

// Before declaring dead-end, check if any failed tasks have retries left
const retriableCount = prd.tasks.filter(t =>
  t.status === 'failed' && (t._retryCount || 0) < MAX_RETRIES
).length;

if (!hasPending && !allDone && retriableCount > 0) {
  // NOT a dead-end — compact loop will reset failed tasks to pending
  console.log(`${retriableCount} failed tasks have retries remaining — entering compact loop`);
  // Fall through to section 5
} else if (!hasPending && !allDone && retriableCount === 0) {
  // TRUE dead-end: all retries exhausted
  console.log(`DEAD-END: ${tasksCompletedNow} done, ${tasksFailed} failed (retries exhausted), ${tasksBlocked} blocked`);
  prd.status = 'failed';
  writeJSON(currentPrdPath, prd);
  // → step-05
}
```

## 5. Compact Loop (MANDATORY — EXECUTE IMMEDIATELY)

> **THIS IS NOT OPTIONAL.** If there are pending tasks, you MUST enter the compact loop NOW.
> DO NOT stop. DO NOT wait for user. DO NOT ask for confirmation. DO NOT re-read step files.
> **Read `references/compact-loop.md` and execute ALL sections inline.**
> **Stopping here is a VIOLATION of the execution guarantee.**

If `hasPending && iteration < max`:

1. Read `references/compact-loop.md`
2. Execute sections A → B → C → D inline (find tasks → delegate to apex → commit PRD → loop back)
3. **IMMEDIATELY** return to section 1 of THIS step (do NOT pause)
4. Repeat until ALL tasks are complete, max iterations reached, or dead-end

---

## CRITICAL RULES — EXECUTION GUARANTEE:

- **NEVER** output completion promise unless ALL tasks done (all modules if multi)
- **NEVER** stop between iterations — this is the CORE PROMISE of business-analyse-develop
- **NEVER** ask the user "should I continue?" or "is this OK?" — JUST CONTINUE
- **NEVER** pause to show intermediate results and wait for feedback
- **NEVER** generate code directly — ALWAYS delegate to /apex
- **BATCH** same-category tasks (max 5)
- **MODULE COMPLETENESS:** All layers must have completed tasks before advancing
- **Only valid stop conditions:** all complete, max iterations, dead-end, or user Ctrl+C

---

## NEXT STEP:

- All done + more modules in queue → `./step-01-task.md`
- All done (no more modules) → `./step-05-report.md`
- Max iterations or dead-end → `./step-05-report.md`
- Tasks remaining → compact loop (section 5)
