---
name: relentless-gates
description: Read a PRD's Agent Handoff block and generate failing TDD gate tests for each machine-verifiable acceptance criterion. Run between relentless-prd and relentless-implement.
user-invocable: true
---

# Gate Test Generator

Read a PRD file, parse the Agent Handoff JSON, and generate **failing test stubs** for every machine-verifiable acceptance criterion. These gate tests become the hard pass/fail termination signal for `relentless-implement`.

**Announce at start:** "Generating gate tests from PRD acceptance criteria."

## Pipeline Position

```
relentless-prd → prd_<slug>.md
    ↓
relentless-gates → backend/tests/gates/*.py + frontend/src/__tests__/gates/*.test.ts + _gates.json
    ↓
relentless-implement → makes gate tests pass (termination: all gates green)
```

## Process

### Step 1: Find and parse the PRD

1. **Argument provided** — If the user typed `/relentless-gates prd_foo.md`, use that file.
2. **Auto-detect** — Run `ls prd_*.md` in current directory. If exactly one, use it. If multiple, ask.
3. **Parse the Agent Handoff JSON** — Extract the `acceptance_criteria[]` array from the fenced JSON block in the "Agent Handoff" section.
4. **Validate** — Confirm each entry has required fields:
   - Machine ACs: `id`, `description`, `verifiable: true`, `test_type`, `gate_file`, `gate_test`
   - Manual ACs: `id`, `description`, `verifiable: false`, `skip_reason`

If the JSON is missing or malformed, tell the user: "The PRD's Agent Handoff block is missing or doesn't have `acceptance_criteria[]`. Run `/relentless-prd` to generate a compatible PRD."

### Step 2: Create gate directories

```bash
mkdir -p backend/tests/gates
mkdir -p frontend/src/__tests__/gates
```

Create `__init__.py` in backend gate dir if it doesn't exist:

```bash
touch backend/tests/gates/__init__.py
```

### Step 3: Generate failing test stubs

For each AC where `verifiable: true`:

#### pytest gates (backend)

Generate a file at `gate_file` path with this structure:

```python
"""
Gate test for {ac_id}: {ac_description}
Auto-generated by relentless-gates. DO NOT EDIT — regenerate from PRD.
"""
import pytest


class Test{AcId}:
    """
    {ac_description}
    """

    def {gate_test}(self):
        # TODO: Implement this gate test
        # AC: {ac_description}
        pytest.fail(
            "GATE NOT PASSED: {ac_id} — {short_description}. "
            "Implement the feature to make this test pass."
        )
```

**Rules for pytest gates:**
- One file per AC (isolation — a failing gate doesn't mask others)
- Class name: `Test` + AC ID in PascalCase (e.g., `TestAc1`, `TestAc5`)
- Test method name: from `gate_test` field (e.g., `test_ac1_base_tcv_within_range`)
- The `pytest.fail()` message must include the AC ID and description — this is what the implementing agent sees when it runs tests
- Import only pytest and stdlib by default. Add fixture imports based on what the test needs (read existing test files for patterns)
- If the AC references an API endpoint, include a comment showing the expected request/response shape
- If the AC references a function, include a comment showing the expected signature

#### vitest gates (frontend)

Generate a file at `gate_file` path with this structure:

```typescript
/**
 * Gate test for {ac_id}: {ac_description}
 * Auto-generated by relentless-gates. DO NOT EDIT — regenerate from PRD.
 */
import { describe, it, expect } from 'vitest';

describe('{ac_id}: {short_description}', () => {
  it('{gate_test}', () => {
    // TODO: Implement this gate test
    // AC: {ac_description}
    expect.fail(
      'GATE NOT PASSED: {ac_id} — {short_description}. ' +
      'Implement the feature to make this test pass.'
    );
  });
});
```

**Rules for vitest gates:**
- One file per AC (same isolation principle)
- Use existing test setup if present (check for `setup.ts` imports)
- If the AC involves React components, import `@testing-library/react` and include a comment about what to render
- If the AC involves store state, import the store and include expected state shape

#### Manual AC stubs

For ACs where `verifiable: false`, generate a **skipped test** so the manifest stays complete:

**pytest:**
```python
@pytest.mark.skip(reason="{skip_reason}")
def test_{ac_id_slug}_manual(self):
    """Manual verification: {ac_description}"""
    pass
```

**vitest:**
```typescript
it.skip('{ac_id}: {ac_description} (manual verification)', () => {
  // {skip_reason}
});
```

### Step 4: Enrich gate stubs with context

**This is the critical step that separates useful gates from useless ones.**

For each machine-verifiable AC, read the relevant source files (from `constraints.key_files` in the Handoff JSON) and enrich the test stub with:

1. **Correct imports** — Import the actual modules/components the test needs
2. **Fixture setup** — If tests need a database session, API client, or test data, add fixture references matching existing test patterns
3. **Assertion shape** — Add commented-out assertions showing what the passing test should look like, based on the AC description and existing code

Example of an enriched pytest gate (not just `pytest.fail`):

```python
"""
Gate test for AC-5: Base scenario projected TCV between $7.2M-$8.8M AUD
"""
import pytest
from fastapi.testclient import TestClient


class TestAc5:
    """Given the AGL sample estimate with projection calculated for the Base
    scenario tier, then the 3-year projected TCV is between $7.2M and $8.8M
    AUD at list price."""

    def test_ac5_base_scenario_tcv_within_range(self, client: TestClient, sample_estimate):
        estimate_id = sample_estimate["estimate_id"]

        # Get scenario tiers
        tiers_resp = client.get(f"/api/v1/scenarios/estimate/{estimate_id}")
        assert tiers_resp.status_code == 200
        tiers = tiers_resp.json()
        base_tier = next(t for t in tiers if t["name"].lower() == "base")

        # Run projection with scenario filter
        proj_resp = client.post(
            f"/api/v1/projections/{estimate_id}/calculate",
            params={"scenario_id": base_tier["id"]}
        )
        assert proj_resp.status_code == 200
        result = proj_resp.json()

        # GATE: TCV must be within range
        tcv_aud = result["summary"]["total_cost_3yr"] * 1.56  # USD to AUD
        assert 7_200_000 <= tcv_aud <= 8_800_000, (
            f"GATE FAILED: AC-5 — Base TCV is ${tcv_aud:,.0f} AUD, "
            f"expected $7.2M-$8.8M AUD"
        )
```

**How to enrich:**
1. Read existing test files in the project to find fixture patterns (e.g., `client`, `sample_estimate`, `db_session`)
2. Read the source files referenced in the AC to understand function signatures, response shapes, model fields
3. Write the test so that it **will fail for the right reason** (missing feature, wrong value) not for the wrong reason (import error, missing fixture)
4. If you can't determine the exact assertion shape, fall back to `pytest.fail()` with a descriptive message — a bad enrichment is worse than an honest stub

### Step 5: Write the gates manifest

Write `_gates.json` to the project root:

```json
{
  "prd_file": "prd_<slug>.md",
  "generated_at": "<ISO timestamp>",
  "gate_summary": {
    "total_acs": 15,
    "machine_verifiable": 11,
    "manual_only": 4,
    "backend_gates": 8,
    "frontend_gates": 3
  },
  "gates": [
    {
      "ac_id": "AC-1",
      "gate_file": "backend/tests/gates/test_<slug>_ac1.py",
      "gate_test": "test_ac1_<slug>",
      "test_type": "pytest",
      "status": "failing"
    },
    {
      "ac_id": "AC-N",
      "gate_file": null,
      "gate_test": null,
      "test_type": "manual",
      "status": "skipped",
      "skip_reason": "requires browser/human judgment"
    }
  ],
  "verify_command": {
    "gates_only": "uv run pytest backend/tests/gates/ -x --no-header -q && npx vitest run frontend/src/__tests__/gates/",
    "full_suite": "cd frontend && npm test && cd ../backend && uv run pytest"
  }
}
```

### Step 6: Verify gates fail correctly

Run the gate tests to confirm they fail with the expected messages:

```bash
# Backend gates
cd backend && uv run pytest tests/gates/ -x --no-header -q --timeout=30 2>&1 | head -30

# Frontend gates (if any)
cd frontend && npx vitest run src/__tests__/gates/ --reporter=verbose 2>&1 | head -30
```

**Expected:** All machine gates FAIL with `GATE NOT PASSED` or assertion errors. Manual gates are SKIPPED. Zero tests PASS (if any pass before implementation, the gate is testing existing behavior, not the new feature — flag this to the user).

If any gate errors (import failure, fixture not found, syntax error), **fix it immediately**. A gate that errors is not a gate — it's noise.

### Step 7: Report

Summarize to the user:

```
Gate tests generated from prd_<slug>.md:

  Backend:  8 gates in backend/tests/gates/  (all failing)
  Frontend: 3 gates in frontend/src/__tests__/gates/  (all failing)
  Manual:   4 ACs skipped (require human verification)
  Manifest: _gates.json

Verify command: uv run pytest backend/tests/gates/ -x && npx vitest run frontend/src/__tests__/gates/

Ready for implementation:
  /relentless-implement prd_<slug>.md
```

## Design Principles

### `_gates.json` is the durable artifact

Following the RelentlessAgent pattern, every phase produces a durable artifact that survives across fresh context windows. For gates, that artifact is `_gates.json` — the manifest that downstream agents (implementer, orchestrator) read from disk to understand what needs to pass. The gate test files are the executable truth; the manifest is the index. Together they enable crash recovery: if the pipeline restarts, it reads `_gates.json` to know what gates exist and which have passed.

### Gates must fail for the right reason

A gate that fails because of a missing import is useless — the implementing agent will "fix" it by changing the import, not by implementing the feature. Gates must fail because the **feature doesn't exist yet** or the **assertion doesn't hold yet**.

### One gate per AC, one file per gate

This gives clear signal. When 3 of 11 gates pass, you know exactly which ACs are done. Putting multiple ACs in one file muddies the signal — one failure masks the rest.

### Enriched > stub

A `pytest.fail("implement me")` stub tells the agent nothing. An enriched gate with real imports, fixture setup, and assertion shape tells the agent exactly what "done" looks like. Spend the time to read existing tests and mirror their patterns.

### Manual ACs are first-class

Skip them, don't omit them. The manifest should always account for 100% of ACs. This prevents "we forgot AC-12 existed" when reviewing results.

### Gates are disposable

Gate tests live in `gates/` subdirectories specifically so they can be deleted after the feature ships. They're scaffolding, not permanent test infrastructure. The implementing agent may also write proper unit/integration tests in the main test directories — gates don't replace those, they bootstrap TDD.

## Troubleshooting

| Problem | Fix |
|---------|-----|
| PRD has no Agent Handoff JSON | Run `/relentless-prd` to regenerate with the gate-ready format |
| `acceptance_criteria[]` missing from JSON | Edit the PRD's Handoff block to add it (see relentless-prd template) |
| Gate test errors instead of failing | Fix imports/fixtures — read existing test files for patterns |
| Gate test passes before implementation | The AC tests existing behavior — either remove the AC or make the assertion more specific |
| No `gate_file` path in AC entry | The PRD needs updating — each machine AC must specify its gate file path |
