---
name: step-01-task
description: Load or create tasks from prd.json (v3 unified or v2 legacy)
next_step: steps/step-02-execute.md
---

# Step 1: Load Task

> **MODULE TRANSITION CHECK:** Before "Only Read Once" rule, check for module transition:

```javascript
if (fileExists('.ralph/module-changed.json')) {
  const t = readJSON('.ralph/module-changed.json');
  console.log(`Module transition: ${t.fromModule} → ${t.toModule}`);
  deleteFile('.ralph/module-changed.json');
  // PROCEED (exception to "Only Read Once" rule)
}
```

> **CONTEXT OPTIMIZATION:** Read ONCE per module (first iteration).
> Iterations > 1 within same module → use COMPACT LOOP in step-04.
> Re-read ONLY on module transition (module-changed.json detected above).

## YOUR TASK:

Load task from prd.json or create initial task breakdown with categories, dependencies, and acceptance criteria.

---

## 0. Multi-Module Queue Check

```javascript
const queuePath = '.ralph/modules-queue.json';
if (fileExists(queuePath)) {
  const queue = readJSON(queuePath);
  const currentModule = queue.modules[queue.currentIndex];
  console.log(`Multi-module: ${currentModule.code} (${queue.currentIndex + 1}/${queue.totalModules})`);

  {modules_queue} = queue;
  {current_module} = currentModule.code;

  // Verify PRD matches current module
  const currentPrd = readJSON('.ralph/prd.json');
  if (currentPrd.metadata?.moduleCode !== currentModule.code) {
    const modulePrd = readJSON(currentModule.prdFile);

    // v3 FAST PATH: unified PRD from ss derive-prd (tasks pre-computed)
    if (modulePrd.$version === '3.0.0') {
      // Inject runtime fields and use directly
      modulePrd.status = 'in_progress';
      modulePrd.feature = `${modulePrd.project?.module || currentModule.code} (${modulePrd.project?.application || 'app'})`;
      modulePrd.created = modulePrd.created || new Date().toISOString();
      modulePrd.updated_at = new Date().toISOString();
      modulePrd.config = modulePrd.config || { max_iterations: {max_iterations}, completion_promise: "{completion_promise}", current_iteration: 1 };
      modulePrd.metadata.branch = modulePrd.metadata.branch || getCurrentBranch();
      modulePrd.metadata.project_path = modulePrd.metadata.project_path || process.cwd();
      modulePrd.metadata.mcp_servers = modulePrd.metadata.mcp_servers || { smartstack: true, context7: true };
      modulePrd.history = modulePrd.history || [];
      writeJSON('.ralph/prd.json', modulePrd);
    }
    // LEGACY: FORMAT A (no $version) — transform + warning
    else if (modulePrd.project && modulePrd.requirements && !modulePrd.$version) {
      console.warn('⚠ DEPRECATED: FORMAT A prd.json detected. Re-run `ss derive-prd` to generate v3 format.');
      writeJSON('.ralph/prd.json', transformPrdJsonToRalphV2(modulePrd, currentModule.code));
    }
    // v2 legacy or already runtime format
    else {
      writeJSON('.ralph/prd.json', modulePrd);
    }
  }
} else {
  {modules_queue} = null;
  {current_module} = null;
}
```

## PrdJson → Ralph v2 Transformation (DEPRECATED)

> **DEPRECATED:** This transformation is a fallback for old FORMAT A PRDs (pre-v3.9.0).
> New PRDs generated by `ss derive-prd` use unified v3 format with pre-computed tasks.
> This function will be removed in a future release.

See [references/task-transform-legacy.md](../references/task-transform-legacy.md) for the full `transformPrdJsonToRalphV2()` function including:
- Layer-ordered task generation (domain → infra → app → api → frontend → seedData → tests)
- Consolidated frontend + i18n tasks
- Guardrails 1d-1j (inject missing infrastructure, API, frontend, tests, seed data, migration)

---

## 1. Check for Existing prd.json

If `.ralph/prd.json` exists:
1. Read the PRD
2. **v3 FAST PATH:** If `$version === "3.0.0"` AND `tasks[]` exists:
   - Inject runtime fields if missing (status, config, feature, created, updated_at, history)
   - Write back to `.ralph/prd.json`
   - **Run CATEGORY COMPLETENESS CHECK (section 4b) before proceeding**
   - Skip directly to section 5 (find next task)
3. **v2 legacy:** If `$version === "2.0.0"` → find next eligible task (section 5), skip to section 5
4. **FORMAT A (deprecated):** If `.project && .requirements && !.$version` → run `transformPrdJsonToRalphV2()` → section 5

If `.ralph/prd.json` does not exist: continue to section 1b.

### 1b. BA Handoff Quality Gate (BLOCKING)

> **CRITICAL:** Before generating ANY tasks, verify the BA handoff is complete.
> A missing handoff means no frontend files, no test plan, no BR-to-code mapping.
> Generating tasks without handoff produces an INCOMPLETE PRD (backend-only).

```javascript
// Find the source feature.json for the current module
const sourceFeatureJson = prd?.metadata?.sourceFeatureJson
  || findFile('docs/business/**/business-analyse/**/feature.json');

if (sourceFeatureJson) {
  const feature = readJSON(sourceFeatureJson);
  const handoff = feature.handoff || {};

  // CHECK 1: Handoff status must be "handed-off"
  if (handoff.status !== 'handed-off') {
    console.error(`
╔══════════════════════════════════════════════════════════════╗
║  BLOCKING: BA HANDOFF INCOMPLETE                            ║
║                                                              ║
║  feature.json handoff.status = "${handoff.status || 'missing'}"
║  Expected: "handed-off"                                      ║
║                                                              ║
║  The /business-analyse did not complete steps 04a→05c.       ║
║  Without handoff, the PRD will be MISSING:                   ║
║    - Frontend tasks (pages, components, routes)              ║
║    - Test tasks (unit, integration, security)                ║
║    - BR-to-code mappings                                     ║
║    - API endpoint summary                                    ║
║                                                              ║
║  ACTION: Run /business-analyse to complete the handoff,      ║
║  then re-run /ralph-loop.                                    ║
╚══════════════════════════════════════════════════════════════╝`);
    STOP;
  }

  // CHECK 2: filesToCreate must have all 7 categories
  const filesToCreate = handoff.filesToCreate || {};
  const requiredCategories = ['domain', 'application', 'infrastructure', 'api', 'frontend', 'seedData', 'tests'];
  const missingInHandoff = requiredCategories.filter(c => !filesToCreate[c] || filesToCreate[c].length === 0);

  if (missingInHandoff.length > 0) {
    console.warn(`⚠ Handoff has empty categories: ${missingInHandoff.join(', ')}`);
    console.warn('PRD generation may produce incomplete tasks. Consider re-running /business-analyse step-05a.');
    // WARNING only — allow proceeding if handoff.status is correct
  }
}
```

## 2. Analyze Task Description

Check for BA handoff source (priority):
1. Per-module PRD files (handled by section 0)
2. Single prd.json already present
3. Markdown handoff: `find . -path "*development-handoff*" -name "*.md"`
4. Direct task from `{task_description}`

Generate **3-30 subtasks** by category (domain → infrastructure → application → api → frontend → i18n → test → validation).

## 3. Create prd.json

Write `.ralph/prd.json` with all tasks (every task MUST have: id, description, status, category, dependencies, acceptance_criteria, started_at, completed_at, iteration, commit_hash, files_changed, validation, error).

Initialize `.ralph/progress.txt`:
```markdown
# Ralph Loop Progress
## Task: {task_description}
## Started: {timestamp}
## Schema: v3.0.0
```

## 4. Validate Task Integrity

- All tasks have required fields
- Dependency references are valid (exist, no forward/circular)
- Task count 3-30

### 4b. Category Completeness Check (BLOCKING)

> **CRITICAL:** The PRD MUST contain tasks for ALL required categories.
> A PRD with only backend categories (domain, infrastructure, application, api) is INCOMPLETE.
> This check prevents the "no frontend generated" failure mode.

```javascript
// CRITICAL: seedData MUST be in the required list — without it, nav_Applications/auth_Roles/auth_Permissions
// are not seeded, and frontend/test tasks execute against an empty database.
const REQUIRED_CATEGORIES = ['domain', 'infrastructure', 'application', 'api', 'seedData', 'frontend', 'test'];
const presentCategories = new Set(prd.tasks.map(t => t.category));
const missingCategories = REQUIRED_CATEGORIES.filter(c => !presentCategories.has(c));

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

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

  // DEPENDENCY CHAIN: domain → infrastructure → application → api → seedData → frontend → test
  // Frontend MUST depend on seedData (not just API) — otherwise navigation/permissions are empty
  const frontendDep = lastSeedDataTask || lastApiTask;

  for (const cat of missingCategories) {
    maxIdNum++;
    const taskId = `${prefix}${String(maxIdNum).padStart(3, '0')}`;

    // Determine dependencies based on category order
    let deps;
    if (cat === 'seedData') {
      deps = [lastApiTask]; // seedData depends on API (infrastructure must exist first)
    } else if (cat === 'frontend') {
      deps = [frontendDep]; // frontend depends on seedData (navigation must be seeded)
    } else if (cat === 'test') {
      deps = [frontendDep]; // test depends on seedData too
    } else {
      deps = [];
    }

    const guardrailTask = {
      id: taskId,
      description: `[GUARDRAIL] Generate ${cat} layer for ${prd.project?.module || 'module'}`,
      status: 'pending',
      category: cat,
      dependencies: deps,
      acceptance_criteria: [
        cat === 'seedData' ? 'NavigationApplicationSeedData + NavigationModuleSeedData + PermissionsSeedData + ApplicationRolesSeedData created, IClientSeedDataProvider wired, startup test passes' :
        cat === 'frontend' ? 'React pages created via MCP scaffold_api_client + scaffold_routes, wired to App.tsx, navigation seed data verified in DB' :
        cat === 'test' ? 'Unit + Integration test projects created, scaffold_tests MCP called, dotnet test passes' :
        `${cat} layer fully implemented per category-rules.md`
      ],
      started_at: null, completed_at: null, iteration: null,
      commit_hash: null, files_changed: [], validation: null, error: null
    };
    prd.tasks.push(guardrailTask);
    console.log(`  → Injected guardrail: [${taskId}] ${cat}`);

    // Update frontendDep if we just injected seedData (frontend guardrail should depend on it)
    if (cat === 'seedData') {
      // Re-resolve for subsequent iterations
      const updatedFrontendDep = taskId;
      // Patch any previously-added frontend guardrail to depend on this seedData task
      const frontendGuard = prd.tasks.find(t => t.category === 'frontend' && t.description.includes('[GUARDRAIL]'));
      if (frontendGuard && !frontendGuard.dependencies.includes(taskId)) {
        frontendGuard.dependencies = [taskId];
      }
    }
  }

  writeJSON('.ralph/prd.json', prd);
  console.log(`PRD updated: ${prd.tasks.length} tasks (${missingCategories.length} guardrails added)`);
}
```

**Why this matters:** In test-v4-005, the PRD was generated with only backend categories (domain, infrastructure, application, api, seedData, validation). The frontend and test categories were entirely absent, resulting in 0 frontend pages and 0 tests generated across 3 modules.

## 5. Find Current Task

```javascript
function findNextTask(tasks) {
  for (const task of tasks) {
    if (task.status !== 'pending') continue;
    const depsBlocked = task.dependencies.some(d => {
      const dep = tasks.find(t => t.id === d);
      return dep && (dep.status === 'failed' || dep.status === 'blocked');
    });
    if (depsBlocked) { task.status = 'blocked'; task.error = 'Blocked by failed dependency'; continue; }
    const depsOk = task.dependencies.every(d => tasks.find(t => t.id === d)?.status === 'completed');
    if (depsOk) return task;
  }
  return null;
}
```

If no eligible task: all completed → step-05, all blocked → step-05, else STOP.

## 6. Show Task Status

```
[{iteration}/{max}] {completed}/{total} done | Task [{id}] {description} ({category})
-> Executing...
```

---

## NEXT STEP:

After task loaded, proceed to `./step-02-execute.md`
