---
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

## YOUR TASK:

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

---

## 1. Read Current State

```javascript
const prd = readJSON('.ralph/prd.json');
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. Lightweight Sanity Check

> **Note:** Full build verification, POST-CHECKs, and test runs are handled by apex.
> Ralph only does a lightweight sanity check to detect regressions between apex invocations.

```bash
# Quick build check (quiet mode)
dotnet build --no-restore --verbosity quiet
if [ $? -ne 0 ]; then
  echo "BUILD REGRESSION detected between apex invocations"
  # Inject fix task — apex will handle the actual fix
  prd.tasks.push({ id: maxId+1, description: "Fix build regression",
    status: "pending", category: "validation", dependencies: [],
    acceptance_criteria: "dotnet build passes" });
  writeJSON('.ralph/prd.json', prd);
fi
```

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

> **CRITICAL:** This check runs REGARDLESS of allDone status.
> In test-v4-005, allDone was never true (PRD status not updated), so the
> module completeness check in section 3b never triggered, and missing
> frontend/test categories were never detected.

```javascript
const presentCategories = new Set(prd.tasks.map(t => t.category));
const REQUIRED_CATEGORIES = ['domain', 'infrastructure', 'application', 'api', 'seedData', 'frontend', 'test'];
const missingFromPrd = REQUIRED_CATEGORIES.filter(c => !presentCategories.has(c));

if (missingFromPrd.length > 0) {
  console.warn(`PRD MISSING CATEGORIES: ${missingFromPrd.join(', ')}`);

  // Inject guardrail tasks for missing categories
  let maxIdNum = Math.max(...prd.tasks.map(t => parseInt(t.id.replace(/[^0-9]/g, ''), 10) || 0));
  const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'GUARD-';
  const lastApiTask = prd.tasks.filter(t => t.category === 'api').pop()?.id;
  const lastSeedDataTask = prd.tasks.filter(t => t.category === 'seedData').pop()?.id;

  for (const cat of missingFromPrd) {
    maxIdNum++;
    const taskId = `${prefix}${String(maxIdNum).padStart(3, '0')}`;
    prd.tasks.push({
      id: taskId,
      description: `[GUARDRAIL] Generate missing ${cat} layer for ${prd.project?.module || 'module'}`,
      status: 'pending', category: cat,
      dependencies: lastSeedDataTask ? [lastSeedDataTask] : (lastApiTask ? [lastApiTask] : []),
      acceptance_criteria: [
        cat === 'frontend' ? 'React pages + routes wired to App.tsx' :
        cat === 'test' ? 'Unit + integration tests passing' :
        `${cat} layer complete`
      ],
      started_at: null, completed_at: null, iteration: null,
      commit_hash: null, files_changed: [], validation: null, error: null
    });
    console.log(`  Injected [${taskId}] ${cat} guardrail`);
  }
  writeJSON('.ralph/prd.json', prd);

  // Re-count after injection
  const newPending = prd.tasks.filter(t => t.status === 'pending').length;
  console.log(`PRD updated: +${missingFromPrd.length} guardrails, ${newPending} pending tasks`);
}

// Also check: artifact existence for categories with completed tasks
const completedCats = new Set(prd.tasks.filter(t => t.status === 'completed').map(t => t.category));
const artifactChecks = {
  'frontend': () => glob.sync('**/src/pages/**/*.tsx').length > 0,
  'test': () => glob.sync('tests/**/*Tests.cs').length > 0,
  'api': () => glob.sync('src/*/Controllers/**/*Controller.cs').length > 0
};
for (const [cat, check] of Object.entries(artifactChecks)) {
  if (completedCats.has(cat) && !check()) {
    console.warn(`ARTIFACT MISSING: ${cat} tasks marked completed but no files found — resetting`);
    prd.tasks.filter(t => t.category === cat && t.status === 'completed')
      .forEach(t => { t.status = 'pending'; t.error = 'Artifacts missing — re-execute'; t.completed_at = null; });
    writeJSON('.ralph/prd.json', prd);
  }
}
```

## 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)

```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 {
    // Module complete — advance
    currentModule.status = 'completed';
    queue.completedModules++;
    const nextIndex = queue.currentIndex + 1;

    if (nextIndex < queue.totalModules) {
      queue.currentIndex = nextIndex;
      queue.modules[nextIndex].status = 'in-progress';
      writeJSON(queuePath, queue);

      writeJSON('.ralph/module-changed.json', {
        fromModule: currentModule.code, toModule: queue.modules[nextIndex].code,
        timestamp: new Date().toISOString()
      });

      prd.status = 'completed';
      writeJSON('.ralph/prd.json', prd);
      const nextPrd = readJSON(queue.modules[nextIndex].prdFile);
      nextPrd.config.current_iteration = 1;
      nextPrd.config.max_iterations = prd.config.max_iterations;
      writeJSON('.ralph/prd.json', nextPrd);

      console.log(`MODULE COMPLETE: ${currentModule.code} → NEXT: ${queue.modules[nextIndex].code}`);
      // → step-01 (module transition)
      return;
    }

    // ALL MODULES COMPLETE
    writeJSON(queuePath, queue);
    console.log(`ALL ${queue.totalModules} MODULES COMPLETE`);
  }
}
```

### 3c. Completion Output

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

Set `prd.status = 'completed'` → 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('.ralph/prd.json', 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 ralph-loop
- **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)
