# How Pulse Is Built

Most AI-assisted code still follows the same rhythm: describe a feature, take the first diff that looks plausible, run it once, ship it. Speed is not the problem. The problem is that nobody can point at the requirement the code satisfies, the failure modes anyone thought about, or the check that would prove the feature works outside a demo.

Pulse is built differently. We still move fast, often with an agent doing the typing, but every change leaves a trail: a requirement, a design, a task, a failing test, a command that went green, a review, and an honest note about what is still risky. What follows is that process, why each piece exists, and how you could run the same loop elsewhere.

Planning, for us, is deciding what not to guess. Tests show where the guesses were wrong. Reviews catch tests that lie. Evidence keeps the story straight.

---

## When a demo is not enough

A demo only shows that one path worked once on one machine. It does not show that an anonymous caller cannot read someone else's data, that a chunked body cannot slip past a `Content-Length` check, that a retry will not open a second incident, that an SSE client can drop and reconnect without gaps or duplicates, that a refresh or a proxy or gzip does not break the live feed, that Redis or Postgres going away fails safely, or that the next engineer can explain why a decision was made.

Those properties separate a product from a prototype. They do not fall out of a single large prompt. They come from a loop that turns an assumption into a claim, the claim into a test, and the test into something you can re-run.

The process in this repository is heavier than a weekend hack on purpose. Correctness, security, and operability matter here, and an agent may write most of the code. The structure is what lets a fast agent help without inventing requirements or skipping the nasty cases.

---

## What lives under `specs/`

Everything for feature work sits under `specs/`. The layout repeats for every feature:

```text
specs/
  constitution.md      non-negotiable engineering principles
  decisions.md         product and technical decisions already made
  CURRENT              the one feature currently in progress
  _templates/          the shapes every feature follows
  001-platform-skeleton/
    spec.md            what and why
    plan.md            how, checked against the constitution
    tasks.md           the ordered units of work
    verification.md    the evidence that each task was done
  002-hook-ingest/
  ...
```

`specs/CURRENT` is a single line: the feature in progress. Only that feature gets implementation work. That rule sounds fussy until you have seven features open and an agent with a fresh context window. The pointer answers "what next?" and stops a "quick fix" from riding along on the wrong change.

Each feature folder holds four artifacts, and they answer different questions. `spec.md` is what we are building, for whom, and why: user stories, numbered requirements like `FR-001`, Given/When/Then acceptance scenarios, edge cases, non-goals, open questions. `plan.md` is how we build it without breaking our principles: a constitution check, the chosen design and the alternatives we rejected, files and interfaces, data changes, dependencies, test approach, threat model, risks, and a table that maps every requirement to design and tests. `tasks.md` is the next small verifiable step: ordered work, test first, an exact check command, abuse and failure cases where they apply, and `[P]` when two tasks can run in parallel because they touch different files. `verification.md` is what we actually ran: commands, counts, acceptance criteria checked, security cases, an independent review verdict, and risks we are still carrying.

If the spec starts prescribing implementation, it goes stale. If the plan repeats the spec, nobody reads either. If a task cannot trace to a requirement, you have either scope creep or a hole in the spec. Templates in `specs/_templates/` are plain Markdown; the format is not the point. The questions are.

---

## The loop we run

```text
/specify  ->  review  ->  /plan  ->  review  ->  /tasks  ->  /analyze  ->  /implement
                                                                          one task at a time
                                                                          red test first
                                                                          evidence, review, tick
```

**Specify** means writing the problem before the solution. Requirements get stable IDs. Acceptance scenarios have to be observable, not wishful. When something is ambiguous, we write `[NEEDS CLARIFICATION: ...]` and stop. Letting the agent guess moves the ambiguity somewhere harder to see.

From `specs/003-api-auth-dashboard/spec.md`, FR-006 is the kind of sentence we want:

> **FR-006** On reconnect with `Last-Event-ID` the API MUST resume from that stream id, which covers persisted and not-yet-persisted entries in one path, with no gaps and no duplicates. A first connection carries no `Last-Event-ID` and MUST NOT be treated as an error: the client subscribes from `$` **before** it fetches history, and de-duplicates the overlaps by `request_id`, so a capture that lands between the two is shown rather than skipped.

That is a spec because it names behavior, a failure case, and ordering. It does not pick an implementation. A reviewer can test it. An agent cannot redefine "works" as "the happy path renders."

**Plan** is where design happens. It opens with a constitution check: one line per relevant principle, or a written exception the user approved. Pulse's constitution is not wallpaper. It includes rules like ingest never blocking on slow systems, every public credential route applying per-IP abuse controls before credential lookup, and tests exercising the real framework lifecycle.

The plan also records what we rejected, which files change, routes and Redis keys and schema deltas, new dependencies and why the stdlib is not enough, which test level can observe each requirement (unit, Testcontainers integration, end-to-end, load, abuse), a threat model for every changed public route or external boundary, risks and mitigations, and a coverage table from every `FR-` to design and test. If the plan cannot say where a requirement is met and how it is tested, we are not ready to implement.

**Tasks** split the plan into reviewable chunks. Each task names a requirement, the files it touches, the failing test to write first, the exact check command and expected result, abuse and dependency-failure cases where they apply, and a reminder that evidence goes in `verification.md`. `[P]` marks parallel-safe work. The first task in a feature is often boring infrastructure on purpose: the smallest vertical slice that lets later tests hit the real system.

**Analyze** is a consistency pass before code. Do all requirements have tasks? Do all tasks have requirements? Do the documents contradict each other? Is there a test that cannot actually prove an acceptance scenario? Did the spec mention a security case that never made it into the plan? This is cheaper than finding the gap after merge.

**Implement** is one task per change. Write the failing test (or runnable check for pure config). Run it and read the red output. Make the smallest change that goes green. Run the task's named check, then affected tests, then lint and typecheck. Hit the abuse and dependency-failure paths the task called out. Get an independent review and close every required finding. Put exact commands, results, criteria, security cases, review verdict, and remaining risks in `verification.md`. Tick the task in `tasks.md`.

The failing test proves the test can fail. The named check proves the command we care about is green. Lint and typecheck catch what tests miss. Review catches tests that assert the wrong thing. Evidence lets a stranger decide whether to trust the claim.

A task is done when the named check passes, affected tests pass, `pnpm lint` and `pnpm typecheck` pass, abuse and dependency cases are covered, review has no open required findings, `verification.md` has commands and risks, the task is ticked, and any new env var is in `.env.example`. That is stricter than "the feature works." It answers whether another engineer, cold, could reproduce the claim and see what is still uncertain.

---

## Tests as design, not homework

The constitution asks for test first. Not because TDD is sacred, but because a test written after the code usually encodes the code, bugs included.

We use four levels, chosen by what can actually observe the requirement. Unit tests suit pure decisions and state transitions, like `apps/api/src/sse/hub.test.ts`. Integration tests run the real framework against real Redis or Postgres with migrations and routes, like `apps/api/src/stream.int.test.ts`. End-to-end tests bring up real processes: webhook, worker, Next server, like `tests/e2e/dashboard.e2e.test.ts`. Human and operational checks use a real browser, load scripts, chaos, or a long soak when behavior only shows up there.

Higher levels are slower and flakier, so we do not default to them. A unit test cannot prove a body limit runs before a route sends a response. An end-to-end test cannot prove every branch returns the right `Retry-After` on a 429. The plan picks the level that can see the requirement.

Tests also have to exercise the real lifecycle. Calling a handler directly does not prove Fastify's parser, hooks, body limits, or `preClose` teardown. Mocking Redis does not prove a Lua rate limiter is atomic. Checking a JSON body does not prove a token stayed out of logs.

The suite is supposed to include the ugly paths: malformed input and plausible wrong credentials; declared, chunked, and slow bodies; rate limits before expensive work; Redis, Postgres, Loki, or an alert provider down; retries, concurrent workers, duplicate delivery; secrets leaking through responses, logs, metrics, alerts, or third-party payloads. Those are not a late "hardening" pass. They are part of what "done" means.

---

## Reviews that try to break the story

Every task gets an independent review before the tick. The reviewer is looking for places where implementation and evidence disagree. Useful questions: which requirement does this satisfy and where is the test? What did the failing test print before the fix? Does the test use the real framework lifecycle or a shortcut? What happens on bad input, unknown token, oversized chunked body, or an outage? Could a caller pick their rate-limit bucket or trust boundary? Does anything sensitive show up in a response, log, metric, or alert? What changed that the spec never asked for? What is still untested, and is that written down?

Reviews here have caught real bugs, not nits. T015: an absolute-form request target bypassed the ingest host check; a raw-socket test covers it now. T016: `nosniff` was defaulted, so a signed-in owner could send an empty header and disable it; the value is forced and the test sends that hostile input. T013: a TTL race in the public-status rate limiter could leave a Redis key without expiry; the limiter is one atomic Lua step with an expiry test. On the live feed, a browser test found the web proxy compressing SSE and buffering events; another found Back rejoining a reader that had already been told to stop, orphaning the new subscriber.

Those last two matter because they came from using the product, not from asking unit tests if they felt complete. A clean review is one data point, not proof.

Required findings get fixed and reviewed again. Optional findings can wait only with a written reason. "We will remember" does not count.

---

## Evidence you can argue with

`verification.md` answers "why should I believe this?" not "what commands exist."

A solid entry looks like this:

```markdown
## T016: Allow HTML responses only on authenticated PATCH and lock the host-only cookie

- Commands run: `pnpm vitest run --project unit apps/api/src/auth`;
  `pnpm vitest run --project integration apps/api/src/endpoints.int.test.ts`;
  `pnpm test`; `pnpm lint`; `pnpm typecheck`.
- Results: auth 3 files / 16 tests passed; endpoints 21 tests passed; full unit suite
  89 files / 539 tests passed; lint and typecheck passed (exit 0).
- Acceptance criteria checked: ...
- Security and abuse cases: ...
- Independent review: first review found one required issue; it was fixed; re-review
  found no required findings.
- Remaining risks: ...
```

Recording counts and exit status matters because "passed" and "never ran" look alike in a summary. Naming remaining risks matters because a page that claims perfection is harder to trust than one that admits trade-offs.

When reality diverges from spec or plan, we update the document first, then the code. When a bug shows a requirement was vague, we fix the requirement before debating implementation. The specs are the current contract, not a diary of old guesses.

Traceability is deliberate. A decision in the solution doc or `decisions.md` flows into numbered requirements and acceptance scenarios in `spec.md`, into the plan's coverage table, into tasks with failing tests and exact checks, into code and tests, into `verification.md`, into a ticked task you can explain. Walk the chain from a requirement to a test, or from a diff to a task, or from a test back to an FR. A missing link means the change is not ready. Vibe coding gives you a diff. This gives you a diff, a reason, a check, and a stated limit.

---

## Keeping scope from eating the process

Only the feature named in `specs/CURRENT` is in flight. Later features may have a spec, but plans and tasks wait until earlier work teaches us something. That is not waterfall; it is not planning the universe upfront.

One logical task per change. If you find an unrelated bug, it becomes separate spec-backed work, not a passenger on the current diff.

No invented requirements. Ambiguity gets `[NEEDS CLARIFICATION: ...]` and a human answer. An agent that guesses is not helping; it is hiding the question.

Yes, that is slower than accepting every plausible diff. It is also easier to review, revert, and explain. We would rather spend a little time writing the claim than a lot of time debugging a surprise.

Security and failure modes belong in the plan, not a late checklist. Who can call this route? What runs before auth or credential lookup? What bounds body size, rate, concurrency, timeout, response size? When a dependency fails, do we fail open or closed? Where could tokens or hashes leak? Can an attacker choose a trust boundary or encoding?

The T015 host guard is a small illustration. Ingest is public; Host is attacker-controlled. The fix was not a string compare somewhere convenient. It is an async Fastify hook before capture or heartbeat routes touch Redis, cache, or metrics; it handles absolute-form targets; it returns the same 404 as an unknown token; it compares lowercase hosts exactly; and it has a raw-socket test for the parser bypass. The live-feed bugs were not CVEs, but they were invisible to unit tests and obvious in a browser. The process did not stop at green CI. Reproduce the report, write the failing test, fix the cause, record what remains.

---

## Humans, agents, and who owns what

The workflow assumes a human reviewer and an agent implementer. Load only what the current task needs: `AGENTS.md`, `constitution.md`, the current `spec.md`, `plan.md`, and `tasks.md`. Pull one section from the solution doc when the plan cites it, not the whole book. Long sessions re-send everything and make the agent sloppier.

One task per session. "Implement the feature" produces a giant diff and a shallow review. "Implement T016" has a boundary and a check.

Ask for failing test output before the fix. Red output is evidence the test is real and the requirement was read.

Use the heavier model for specify, plan, and analyze; use the faster model for implementation once the task and test are clear. Mistakes are cheapest in the documents.

If the agent edits the wrong files, restate the task from `tasks.md` and start fresh. Persistence with wrong context is not virtue.

Never let the agent silently change the requirement. Ambiguity gets a question, not a confident patch.

The human still owns decisions and the final review. The agent is fast at drafting and bad at guessing; the process assigns each role accordingly.

---

## What good planning is not

It is not a 200-page design before anything runs. It is not frozen decisions. It is not a plan that is always right.

Features 004 through 007 started with specs only; plans and tasks appeared just in time after earlier features taught us something. The `verification.md` files mention bugs, flakes, deferred risks, and assumptions that might break under different load. That is normal. Planning is not eliminating uncertainty. It is making uncertainty visible, testing the parts that matter, and writing down what is left.

The opposite of vibe coding is not perfect foresight. It is small claims, tested where they matter, reviewed honestly, with lessons carried forward.

You are drifting back toward vibe coding when there was no failing test before the fix, the change has no requirement or task, the test would pass either way, "works on my machine" is the only evidence, the diff carries unrelated refactors, security waits for "later," the spec was rewritten to match the code without review, a review finding was "fixed" by weakening the test, risks were dropped because the task felt done, or "the tests pass" is the whole answer to "what does this prove?" None of that automatically kills a change. Each item is a reason to slow down.

If you wanted to try this elsewhere: write a short constitution, keep a decision log, pick one vertical slice and a spec with numbered requirements and observable acceptance (implementation out of scope), write a plan with constitution check, files, threat model, and requirement-to-test mapping, break it into ordered tasks with one failing test and one check each, implement one task at a time with red-green-lint-typecheck, review independently and re-review real fixes, record commands and risks, exercise the system like a user (browser, load, killed dependency, restore, soak), keep one current feature pointer, and update documents when reality shifts.

---

## Running it in this repository

Principles live in `specs/constitution.md`. The loop and gates are in `specs/WORKFLOW.md`. The active feature is `specs/CURRENT`. Each feature's spec, plan, tasks, and evidence sit under `specs/NNN-name/`. Agent rules are in `AGENTS.md`. Architecture is in `docs/solution-document.md`. A stack walkthrough is in `docs/the-stack.md`.

```bash
pnpm install
pnpm test              # unit
pnpm test:integration  # Testcontainers; needs Docker
pnpm test:e2e          # real services against Compose datastores
pnpm lint
pnpm typecheck
docker compose -f deploy/compose/docker-compose.yml --profile apps up -d --build
```

Run `pnpm lint` and `pnpm typecheck` one after the other, not in parallel. Both build the web app and will step on each other. The same note is in `AGENTS.md`.

At the start of a session, read `AGENTS.md`, `constitution.md`, and the current feature's `spec.md`, `plan.md`, and `tasks.md`. Pick the first unticked task. If you cannot verify the task in one sitting, the plan is not finished yet.

Planning gets sold as documentation. Here it acts more like a control system for uncertainty. The spec says what "correct" means. The plan says where the risk sits. The failing test says whether behavior actually changed. The review says whether the test is honest. The evidence says whether the next person should believe any of it.

That is what separates a vibe-coded project from an engineered one: not less AI, not less speed, but a trail from claim to proof.
