---
name: step-02-execute
description: Execute task(s) with dependency checks and rich tracking
next_step: steps/step-03-commit.md
---

# Step 2: Execute Task

> **CONTEXT OPTIMIZATION:** Read ONCE (first iteration). Subsequent iterations use compact-loop.md.

## YOUR TASK:

Execute the current task from prd.json. First iteration = ONE task. After that, compact loop handles batches.

**ULTRA THINK about the implementation.**

---

## EXECUTION RULES:

1. **ATOMIC CHANGES** — Complete and working
2. **USE MCP** — Validate with SmartStack MCP (once per batch)
3. **TRACK FILES** — Record every file created or modified
4. **CHECK DEPENDENCIES** — Verify before starting
5. **BATCH ALLOWED** — Same category, max 5 tasks

---

## EXECUTION SEQUENCE:

### 1. Verify Dependencies

```javascript
const prd = readJSON('.ralph/prd.json');
const task = prd.tasks.find(t => t.id === {current_task_id});
for (const depId of task.dependencies) {
  const dep = prd.tasks.find(t => t.id === depId);
  if (!dep || dep.status !== 'completed') {
    task.status = 'blocked'; task.error = `Dependency ${depId} not completed`;
    writeJSON('.ralph/prd.json', prd);
    STOP — return to step-01
  }
}
```

### 2. Mark In-Progress

```javascript
task.status = 'in_progress';
task.started_at = new Date().toISOString();
writeJSON('.ralph/prd.json', prd);
```

### 3. Understand and Execute

**ULTRA THINK:** What needs to be done? What files? What acceptance criteria? What category?

**Read `references/category-rules.md` for category-specific execution rules.**

Key category triggers:

| Category | Special Action |
|----------|---------------|
| `infrastructure` with `_migrationMeta` | Migration sequence: MCP suggest → add → update → build |
| `infrastructure` with seed data keywords | **MANDATORY**: Read `references/core-seed-data.md` |
| `frontend` | MCP-first: scaffold_api_client → scaffold_routes → `/ui-components` skill (pages) → form pages (create/edit = full pages, ZERO modals) → form tests → i18n (4 langues) |
| `test` | Generate via MCP → run → fix loop → 100% pass required |
| `validation` | Clean build → full test suite → MCP validate |

### 4. Implement

- Create/modify files following SmartStack conventions
- Track: `{files_created} = []`, `{files_modified} = []`
- Use MCP tools per category (see category-rules.md)

### 5. Post-Infrastructure Build Gate (BLOCKING)

**After completing ALL infrastructure tasks (configs + migration + seed data):**

```bash
dotnet build --no-restore
```

MUST pass before proceeding to application/api/test/frontend. Fix if fails.

**POST-CHECK: Route kebab-case validation (BLOCKING)**

After generating NavigationSeedData files, verify routes follow kebab-case convention:

```bash
# Search for routes with uppercase in NavigationSeedData files
UPPERCASE_ROUTES=$(grep -E 'Route = "?/[^"]*[A-Z]' Infrastructure/Persistence/Seeding/Data/*/NavigationSeedData.cs 2>/dev/null)

if [ -n "$UPPERCASE_ROUTES" ]; then
  echo "❌ ERROR: Routes must be kebab-case lowercase. Found PascalCase in NavigationSeedData routes:"
  echo "$UPPERCASE_ROUTES"
  echo ""
  echo "Expected format: /business/human-resources/projects"
  echo "Fix: Use ToKebabCase() helper in route generation (see core-seed-data.md)"
  exit 1
fi

# Search for multi-word route segments missing hyphens (e.g., "humanresources" instead of "human-resources")
MULTI_WORD_ROUTES=$(grep -oP 'Route\s*=\s*"([^"]+)"' Infrastructure/Persistence/Seeding/Data/*/NavigationSeedData.cs 2>/dev/null | grep -P '"[^"]*(?:human(?:resources|resource)|time(?:management|tracking)|project(?:management|member)|leave(?:balance|request))[^"]*"')

if [ -n "$MULTI_WORD_ROUTES" ]; then
  echo "❌ BLOCKING: Route segments contain concatenated multi-word names without hyphens"
  echo "Found: $MULTI_WORD_ROUTES"
  echo ""
  echo "Convention: multi-word segments MUST use kebab-case"
  echo "  humanresources → human-resources"
  echo "  timemanagement → time-management"
  echo "Fix: Apply ToKebabCase() to all route segments in NavigationSeedData"
  exit 1
fi
```

**Why this matters:**
- Backend seed data defines menu navigation routes
- Frontend React Router expects kebab-case URLs
- Mismatch causes 404 on menu click
- Convention: `HumanResources` (C# code) → `human-resources` (web URL)

**Fix:** If check fails, routes were generated without `ToKebabCase()` transformation. Regenerate NavigationSeedData with the helper.

**POST-CHECK: Core Seed Data Integrity (BLOCKING)**

After generating ALL seed data files (NavigationApplicationSeedData, NavigationModuleSeedData, PermissionsSeedData, RolesSeedData, ApplicationRolesSeedData, IClientSeedDataProvider), verify the complete chain:

```bash
# 1. NavigationApplicationSeedData.cs exists
APP_SEED=$(find . -path "*/Seeding/Data/NavigationApplicationSeedData.cs" 2>/dev/null | head -1)
if [ -z "$APP_SEED" ]; then
  echo "❌ BLOCKING: NavigationApplicationSeedData.cs NOT FOUND"
  echo "Without this, nav_Applications is empty → navigation menu invisible"
  echo "Fix: Generate from core-seed-data.md section 1b using seedDataCore.navigationApplications"
  exit 1
fi

# 2. ApplicationRolesSeedData.cs references NavigationApplicationSeedData.ApplicationId (no placeholder)
if grep -q '{ApplicationGuid}' Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs 2>/dev/null; then
  echo "❌ BLOCKING: ApplicationRolesSeedData still has {ApplicationGuid} placeholder"
  echo "Fix: Replace with NavigationApplicationSeedData.ApplicationId"
  exit 1
fi

# 3. IClientSeedDataProvider has no hardcoded app placeholders
PROVIDER=$(find . -path "*/Seeding/*SeedDataProvider.cs" 2>/dev/null | head -1)
if [ -n "$PROVIDER" ]; then
  if grep -qE '\{appLabel_|appDesc_|appIcon\}' "$PROVIDER" 2>/dev/null; then
    echo "❌ BLOCKING: SeedDataProvider has hardcoded {appLabel_xx}/{appIcon} placeholders"
    echo "Fix: Use NavigationApplicationSeedData.GetApplicationEntry() and GetTranslationEntries()"
    exit 1
  fi
fi

# 4. Quick startup test — verify seed data doesn't crash at runtime
echo "Running startup test to verify seed data..."
dotnet run --project "$API_PROJECT" --urls "http://localhost:0" -- --environment Development &
RUN_PID=$!
sleep 8

if ! kill -0 $RUN_PID 2>/dev/null; then
  echo "❌ BLOCKING: Application crashed during startup (seed data likely failed)"
  echo "Check logs for: navigation, role, or permission seed data errors"
  wait $RUN_PID 2>/dev/null
  exit 1
else
  echo "✓ Startup test passed — seed data executed without crash"
  kill $RUN_PID 2>/dev/null
  wait $RUN_PID 2>/dev/null
fi
```

**Why this matters:**
- If `nav_Applications` is empty → navigation menu shows nothing → user can't access modules
- If `auth_Roles` is empty → role-permission mappings fail silently → RBAC broken
- If `auth_Permissions` is empty → all authorization checks reject → 403 on every endpoint
- These are **foundational** — without them, ALL subsequent features (frontend, API, tests) are useless

**POST-CHECK: Section route/permission completeness (BLOCKING — when sections defined)**

After generating section seed data, verify completeness:

```bash
# 1. Verify every NavigationSection seed route has a frontend route match
SECTION_SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" -exec grep -l 'GetSectionEntries' {} \; 2>/dev/null)
if [ -n "$SECTION_SEED_FILES" ]; then
  echo "Sections defined — verifying frontend route matches..."
  SECTION_ROUTES=$(grep -ohP 'Route\s*=\s*ToKebabCase\(\$?"([^"]+)"\)' $SECTION_SEED_FILES 2>/dev/null | grep -i section)
  APP_TSX="$WEB_SRC/App.tsx"
  if [ -n "$APP_TSX" ] && [ -n "$SECTION_ROUTES" ]; then
    while IFS= read -r ROUTE; do
      ROUTE_PATH=$(echo "$ROUTE" | grep -oP '"([^"]+)"' | tr -d '"')
      if [ -n "$ROUTE_PATH" ] && ! grep -q "$ROUTE_PATH" "$APP_TSX" 2>/dev/null; then
        echo "WARNING: Section route '$ROUTE_PATH' not found in App.tsx — add frontend route"
      fi
    done <<< "$SECTION_ROUTES"
  fi

  # 2. Verify section-level permissions exist when sections are defined
  PERM_SEED=$(find . -path "*/Seeding/Data/*" -name "PermissionsSeedData.cs" 2>/dev/null)
  if [ -n "$PERM_SEED" ]; then
    for PS in $PERM_SEED; do
      HAS_SECTION_PERMS=$(grep -c 'sectionCode\|SectionPascal\|Level = PermissionLevel.Section' "$PS" 2>/dev/null)
      if [ "$HAS_SECTION_PERMS" -eq 0 ]; then
        echo "BLOCKING: Sections defined but PermissionsSeedData missing section-level permissions: $PS"
        echo "Fix: Add section-level permission GUIDs and entries per core-seed-data.md Step C2"
        exit 1
      fi
    done
  fi

  # 3. Verify section-level role mappings exist
  ROLE_SEED=$(find . -path "*/Seeding/Data/*" -name "RolesSeedData.cs" 2>/dev/null)
  if [ -n "$ROLE_SEED" ]; then
    for RS in $ROLE_SEED; do
      HAS_SECTION_ROLES=$(grep -c 'sectionCode' "$RS" 2>/dev/null)
      if [ "$HAS_SECTION_ROLES" -eq 0 ]; then
        echo "BLOCKING: Sections defined but RolesSeedData missing section-level role mappings: $RS"
        echo "Fix: Add section-level role-permission entries per core-seed-data.md section 5"
        exit 1
      fi
    done
  fi
fi
```

**POST-CHECK: Navigation section route CRUD suffix detection (BLOCKING)**

After generating NavigationSeedData files with sections, verify no route contains `/list` or `/detail` suffixes:

```bash
# Detect CRUD suffixes in navigation section routes
CRUD_ROUTES=$(grep -rn 'Route.*=.*\/list\b\|Route.*=.*\/detail' Infrastructure/Persistence/Seeding/Data/ 2>/dev/null | grep -v '//.*Route')

if [ -n "$CRUD_ROUTES" ]; then
  echo "BLOCKING: Navigation section routes contain CRUD suffixes"
  echo "Convention:"
  echo "  - 'list' section route = module route (NO /list suffix)"
  echo "  - 'detail' section route = module route + /:id (NOT /detail/:id)"
  echo "  - Other sections = module route + /{section-kebab}"
  echo ""
  echo "Found:"
  echo "$CRUD_ROUTES"
  echo ""
  echo "Fix: Remove /list suffix (use module route), replace /detail/:id with /:id"
  exit 1
fi
```

**Why this matters:**
- React Router defines NO `/list` child route — the module route IS the list view
- React Router uses `/:id` for detail views, NOT `/detail/:id`
- Mismatch causes 404 on navigation menu click

**POST-CHECK: Permission path segment count (WARNING — when permissions defined)**

```bash
PERM_FILES=$(find Infrastructure/Persistence/Seeding/Data/ -name "PermissionsSeedData.cs" 2>/dev/null)
if [ -n "$PERM_FILES" ]; then
  MALFORMED=$(grep -oP 'Path\s*=\s*"([^"]+)"' $PERM_FILES | grep -oP '"[^"]+"' | tr -d '"' | while read -r path; do
    DOTS=$(echo "$path" | tr -cd '.' | wc -c)
    if echo "$path" | grep -qP '\.\*$'; then continue; fi
    if [ "$DOTS" -lt 3 ] || [ "$DOTS" -gt 5 ]; then
      echo "  Unexpected segment count ($((DOTS+1))): $path"
    fi
  done)
  if [ -n "$MALFORMED" ]; then
    echo "WARNING: Permission paths with unexpected segment count:"
    echo "$MALFORMED"
    echo "  Expected: 4 segments (module-level) or 5 segments (section-level)"
  fi
fi
```

**POST-CHECK: DefaultTenantId cross-schema FK validation (WARNING)**

After generating SeedConstants.cs and DevDataSeeder, verify that `DefaultTenantId` is not a phantom GUID that doesn't exist in `core.tenant_Tenants`:

```bash
# Find SeedConstants.cs
SEED_CONSTANTS=$(find . -path "*/SeedConstants.cs" 2>/dev/null | head -1)
if [ -n "$SEED_CONSTANTS" ]; then
  # Extract the DefaultTenantId GUID value
  TENANT_GUID=$(grep -oP 'DefaultTenantId\s*=\s*Guid\.Parse\("([^"]+)"\)' "$SEED_CONSTANTS" | grep -oP '"[^"]+"' | tr -d '"')

  if [ -z "$TENANT_GUID" ]; then
    echo "⚠️ WARNING: SeedConstants.cs found but DefaultTenantId not defined"
    echo "DevDataSeeder requires SeedConstants.DefaultTenantId for business seed data"
    echo "Fix: Add 'public static readonly Guid DefaultTenantId = Guid.Parse(\"...\");' to SeedConstants.cs"
  else
    # Verify the GUID is referenced somewhere in platform config (not an invented value)
    GUID_FOUND=$(grep -r "$TENANT_GUID" appsettings*.json src/ 2>/dev/null | grep -v SeedConstants)

    if [ -z "$GUID_FOUND" ]; then
      echo "⚠️ WARNING: SeedConstants.DefaultTenantId ($TENANT_GUID) not found in platform config"
      echo "This GUID must exist in core.tenant_Tenants at runtime"
      echo "Verify: The SmartStack platform seeds this tenant during InitializeSmartStackAsync()"
      echo "If using a custom TenantId, ensure it's created before DevDataSeeder runs (Order >= 200)"
      echo "validate-feature step-05 will verify this FK exists in real database"
    else
      echo "✓ SeedConstants.DefaultTenantId ($TENANT_GUID) found in platform config"
    fi
  fi
fi
```

**Why this matters:**
- If `DefaultTenantId` references a GUID not in `core.tenant_Tenants`, DevDataSeeder inserts orphan rows
- Business entities with invalid `TenantId` FK cause runtime 500 errors
- The tenant must be seeded by `InitializeSmartStackAsync()` BEFORE DevDataSeeder (Order >= 200) runs

### 6. Validate with MCP

```
mcp__smartstack__validate_conventions: checks: ["all"]

# Category-specific:
infrastructure → mcp__smartstack__check_migrations
api            → mcp__smartstack__validate_security
frontend       → mcp__smartstack__validate_frontend_routes
test           → mcp__smartstack__analyze_test_coverage
```

If validation fails: fix → re-validate → do NOT proceed until clean.

### 7. Check Acceptance Criteria

Verify against `{current_task_criteria}`. If not met: continue working. If impossible: document in `task.error`.

### 8. Build & Test Verification (BLOCKING)

**Backend — EVERY task (domain, infra, application, api):**
```bash
dotnet build --no-restore
```
If FAIL → read error output → identify file:line → fix code → rebuild → loop until exit code 0.

**Test category tasks — MANDATORY execution:**
```bash
TEST_PROJECT="tests/$(basename $(pwd)).Tests.Unit"
dotnet build "$TEST_PROJECT" --no-restore
dotnet test "$TEST_PROJECT" --no-build --verbosity normal
```
If FAIL → analyze failing test names → fix SOURCE CODE (not tests) → rebuild → retest.
**Loop until 100% pass — NEVER proceed with failing tests.**

**Frontend tasks — MANDATORY verification:**
```bash
npm run typecheck    # TypeScript compilation — MUST exit 0
npm run lint         # ESLint — MUST exit 0
```
If FAIL → read errors → fix TSX/TS files → re-run → loop until pass.

**Frontend POST-CHECKs (BLOCKING for frontend category):**

> **Path resolution:** Web projects may live in subdirectories (e.g., `web/app-web/src/pages/`).
> All POST-CHECKs below use dynamic path discovery instead of hardcoded `src/pages/`.

```bash
# Resolve frontend directories dynamically (handles web/ prefix and varied project structures)
PAGE_DIR=$(find . -path "*/src/pages" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
HOOK_DIR=$(find . -path "*/src/hooks" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
COMP_DIR=$(find . -path "*/src/components" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
WEB_SRC=$(find . -name "App.tsx" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null | head -1)
# All POST-CHECKs below use $PAGE_DIR, $HOOK_DIR, $COMP_DIR, $WEB_SRC instead of hardcoded paths
```

```bash
# POST-CHECK: Forms must be full pages — ZERO modals/popups/drawers
PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" 2>/dev/null)
if [ -n "$PAGE_FILES" ]; then
  MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet)" $PAGE_FILES 2>/dev/null)
  if [ -n "$MODAL_IMPORTS" ]; then
    echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup components"
    echo "Create/Edit forms MUST be full pages with own URL routes (/create, /:id/edit)"
    echo "$MODAL_IMPORTS"
    exit 1
  fi
fi

# POST-CHECK: No raw HTML <table> in page files — MUST use SmartTable/DataTable (BLOCKING)
PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" 2>/dev/null)
if [ -n "$PAGE_FILES" ]; then
  RAW_TABLES=$(grep -Pn '<table[\s>]|<thead[\s>]|<tbody[\s>]' $PAGE_FILES 2>/dev/null)
  if [ -n "$RAW_TABLES" ]; then
    echo "BLOCKING: Raw HTML <table> detected in page files — MUST use SmartTable or DataTable"
    echo "SmartStack convention: Lists MUST use SmartTable + SmartFilter (see /ui-components skill)"
    echo "Import: import { DataTable } from '@/components/ui/DataTable'"
    echo ""
    echo "Found:"
    echo "$RAW_TABLES"
    exit 1
  fi
fi

# POST-CHECK: No hardcoded Tailwind colors — MUST use CSS variables (BLOCKING)
PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" -o -name "*.tsx" 2>/dev/null | sort -u)
COMPONENT_FILES=$(find "$COMP_DIR" -name "*.tsx" 2>/dev/null)
ALL_TSX="$PAGE_FILES $COMPONENT_FILES"
if [ -n "$ALL_TSX" ]; then
  # Match bg-{color}-{shade} or text-{color}-{shade} patterns (excluding bg-white, bg-black, bg-transparent, bg-current)
  HARDCODED_COLORS=$(grep -Pn '(?:bg|text|border|ring|from|to|via)-(?:red|blue|green|yellow|orange|purple|pink|indigo|teal|cyan|emerald|violet|fuchsia|rose|amber|lime|sky|slate|gray|zinc|neutral|stone)-\d{2,3}' $ALL_TSX 2>/dev/null | head -20)
  if [ -n "$HARDCODED_COLORS" ]; then
    echo "BLOCKING: Hardcoded Tailwind colors detected — MUST use CSS variables"
    echo "SmartStack convention: ALL colors via CSS variables for theming support"
    echo "  bg-blue-600    → bg-[var(--color-accent-600)]"
    echo "  text-red-500   → text-[var(--error-text)]"
    echo "  bg-green-100   → bg-[var(--success-bg)]"
    echo "  bg-gray-50     → bg-[var(--bg-secondary)]"
    echo ""
    echo "Found:"
    echo "$HARDCODED_COLORS"
    exit 1
  fi
fi

# POST-CHECK: List pages must import SmartTable/DataTable OR EntityCard (WARNING)
LIST_PAGES=$(find "$PAGE_DIR" -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
if [ -n "$LIST_PAGES" ]; then
  for LP in $LIST_PAGES; do
    HAS_TABLE=$(grep -c 'SmartTable\|DataTable' "$LP" 2>/dev/null)
    HAS_CARD=$(grep -c 'EntityCard' "$LP" 2>/dev/null)
    if [ "$HAS_TABLE" -eq 0 ] && [ "$HAS_CARD" -eq 0 ]; then
      echo "WARNING: List page missing SmartTable/DataTable or EntityCard: $LP"
      echo "List pages MUST use SmartStack UI components from /ui-components skill"
      echo "  Tables: import { DataTable } from '@/components/ui/DataTable'"
      echo "  Cards:  import { EntityCard } from '@/components/ui/EntityCard'"
    fi
  done
fi

# POST-CHECK: No window.confirm() — MUST use confirmation component (BLOCKING)
PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" 2>/dev/null)
if [ -n "$PAGE_FILES" ]; then
  BAD_CONFIRM=$(grep -Pn 'window\.confirm\s*\(|(?<!\w)confirm\s*\(' $PAGE_FILES 2>/dev/null | grep -v '//' | grep -v test)
  if [ -n "$BAD_CONFIRM" ]; then
    echo "BLOCKING: window.confirm() detected — MUST use a SmartStack confirmation component"
    echo "Native browser dialogs break theming and i18n"
    echo "Use a ConfirmDialog component with translated messages"
    echo ""
    echo "Found:"
    echo "$BAD_CONFIRM"
    exit 1
  fi
fi

# POST-CHECK: Pages must handle loading, error, and empty states (WARNING)
PAGE_FILES=$(find "$PAGE_DIR" -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
if [ -n "$PAGE_FILES" ]; then
  for LP in $PAGE_FILES; do
    HAS_LOADING=$(grep -c 'loading\|isLoading\|skeleton\|Skeleton\|PageLoader\|spinner' "$LP" 2>/dev/null)
    HAS_ERROR=$(grep -c 'error\|Error' "$LP" 2>/dev/null)
    HAS_EMPTY=$(grep -c 'empty\|Empty\|no.*found\|length === 0' "$LP" 2>/dev/null)
    MISSING=""
    [ "$HAS_LOADING" -eq 0 ] && MISSING="${MISSING}loading "
    [ "$HAS_ERROR" -eq 0 ] && MISSING="${MISSING}error "
    [ "$HAS_EMPTY" -eq 0 ] && MISSING="${MISSING}empty "
    if [ -n "$MISSING" ]; then
      echo "WARNING: List page missing state handling ($MISSING): $LP"
      echo "All list pages MUST handle: loading skeleton, error with retry, empty state"
    fi
  done
fi

# POST-CHECK: Create/Edit pages must exist for each module with a list page
LIST_PAGES=$(find "$PAGE_DIR" -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
if [ -n "$LIST_PAGES" ]; then
  for LP in $LIST_PAGES; do
    DIR=$(dirname "$LP"); MOD=$(basename "$DIR")
    if [ -z "$(find "$DIR" -name "*CreatePage.tsx" 2>/dev/null)" ]; then
      echo "WARNING: Module $MOD has list page but no CreatePage"
    fi
    if [ -z "$(find "$DIR" -name "*EditPage.tsx" 2>/dev/null)" ]; then
      echo "WARNING: Module $MOD has list page but no EditPage"
    fi
  done
fi

# First check: at least one test file must exist in pages/
PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
if [ -n "$PAGE_FILES" ]; then
  TEST_FILES=$(find "$PAGE_DIR" -name "*.test.tsx" 2>/dev/null)
  if [ -z "$TEST_FILES" ]; then
    echo "BLOCKING: No frontend test files found in src/pages/"
    echo "Every module with pages MUST have at least one .test.tsx file"
    exit 1
  fi
fi

# POST-CHECK: Form pages must have companion test files
FORM_PAGES=$(find "$PAGE_DIR" -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
if [ -n "$FORM_PAGES" ]; then
  for FP in $FORM_PAGES; do
    TEST="${FP%.tsx}.test.tsx"
    if [ ! -f "$TEST" ]; then
      echo "BLOCKING: Form page missing test file: $FP → expected $TEST"
      exit 1
    fi
  done
fi

# POST-CHECK: FK fields must NOT be plain text inputs — use EntityLookup
FORM_PAGES=$(find "$PAGE_DIR" -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test | grep -v node_modules)
if [ -n "$FORM_PAGES" ]; then
  # Match any input with name ending in "Id" (except hidden inputs)
  FK_TEXT_INPUTS=$(grep -Pn '<input[^>]*name=["\x27][a-zA-Z]*Id["\x27]' $FORM_PAGES 2>/dev/null | grep -v 'type=["\x27]hidden["\x27]')
  if [ -n "$FK_TEXT_INPUTS" ]; then
    echo "BLOCKING: FK Guid fields rendered as plain text inputs — MUST use EntityLookup"
    echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
    echo "$FK_TEXT_INPUTS"
    exit 1
  fi
  FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*[Ee]nter.*[Ii][Dd]|placeholder=["\x27].*[Gg][Uu][Ii][Dd]' $FORM_PAGES 2>/dev/null)
  if [ -n "$FK_PLACEHOLDER" ]; then
    echo "BLOCKING: Form placeholder asks user to enter ID/GUID — use EntityLookup instead"
    echo "$FK_PLACEHOLDER"
    exit 1
  fi
fi

# POST-CHECK: Backend GetAll endpoints must support ?search= for EntityLookup
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
if [ -n "$CTRL_FILES" ]; then
  for f in $CTRL_FILES; do
    if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
      if ! grep -q "search" "$f"; then
        echo "WARNING: Controller missing search parameter on GetAll: $f"
        echo "GetAll MUST accept ?search= to enable EntityLookup on frontend"
      fi
    fi
  done
fi

# POST-CHECK: Route seed data vs frontend cross-validation (CONVENTION + EXISTENCE)
SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null)
APP_TSX="$WEB_SRC/App.tsx"
if [ -n "$SEED_NAV_FILES" ] && [ -n "$APP_TSX" ]; then
  SEED_ROUTES=$(grep -ohP 'Route\s*=\s*"([^"]+)"' $SEED_NAV_FILES | sed 's/Route\s*=\s*"//' | sed 's/"//' | sort -u)
  CLIENT_PATHS=$(grep -ohP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | sed "s/path:\s*['\"]//;s/['\"]//" | sort -u)
  if [ -n "$SEED_ROUTES" ] && [ -z "$CLIENT_PATHS" ]; then
    echo "WARNING: Seed data has navigation routes but App.tsx has no client routes defined"
  fi
  # Cross-check: frontend paths must use same kebab-case segments as backend API routes
  API_CONTROLLERS=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
  if [ -n "$API_CONTROLLERS" ] && [ -n "$CLIENT_PATHS" ]; then
    API_SEGMENTS=$(grep -ohP '\[Route\("api/([^"]+)"\)\]' $API_CONTROLLERS | sed 's/\[Route("api\///;s/")\]//' | tr '/' '\n' | sort -u)
    for SEG in $API_SEGMENTS; do
      # Check if frontend path contains same segment (kebab-case match)
      if echo "$SEG" | grep -qP '[a-z]+-[a-z]+'; then
        # Multi-word segment like "human-resources" — verify frontend uses same convention
        NO_HYPHEN=$(echo "$SEG" | tr -d '-')
        if echo "$CLIENT_PATHS" | grep -q "$NO_HYPHEN"; then
          echo "BLOCKING: Frontend route uses '$NO_HYPHEN' but backend API uses '$SEG' (kebab-case)"
          echo "Fix: Frontend paths MUST match backend route convention"
          echo "  Backend: api/business/$SEG/..."
          echo "  Frontend: /business/$SEG/... (NOT /business/$NO_HYPHEN/...)"
          exit 1
        fi
      fi
    done
  fi
fi

# POST-CHECK: HasQueryFilter anti-pattern
CONFIG_FILES=$(find src/ -path "*/Configurations/*" -name "*Configuration.cs" 2>/dev/null)
if [ -n "$CONFIG_FILES" ]; then
  BAD_FILTER=$(grep -Pn 'HasQueryFilter.*Guid\.Empty' $CONFIG_FILES 2>/dev/null)
  if [ -n "$BAD_FILTER" ]; then
    echo "BLOCKING: HasQueryFilter uses Guid.Empty — must use runtime tenant filter"
    echo "$BAD_FILTER"
    exit 1
  fi
fi

# POST-CHECK: PaginatedResult<T> for GetAll
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$SERVICE_FILES" ]; then
  BAD_GETALL=$(grep -Pn 'Task<(List|IEnumerable|IList|ICollection)<' $SERVICE_FILES 2>/dev/null | grep -i "getall")
  if [ -n "$BAD_GETALL" ]; then
    echo "BLOCKING: GetAll methods must return PaginatedResult<T>, not List/IEnumerable"
    echo "$BAD_GETALL"
    exit 1
  fi
fi

# POST-CHECK: i18n file structure — per-module JSON files MUST exist (BLOCKING)
WEB_DIR=$(find . -name "App.tsx" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null | head -1)
if [ -n "$WEB_DIR" ]; then
  I18N_DIR="$WEB_DIR/i18n/locales"
  if [ -d "$I18N_DIR" ]; then
    for LANG in fr en it de; do
      LANG_DIR="$I18N_DIR/$LANG"
      if [ ! -d "$LANG_DIR" ]; then
        echo "BLOCKING: Missing i18n locale directory: $LANG_DIR"
        echo "All 4 languages (fr, en, it, de) MUST have a dedicated directory"
        exit 1
      fi
      # Check for at least one module-specific JSON (not just common.json)
      MODULE_JSONS=$(find "$LANG_DIR" -name "*.json" ! -name "common.json" ! -name "navigation.json" 2>/dev/null)
      if [ -z "$MODULE_JSONS" ]; then
        echo "BLOCKING: No module translation files in $LANG_DIR/"
        echo "Each module MUST have src/i18n/locales/$LANG/{moduleLower}.json"
        exit 1
      fi
    done
  else
    echo "BLOCKING: Missing i18n/locales directory — i18n file structure not created"
    echo "Expected: src/i18n/locales/{fr,en,it,de}/{module}.json"
    exit 1
  fi
fi

# POST-CHECK: i18n required key structure (BLOCKING — upgraded from WARNING)
if [ -d "$I18N_DIR/fr" ]; then
  for f in $(find "$I18N_DIR/fr/" -name "*.json" 2>/dev/null); do
    BASENAME=$(basename "$f")
    case "$BASENAME" in common.json|navigation.json) continue;; esac
    for KEY in "actions" "labels" "errors" "messages" "empty"; do
      if ! grep -q "\"$KEY\"" "$f"; then
        echo "BLOCKING: i18n file missing required key group '$KEY': $f"
        echo "All module i18n files MUST contain: actions, labels, errors, messages, empty"
        exit 1
      fi
    done
  done
fi

# POST-CHECK: Pages must NOT contain hardcoded English UI text (BLOCKING)
PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
if [ -n "$PAGE_FILES" ]; then
  # Detect common hardcoded English patterns in JSX (excluding imports/comments/code)
  HARDCODED_TEXT=$(grep -Pn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No .* found|Are you sure|Submit|Back|Actions|Status|Name|Description|Total)\s*<' $PAGE_FILES 2>/dev/null | grep -v '//' | grep -v 'test' | head -20)
  if [ -n "$HARDCODED_TEXT" ]; then
    echo "BLOCKING: Hardcoded English UI text found in page files"
    echo "ALL visible text MUST use t() from useTranslation() — NEVER hardcode strings"
    echo "Pattern: t('{module}:actions.create', 'Create') — namespace + fallback"
    echo ""
    echo "Found:"
    echo "$HARDCODED_TEXT"
    exit 1
  fi
fi

# POST-CHECK: Hooks must NOT contain hardcoded error messages (BLOCKING)
HOOK_FILES=$(find "$HOOK_DIR" -name "*.ts" -o -name "*.tsx" 2>/dev/null)
if [ -n "$HOOK_FILES" ]; then
  HARDCODED_ERRORS=$(grep -Pn "(setError|throw new Error)\(['\"](?!t\()[A-Z][a-z]+" $HOOK_FILES 2>/dev/null | head -15)
  if [ -n "$HARDCODED_ERRORS" ]; then
    echo "BLOCKING: Hardcoded error messages in hooks — MUST use i18n t() function"
    echo "Hooks must import useTranslation and use t('{module}:errors.loadFailed') for all messages"
    echo ""
    echo "Found:"
    echo "$HARDCODED_ERRORS"
    exit 1
  fi
fi

# POST-CHECK: SeedConstants must NOT contain ContextId (pre-seeded by SmartStack core)
SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
if [ -n "$SEED_CONST_FILES" ]; then
  BAD_CTX=$(grep -Pn 'ContextId\s*=' $SEED_CONST_FILES 2>/dev/null)
  if [ -n "$BAD_CTX" ]; then
    echo "BLOCKING: SeedConstants must NOT contain a ContextId constant"
    echo "Context IDs are pre-seeded by SmartStack core — look up by code at runtime"
    echo "$BAD_CTX"
    exit 1
  fi
fi
SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
if [ -n "$SEED_ALL_FILES" ]; then
  BAD_CTX_GUID=$(grep -Pn 'DeterministicGuid\("nav:(business|platform|personal)"\)' $SEED_ALL_FILES 2>/dev/null)
  if [ -n "$BAD_CTX_GUID" ]; then
    echo "BLOCKING: Deterministic GUID for NavigationContext detected"
    echo "Fix: Look up context by code at runtime in SeedDataProvider.SeedNavigationAsync()"
    echo "$BAD_CTX_GUID"
    exit 1
  fi
fi

# POST-CHECK: RolePermission seed data must NOT use deterministic role GUIDs
SEED_ALL_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
if [ -n "$SEED_ALL_FILES" ]; then
  BAD_ROLE_GUID=$(grep -Pn 'DeterministicGuid\("role:' $SEED_ALL_FILES $SEED_CONST_FILES 2>/dev/null)
  if [ -n "$BAD_ROLE_GUID" ]; then
    echo "BLOCKING: Deterministic GUID for role detected"
    echo "System roles are pre-seeded by SmartStack core — look up by Code at runtime"
    echo "$BAD_ROLE_GUID"
    exit 1
  fi
fi

# POST-CHECK: Services must NOT use TenantId!.Value (crashes with 500 instead of 401)
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
if [ -n "$SERVICE_FILES" ]; then
  BAD_PATTERN=$(grep -Pn 'TenantId!\s*\.Value|TenantId!\s*\.ToString|\.TenantId!' $SERVICE_FILES 2>/dev/null)
  if [ -n "$BAD_PATTERN" ]; then
    echo "BLOCKING: TenantId!.Value causes 500 when tenant context is missing"
    echo "Fix: var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();"
    echo "NEVER use UnauthorizedAccessException for tenant context — it returns 401 which clears the frontend token."
    echo "$BAD_PATTERN"
    exit 1
  fi
fi

# POST-CHECK: Services must NOT use UnauthorizedAccessException for tenant context (causes token clearing)
if [ -n "$SERVICE_FILES" ]; then
  BAD_UNAUTH=$(grep -Pn 'UnauthorizedAccessException.*[Tt]enant' $SERVICE_FILES 2>/dev/null)
  if [ -n "$BAD_UNAUTH" ]; then
    echo "BLOCKING: Services use UnauthorizedAccessException for tenant context — causes 401 which clears the frontend token"
    echo "$BAD_UNAUTH"
    echo "Fix: var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();"
    exit 1
  fi
fi

# POST-CHECK: IAuditableEntity + Validator pairing
ENTITY_FILES=$(find src/ -path "*/Domain/Entities/Business/*" -name "*.cs" 2>/dev/null)
if [ -n "$ENTITY_FILES" ]; then
  for f in $ENTITY_FILES; do
    if grep -q "ITenantEntity" "$f" && ! grep -q "IAuditableEntity" "$f"; then
      echo "WARNING: Entity has ITenantEntity but missing IAuditableEntity: $f"
    fi
  done
fi
CREATE_VALIDATORS=$(find src/ -path "*/Validators/*" -name "Create*Validator.cs" 2>/dev/null)
if [ -n "$CREATE_VALIDATORS" ]; then
  for CV in $CREATE_VALIDATORS; do
    UV=$(echo "$CV" | sed 's/Create/Update/')
    if [ ! -f "$UV" ]; then
      echo "BLOCKING: CreateValidator without matching UpdateValidator: $CV"
      echo "Expected: $UV"
      exit 1
    fi
  done
fi
```

**Error resolution cycle (ALL categories):**
1. Read the FULL error output (not just first line)
2. Identify root cause: file path + line number
3. Fix the issue in source code
4. Rebuild: `dotnet build --no-restore` or `npm run typecheck`
5. If still failing → repeat from step 1
6. **NEVER** mark task completed with failing build or tests
7. **NEVER** skip the verification — it is BLOCKING

### 9. Update Task State

```javascript
task.files_changed = { created: {files_created}, modified: {files_modified} };
task.validation = {validation_result};
if ({task_failed}) { task.status = 'failed'; task.error = '{reason}'; task.completed_at = now; }
prd.updated_at = new Date().toISOString();
writeJSON('.ralph/prd.json', prd);
```

Note: `task.status` stays `"in_progress"` until step-03 confirms commit. Only `"failed"` is set here.

---

## CONSTRAINTS:

**DO:** Implement exactly what task describes, follow patterns, validate with MCP, track files, check criteria.
**DON'T:** Implement other tasks, refactor unrelated code, add extras, skip validation.

---

## NEXT STEP:

After task executed and validated, proceed to `./step-03-commit.md`
