RSS Amplifier

Ryan’s Substack · Sep 19, 2025

Bring a Spec, Not a Vibe

0
Sign in to vote or save

Ryan Booth · Ryan’s Substack

A good PRD is the contract your agents and humans answer to. In this post, we’ll define what a developer‑grade PRD looks like, show how to turn it into Playwright tests + evals as merge gates, and encode context control so agents don’t redecorate the codebase. Real examples come from my Artist Dashboard app.

From Post #1: we ship with gates, not guesses. That starts with a PRD that’s short, enforceable, and directly mapped to tests/evals. If the PRD doesn’t name it, we don’t build it. If a test/eval doesn’t pass, it doesn’t merge. Simple, predictable, boring. The way production likes it.

The hard part is providing the agent with enough detail to complete the work without blowing up the context window. Long PRDs invite context rot (stale or conflicting instructions), while short PRDs often miss requirements or interoperability details. Treat length as a design constraint and plan for it as you write.

  • Target length: 1–2 pages (≈600–1,200 words) per feature PRD. Break large initiatives into phased PRDs or feature PRDs instead of one monolith. Remember: context switching hurts models the same way it hurts humans—minimize how often the agent must pull new parts of the repo into active context.

  • Task context files: Move deep, task‑specific details (API samples, CLI commands, fixtures) into task‑focused context files and reference them lazily (only when the task needs them).

  • Phased deployments: Use Phase 1/2/3 PRDs to enable parallelism on complex projects. Define phase handoffs explicitly so later phases don’t undo earlier stability.

Rule of thumb: You can “test the waters” of a PRD by starting an early phase and seeing if the agents stay focused in the first few tasks. If you see drift, stop, create a new branch, revise the PRD and start over. Better to fix in Phase 1 than discover the miss in Phase 4.

  1. Problem & Goal — What user pain are we fixing, and how will we know it worked?

  2. Scope (In/Out) — What’s in; what’s explicitly not (use negative scope).

  3. Personas & Stories — Who’s using it; what jobs do they need to do?

  4. Acceptance Criteria — Checkable statements that turn into tests/evals.

  5. Non‑functional Requirements (NFRs) — Latency, reliability, security, accessibility.

  6. Interfaces & Data — Endpoints, request/response shapes, schemas.

  7. Tooling & Approvals — Which tools/MCPs are allowed; who approves risky calls (HITL).

  8. Observability & SLOs — What we measure (e.g., p95, error budget) and what we alert on.

  9. Rollout & Guardrails — HITL, Flags, read‑only phases, kill switches, rollback steps.

Keep the PRD lean and reference supporting context files by path so they’re pulled into the prompt only when needed:

# docs/rules/agent-context.yml (pattern)
context_refs:
  - when: task.tags anyof ["upload", "multipart"]
    include: docs/context/uploads_local_api.md
  - when: task.tags anyof ["social", "instagram", "facebook"]
    include: docs/context/social_validation.md

If your orchestration doesn’t support conditional includes, add a prompt macro in the PRD:

“When working on tasks tagged upload, consult docs/context/uploads_local_api.md.”

Most teams already break projects into phases. The key is how you slice. Prefer vertical slices that ship a thin, testable path end‑to‑end—DB + API + UI—so E2E stays green and risk stays local. Use DB‑first (or API‑first) only when it materially reduces risk (e.g., large schema work that must land before any UI can exist).

docs/prd/PRD_uploads_phase_1.md   # Resume multipart upload (thin DB/API/UI)
docs/prd/PRD_uploads_phase_2.md   # Post-upload verification & dedupe
docs/prd/PRD_uploads_phase_3.md   # Cross-provider retries & quotas

At the end of each slice/phase, freeze acceptance criteria, public interfaces, and SLOs. If you must change them later, add a compatibility note in the next phase PRD and adjust tests/evals accordingly.

Aim for 6–12 acceptance criteria per feature. If you need more, you likely have two features. Agent‑generated tests/evals can balloon quickly; remember, you’ll maintain those tests for the life of the feature—keep only what’s necessary to prove behavior.

Because Artist Dashboard was greenfield, I automated it by creating a PRD generator named PRDBuilder to standardize and speed things up:

  • Gather inputs — Feature requests, user stories, feedback, and product docs.

  • Draft PRD — Build a first pass from Jinja base templates + inputs.

  • Repo & architecture pass — Scan the codebase for limitations, repo rules, and interfaces; update the draft with engineering constraints and specs.

  • Red‑team audits (lightweight) — Security (keys/RBAC/PII), Business (value/scope/KPIs), Documentation (clarity/testability).

  • Final human review — Tighten wording; verify acceptance criteria maps to a test/eval.

  • Publish & link — Final PRD is linked from the feature branch/issue.

PRDBuilder along with any scripts and templates for this workflow are available to paid subscribers.

The Tests section defines what must pass before an agent task is complete. The PRD won’t enforce behavior by itself; it uses a rules file and prompt text to reinforce it at the workflow level.

Prompt add‑on (paste into your agent prompt or rules):

“Adhere to all rules and suggestions in the Tests section. Do not skip test cases or use mocks to bypass problematic logic. If you cannot satisfy the requirements and associated E2E tests, stop work, set task status to blocked, and ask the user for further direction.”

// tests/e2e/upload-resume.spec.ts
import { test, expect } from '@playwright/test';
import { simulateNetworkDrop, progressBar, assetRow } from './_utils';
test('resume a multipart upload after network drop', async ({ page }) => {
  await page.goto('/uploads/new');
  await page.setInputFiles('#file', 'fixtures/200mb.mp4'); // replace with a local test fixture
  // Simulate upload to ~60% then drop, then reload the app
  await simulateNetworkDrop(page, { atPercent: 60 });
  await page.reload();
  // App should offer "Resume"
  await page.getByRole('button', { name: 'Resume upload' }).click();
  // Verify resume progressed (no restart at 0%)
  const [value, max] = await progressBar(page).evaluate(el => {
    const p = el as HTMLProgressElement;
    return [Number(p.value || 0), Number(p.max || 1)];
  });
  expect(value / (max || 1)).toBeGreaterThan(0.6);
  // Finalize and validate
  await expect(page.getByText(/Upload complete/i)).toBeVisible();
  await expect(assetRow(page)).toContainText('checksum: ok');
});

For non‑LLM features, define policy evals that guard agent/tool behavior. Example: a diff‑scope eval to prevent broad rewrites during a focused task:

// evals/policy/diff_scope.json
{
  "name": "upload_engine_surgical_diff",
  "allow": [
    "backend/upload/**",
    "tests/e2e/upload-resume.spec.ts",
    "evals/policy/diff_scope.json"
  ],
  "deny": [
    "frontend/**",
    "backend/payments/**"
  ],
  "fail_message": "Changes exceed allowed scope for 'Resume Multipart Upload'. Keep diffs surgical."
}

Why this matters: Tests are the gatekeepers of functioning code. Models love to skip work with a // TODO or by mocking entire functions just to mark a task complete. If you let that slide, you’ll pay it back later in refactors, branch rollbacks and instability. Gates prevent lazy work.

Rules and context files are the best way to control how agents interact with your repo. Keep them in‑repo (Markdown/YAML), close to the code, so humans and agents use the same source of truth. There isn’t a universal standard yet, each tool has its own mechanism, so wire up what your team actually runs and work toward a common baseline:

  • Claude Code (Anthropic): use CLAUDE.md at the project root; optional settings files can scope behavior/guardrails.

  • Cursor: create project rules in .cursor/rules/; scope by path, version‑control, and apply per‑folder for precise guardrails.

  • Gemini (Code Assist / CLI): define persistent instructions in Code Assist; for CLI, use a project system file (config/env) for consistent context.

  • Taskmaster: ships with a Cursor‑compatible rules file (e.g., .cursor/rules/dev_workflow.mdc) point it at your PRD and guardrails.

Tip: Add a repo‑level Agent Rules file that spells out how agents interact with your API, SDKs, and local environment (especially if you run in containers). Most models assume direct host processes. Be explicit about URLs, headers, auth, and containerized commands. You can thank me later.

feature: Repository-wide agent guardrails
read_only_phase: true
allowed_paths: [backend/upload/**, tests/**, docs/prd/**]
blocked_paths: [backend/payments/**, infra/prod/**]
require_hitl_for:
  - changes to auth, secrets, or env files
  - write operations to social APIs or storage
  - schema migrations
tooling:
  allowed: [storage_sdk_read, checksum_validator]
  # storage_sdk_write permitted only with HITL
merge_gates:
  required_checks: [unit, integration, e2e, eval-policy]
diff_scope_policy:
  allow: ["backend/**/routes.py", "docs/rules/**", "tests/**"]
  deny:  ["frontend/**", "backend/payments/**"]
fail_message: "Keep diffs surgical and within declared scope."
runtime:
  local:
    base_url: "https://localhost"
    notes:
      - "Use Docker Compose; do NOT run services directly on the host."
      - "Health checks expect X-Forwarded-* headers when TLS terminates at a proxy."
  test_accounts:
    ADMIN_EMAIL: "${ADMIN_EMAIL}"
    ADMIN_PASSWORD: "${ADMIN_PASSWORD}"
api_contracts:
  base_path: "/api"
  endpoints:
    - { method: GET,  path: "/health",      auth: none, expected_status: 200 }
    - { method: POST, path: "/users/token", auth: none, content_type: "application/x-www-form-urlencoded",
        response: { access_token: "string", token_type: "bearer" } }

Agents need good examples to debug or troubleshoot without guessing how thats done. Keep a single file named docs/API_Examples.md that shows copy‑pasteable curl calls and expected responses for dev and production environments. Reference it from rules and prompts when tasks fail or you are troubleshooting.

“I am getting the error {{error_text}} when I try to log in locally. Use docs/API_Examples_Local.md to diagnose. Review the active task in Taskmaster to see what changed.”

Why this matters

  • The rules file keeps agents within approved areas (e.g., away from payments).

  • The API doc provides a reproducible way to test inside containers with the right headers/TLS assumptions, no wasted tokens rediscovering basics.

  • Merge gates ensure nothing merges until unit/integration/E2E + eval‑policy all pass.

  • Vague acceptance criteria. Agents can create passing tests; they’re not great at tests that truly validate logic. Strong ACs → strong code.

  • E2E‑only testing. Keep the pyramid; use E2E sparingly for user‑visible flows. Build what you need, no more.

  • Vibe‑coding. No PRD? No rules file? Don’t expect approval. Add a linter/check that verifies PRD and rules versions for the branch.

  • MCP everywhere. If a terminal command or direct SDK call works, use it. Add MCP only if it’s materially better; keep installed MCPs to a minimum.

  • No rollback plan. Feature flags + a known‑good commit. Encode rollback steps in the PRD.

  • Each acceptance criterion has ≥1 E2E and (if relevant) ≥1 eval

  • Schema changes documented

  • Interfaces documented with request/response examples

  • Tool/MCP policy declared + HITL points identified

  • Rules and context files committed next to the PRD

Now that you have a complete PRD, put it to work. Import it into Taskmaster (or your PRD manager of choice) and start the first task. If you do, I’d love to hear how it went, good or bad. Next post: setting up Taskmaster, importing the PRD, and preparing the environment for a guarded first implementation.

No posts

Read the original on abstractryan.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.