# Compact Loop — Delegate to /apex

> **Loaded by:** step-04 section 5 (after first full iteration)
> **Purpose:** Execute ALL remaining tasks autonomously by delegating to /apex.
> **EXECUTION GUARANTEE:** This loop runs until ALL tasks are done, max iterations, or dead-end.
> **ABSOLUTE RULES:**
> - NEVER stop the loop
> - NEVER wait for user input
> - NEVER ask for confirmation
> - NEVER re-read step files
> - NEVER pause between iterations
> - NEVER generate code directly — delegate to /apex
> - Execute A → B → C → D → back to step-04 section 1 → repeat

---

## Loop Entry

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

Display compact progress:
```
[{iteration}/{max}] {completed}/{total} done | Finding eligible tasks...
```

---

## A0. Section-Split Mode (checked BEFORE standard batching)

```javascript
const prd = readJSON(currentPrdPath);

if (prd._sectionSplit?.enabled) {
  // SECTION-SPLIT: Delegate per-phase instead of per-category batch

  // Find next pending phase with dependencies met
  const nextPhase = prd._sectionSplit.phases.find(p => p.status === 'pending');

  if (!nextPhase) {
    // All phases done — check if master PRD tasks are all complete
    // → STOP this section. Return to step-04 section 1 (CHECK_COMPLETION path).
  }

  const depsOk = nextPhase.dependsOn.every(depIdx =>
    prd._sectionSplit.phases[depIdx].status === 'completed'
  );

  if (!depsOk) {
    // Phase blocked — should not happen with topological sort
    console.warn(`Phase ${nextPhase.phase} blocked — dependencies incomplete`);
    // → STOP this section. Return to step-04 section 1 (CHECK_COMPLETION path).
  }

  const phaseLabel = nextPhase.type === 'foundation'
    ? `Phase 0: Foundation (${nextPhase.entities.length} entities + migration)`
    : `Phase ${nextPhase.phase}: Section "${nextPhase.sectionCode}" (${nextPhase.entities.length} entities)`;
  console.log(`[{iteration}/{max}] SECTION SPLIT: ${phaseLabel}`);

  // ↓ MANDATORY: execute this skill call now (not pseudo-code)
```

**INVOKE `/apex -d {nextPhase.prdFile}`**

Apex sees a focused PRD for this phase only. After apex returns, continue:

```javascript
  const phasePrd = readJSON(nextPhase.prdFile);
  for (const phaseTask of phasePrd.tasks) {
    const masterTask = prd.tasks.find(t => t.id === phaseTask.id);
    if (masterTask) {
      masterTask.status = phaseTask.status;
      masterTask.completed_at = phaseTask.completed_at;
      masterTask.commit_hash = phaseTask.commit_hash;
      masterTask.files_changed = phaseTask.files_changed;
      masterTask.error = phaseTask.error;
      masterTask.validation = phaseTask.validation;
    }
  }

  // Handle phase failure
  if (phasePrd.tasks.some(t => t.status === 'failed')) {
    const retryCount = nextPhase._retryCount || 0;
    if (retryCount < 2) {
      nextPhase.status = 'pending';
      nextPhase._retryCount = retryCount + 1;
      console.log(`Phase ${nextPhase.phase} had failures — retry ${retryCount + 1}/2`);
      // Reset failed tasks in phase PRD for retry
      for (const task of phasePrd.tasks) {
        if (task.status === 'failed') { task.status = 'pending'; task.error = null; }
      }
      writeJSON(nextPhase.prdFile, phasePrd);
    } else {
      nextPhase.status = 'failed';
      console.error(`Phase ${nextPhase.phase} failed after 2 retries`);
    }
  } else {
    nextPhase.status = 'completed';
  }

  prd._sectionSplit.currentPhase = nextPhase.phase;
  writeJSON(currentPrdPath, prd);

  // → Skip to section C (commit PRD state), then D (loop back)
  // → SKIP sections A and B. Jump directly to section C (COMMIT_PRD) below.
}
```

> **IMPORTANT:** When `_sectionSplit.enabled`, the standard category-based batching (section A below)
> is SKIPPED. The loop delegates per-phase to apex instead of per-category-batch.

---

## A. Find Eligible Tasks

> **Note:** This section is SKIPPED when `prd._sectionSplit?.enabled` (handled by A0 above).

```javascript
const MAX_RETRIES = 3;

// DEAD-END PRE-CHECK: verify if we are truly stuck BEFORE resetting retries.
// Resetting retries FIRST would mask a dead-end — the check must run on the raw state.
const failedBeforeRetry = prd.tasks.filter(t => t.status === 'failed');
const retriableBeforeReset = failedBeforeRetry.filter(t => (t._retryCount || 0) < MAX_RETRIES);
const hasPendingBeforeRetry = prd.tasks.some(t => t.status === 'pending');
const allDoneBeforeRetry = prd.tasks.every(t => t.status === 'completed' || t.status === 'skipped');

const isDeadEnd = !hasPendingBeforeRetry && !allDoneBeforeRetry && retriableBeforeReset.length === 0;

if (isDeadEnd) {
  // TRUE dead-end: no pending tasks, not all done, and no retries left.
  // Mark prd.status = 'failed' NOW so step-04 section 4 routes to step-05.
  // Then fall through to `eligible.length === 0` below which returns to step-04.
  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`);
  prd.status = 'failed';
  writeJSON(currentPrdPath, prd);
  // DO NOT reset retries below — skip to eligible search (will find 0 tasks → CHECK_COMPLETION)
}

// RETRY: Reset failed tasks to pending if retries remain (max 3 attempts per task)
// Skipped when isDeadEnd = true (no retriable tasks remain anyway).
if (!isDeadEnd) {
  for (const task of prd.tasks) {
    if (task.status !== 'failed') continue;
    const retryCount = task._retryCount || 0;
    if (retryCount < MAX_RETRIES) {
      task.status = 'pending';
      task._retryCount = retryCount + 1;
      task.error = null;
      console.log(`RETRY [${task.id}] attempt ${retryCount + 1}/${MAX_RETRIES}: ${task.description.substring(0, 60)}`);
    }
  }

  // Unblock tasks whose dependencies are no longer failed (due to retries above)
  for (const task of prd.tasks) {
    if (task.status !== 'blocked') continue;
    const stillBlocked = task.dependencies.some(depId => {
      const dep = prd.tasks.find(t => t.id === depId);
      return dep && (dep.status === 'failed' || dep.status === 'blocked');
    });
    if (!stillBlocked) {
      task.status = 'pending';
      task.error = null;
      console.log(`UNBLOCKED [${task.id}]: dependencies resolved`);
    }
  }

  // Block pending tasks whose dependencies are still failed/blocked
  for (const task of prd.tasks) {
    if (task.status !== 'pending') continue;
    const depsBlocked = task.dependencies.some(depId => {
      const dep = prd.tasks.find(t => t.id === depId);
      return dep && (dep.status === 'failed' || dep.status === 'blocked');
    });
    if (depsBlocked) { task.status = 'blocked'; task.error = 'Blocked by failed dependency'; }
  }

  writeJSON(currentPrdPath, prd);
} // end if (!isDeadEnd)

// Find ALL eligible tasks (dependencies met)
const eligible = prd.tasks.filter(task => {
  if (task.status !== 'pending') return false;
  return task.dependencies.every(depId => {
    const dep = prd.tasks.find(t => t.id === depId);
    return dep && dep.status === 'completed';
  });
});

if (eligible.length === 0) {
  // All done or dead-end — return to step-04 sections 2-4
  // [goto CHECK_COMPLETION]
}

// BATCH: group by category, take first group (max 5)
const firstCategory = eligible[0].category;
const batch = eligible.filter(t => t.category === firstCategory).slice(0, 5);
```

Display:
```
Batch: {batch.length} [{firstCategory}] → {batch.map(t => `[${t.id}] ${t.description.substring(0, 60)}`).join(' | ')}
```

---

## B. Delegate Batch to /apex

> **Ralph NEVER generates code.** All code generation, MCP calls, POST-CHECKs,
> and build verification are handled by apex.

### B1. Mark Batch In-Progress

```javascript
for (const task of batch) {
  task.status = 'in_progress';
  task.started_at = new Date().toISOString();
}
writeJSON(currentPrdPath, prd);
```

### B1b. State Persistence (before delegation)

```javascript
// Write state BEFORE delegating to apex so position survives context compression
writeJSON('.ralph/ralph-state.json', {
  currentStep: 'compact-loop',
  nextStep: 'step-04-check',
  currentModule: {current_module},
  phase: 'apex-delegating',
  iteration: prdCheck?.config?.current_iteration || 'unknown',
  timestamp: new Date().toISOString()
});
```

### B2. Invoke /apex

**INVOKE `/apex -d {currentPrdPath}`**

Apex handles everything for the current module:
- Reads PRD, extracts context (app_name, module_code, entities, sections)
- Executes ALL layers: domain → infrastructure → migration → application → api → seed data → frontend → tests
- Runs full POST-CHECKs (50 checks from `references/post-checks.md`)
- Commits per layer (atomic commits)
- Validates with MCP (`validate_conventions`)
- Updates task statuses in the PRD file directly

> **FLAGS:** `-d` implies `-a` (auto, no user confirmation) and `-e` (economy, no nested teams).

### B2b. MANDATORY CHECKPOINT: Verify prd.json tasks updated

```javascript
// Re-read PRD after apex execution
const updatedPrd = readJSON(currentPrdPath);
const batchIds = batch.map(t => t.id);

// BLOCKING CHECK: Verify apex actually updated task statuses
const stillPending = updatedPrd.tasks.filter(t => batchIds.includes(t.id) && t.status === 'pending').length;
if (stillPending === batchIds.length) {
  console.error(`BLOCKING ERROR: apex did not update any task statuses — all ${stillPending} tasks still pending`);
  console.error('Possible causes: apex crashed, PRD path mismatch, or apex did not receive the correct PRD');
  // Mark tasks as failed so retry logic can handle them
  for (const task of updatedPrd.tasks) {
    if (batchIds.includes(task.id) && task.status === 'pending') {
      task.status = 'failed';
      task.error = 'apex did not update task status';
    }
  }
  writeJSON(currentPrdPath, updatedPrd);
}
```

> **NEVER write progress.txt manually. ALWAYS update prd.json tasks FIRST,
> then derive progress.txt from prd.json state.**

### B3. Verify Post-Apex Results

```javascript
// Re-read PRD after apex execution (use currentPrdPath)
const updatedPrd = readJSON(currentPrdPath);
const batchIds = batch.map(t => t.id);

const completed = updatedPrd.tasks.filter(t => batchIds.includes(t.id) && t.status === 'completed').length;
const failed = updatedPrd.tasks.filter(t => batchIds.includes(t.id) && t.status === 'failed').length;
const pending = updatedPrd.tasks.filter(t => batchIds.includes(t.id) && t.status === 'pending').length;

if (failed > 0) {
  console.log(`WARNING: ${failed} tasks failed during apex execution`);
  // Failed tasks will be retried in next loop iteration if retries remain
}

if (pending > 0) {
  console.log(`INFO: ${pending} tasks still pending — apex may not have reached them`);
}

console.log(`Apex completed: ${completed}/${batchIds.length} tasks`);
```

### B3b. Post-Batch Build Verification (BLOCKING)

> **LESSON LEARNED (audit ba-002):** Without build checks between batches, compilation
> errors from early batches propagate silently through all subsequent batches.
> The final "Build PASS" was never actually verified.

```bash
# Quick build check after each batch — BLOCKING if fails
dotnet build --no-restore --verbosity quiet
if [ $? -ne 0 ]; then
  echo "BLOCKING: Build fails after batch [${firstCategory}]"
  # Inject immediate fix task — will be picked up next iteration
fi
```

```javascript
if (BUILD_FAILED) {
  const maxId = Math.max(...updatedPrd.tasks.map(t => parseInt(t.id.replace(/\D/g,''))||0));
  updatedPrd.tasks.push({
    id: `FIX-${maxId+1}`,
    description: `BLOCKING: Fix build regression after ${firstCategory} batch`,
    status: 'pending', category: 'validation', dependencies: [],
    acceptance_criteria: 'dotnet build passes with 0 errors'
  });
  writeJSON(currentPrdPath, updatedPrd);
  // Continue to section C (commit) then D (loop) — fix task will be picked up next iteration
}
```

---

## C. Commit PRD State

### C1. Update Progress File (MANDATORY)

```javascript
const prdCheck = readJSON(currentPrdPath);
const totalCompleted = prdCheck.tasks.filter(t => t.status === 'completed').length;
const total = prdCheck.tasks.length;

appendFile('.ralph/progress.txt',
  `Iteration ${prdCheck.config.current_iteration}: Delegated to /apex — ` +
  `${completed}/${batchIds.length} batch tasks completed. ${totalCompleted}/${total} overall.\n`
);
```

### C2. Skipped Task Audit

After apex returns, check for newly skipped tasks:

> **LESSON LEARNED (audit ba-002):** Individual skips in critical categories (api, test, domain,
> infrastructure) allowed 4/17 endpoints and 74% stub tests to pass undetected. The old logic
> only blocked when ALL tasks in a category were skipped — individual skips slipped through.

```javascript
const CRITICAL_CATEGORIES = ['api', 'test', 'domain', 'infrastructure'];
const MAX_SKIP_RETRIES = 3;

const newlySkipped = prdCheck.tasks.filter(t =>
  batchIds.includes(t.id) && t.status === 'skipped'
);
if (newlySkipped.length > 0) {
  console.warn(`⚠ ${newlySkipped.length} tasks were SKIPPED by apex:`);
  newlySkipped.forEach(t => console.warn(`  - ${t.id}: ${t.description} [${t.category}]`));

  const skippedCategories = [...new Set(newlySkipped.map(t => t.category))];
  for (const cat of skippedCategories) {
    const isCritical = CRITICAL_CATEGORIES.includes(cat);
    const allInCat = prdCheck.tasks.filter(t => t.category === cat);
    const allSkipped = allInCat.every(t => t.status === 'skipped');

    if (isCritical) {
      // CRITICAL CATEGORY: Block on INDIVIDUAL skips (not just when ALL are skipped)
      const skippedInCat = newlySkipped.filter(t => t.category === cat);
      for (const t of skippedInCat) {
        const retryCount = t._skipRetryCount || 0;
        if (retryCount < MAX_SKIP_RETRIES) {
          console.error(`BLOCKING: Task ${t.id} skipped in critical category "${cat}" — reset to pending (retry ${retryCount + 1}/${MAX_SKIP_RETRIES})`);
          t.status = 'pending';
          t._skipRetryCount = retryCount + 1;
          t._retryCount = (t._retryCount || 0) + 1;
          t.error = `Skipped in critical category "${cat}" — auto-retry ${retryCount + 1}/${MAX_SKIP_RETRIES}`;
        } else {
          console.error(`FAILED: Task ${t.id} skipped ${MAX_SKIP_RETRIES} times in critical category "${cat}" — marking failed`);
          t.status = 'failed';
          t.error = `Skipped ${MAX_SKIP_RETRIES} times in critical category "${cat}" — max retries exhausted`;
        }
      }
    } else {
      // NON-CRITICAL CATEGORY: Block only when ALL tasks in category are skipped (existing behavior)
      if (allSkipped) {
        console.error(`BLOCKING: ALL tasks in category "${cat}" were skipped — investigate root cause`);
        allInCat.forEach(t => {
          t.status = 'pending';
          t._retryCount = (t._retryCount || 0) + 1;
          t.error = `All tasks in category "${cat}" skipped — auto-retry`;
        });
      }
    }
  }
  writeJSON(currentPrdPath, prdCheck);
}
```

### C3. Increment Iteration

```javascript
prdCheck.config.current_iteration++;
prdCheck.updated_at = new Date().toISOString();
writeJSON(currentPrdPath, prdCheck);
```

### C4. Git Commit (PRD state only — apex already committed code)

```bash
git add {currentPrdPath} .ralph/progress.txt
[ -f .ralph/modules-queue.json ] && git add .ralph/modules-queue.json

git commit -m "$(cat <<'EOF'
chore(ralph): update PRD state — iteration {iteration}

Tasks: {completed}/{batch_size} completed via /apex
Overall: {totalCompleted}/{total}

Co-Authored-By: Claude AI <noreply@anthropic.com>
EOF
)"
```

### C5. PRD Sync Verification (HARD CHECK)

> **Note:** `prdCheck` (read in C1) is the authoritative post-apex snapshot.
> `completed` (from B3) was computed on a DIFFERENT read — do NOT mix the two.
> Re-derive the count from `prdCheck` to avoid stale-reference mismatches.

```javascript
// Re-derive completed count from the SAME object used for the commit (prdCheck)
const actuallyCompleted = prdCheck.tasks.filter(t => batchIds.includes(t.id) && t.status === 'completed').length;
const actuallyFailed    = prdCheck.tasks.filter(t => batchIds.includes(t.id) && t.status === 'failed').length;

if (actuallyCompleted !== completed) {
  console.warn(`PRD SYNC WARNING: B3 reported ${completed} completed, prdCheck shows ${actuallyCompleted}`);
  console.warn('Using prdCheck as source of truth (apex may have written between reads)');
  // DO NOT overwrite prdCheck — apex is the source of truth for task statuses
  // Update local `completed` variable for progress reporting consistency
  completed = actuallyCompleted;
}
```

---

## D. Loop Back (MANDATORY — NO PAUSE)

**IMMEDIATELY return to step-04 section 1 (Read Current State).**

DO NOT stop. DO NOT wait for user. DO NOT ask "should I continue?". DO NOT re-read step files.
DO NOT display a summary and pause. DO NOT output partial results and wait.
**Go directly to step-04 section 1. NOW.**

The ONLY exit conditions (enforced by step-04 sections 2-4):
- ALL tasks complete (all modules if multi-module) → step-05
- Max iterations reached → step-05
- Dead-end (all remaining blocked/failed) → step-05
- User Ctrl+C (external interrupt)
