How Pulse Is Built
A spec-driven, test-first process for software that has to be trusted

Most AI-assisted code is written like this: describe a feature, accept the first plausible diff, run it once, call it done. That can feel productive. The problem is not speed; it is that the process hides uncertainty. Nobody can say which requirement the code serves, which failure modes were considered, or what would prove the feature actually works.
Pulse is built the other way around. The code is still written quickly, often with an AI agent, but every change has a paper trail: a requirement, a design, a task, a failing test, a command that passed, a review, and a written record of what remains risky. This document explains that process, why each piece exists, and how to copy it.
The short version: planning is how we decide what not to guess. Tests are how we find out where the guesses were wrong. Reviews are how we catch the tests that lie. Evidence is how we keep ourselves honest.
1. The problem with "it works on my machine"
A demo proves that one path worked, on one machine, once. It does not prove that:
- an anonymous caller cannot read another user's data;
- a chunked body cannot exceed the size limit that a
Content-Lengthcheck enforced; - a retry does not create a second incident or a second alert;
- a dropped SSE connection resumes without losing or duplicating a request;
- a browser refresh, back button, proxy, or gzip header does not break the live feed;
- the service fails safely when Redis or Postgres is down;
- the next engineer can tell why a decision was made.
Those are the properties that separate a product from a prototype. They do not emerge from one large prompt. They emerge from a loop that repeatedly turns an assumption into a testable claim, then into evidence.
The process in this repository is deliberately heavier than a weekend prototype. It is meant for systems where correctness, security, and operability matter, and where an AI agent may be doing much of the typing. The structure is what lets a fast agent be useful without letting it invent requirements or quietly skip the hard cases.
2. Four documents and one pointer
Everything lives under specs/. The shape is always the same:
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 naming the feature in progress. Only that feature may be worked on. This sounds bureaucratic until a project has seven features and an agent with a fresh context window. The pointer removes the question "what should I do next?" and prevents an unrelated "quick fix" from being folded into the current change.
The four artifacts
| Artifact | Question it answers | What it must contain |
|---|---|---|
spec.md |
What are we building, for whom, and why? | user stories, numbered requirements (FR-001), Given/When/Then acceptance scenarios, edge cases, non-goals, open questions |
plan.md |
How will we build it without violating our principles? | constitution check, design and rejected alternatives, files, interfaces, data changes, dependencies, test approach, threat model, risks, requirement coverage |
tasks.md |
What is the next small, verifiable change? | ordered tasks, test first, exact check command, applicable abuse/failure cases, [P] for parallel-safe work |
verification.md |
What did we actually run, and what did it prove? | exact commands, results and counts, acceptance criteria checked, security cases, independent review verdict, remaining risks |
The separation matters. A spec that contains implementation details becomes stale and hard to review. A plan that repeats the spec adds no value. A task that is not traceable to a requirement is either unplanned work or a missing requirement.
Templates live in specs/_templates/. They are intentionally plain Markdown. The format is not the point; the questions are.
3. The loop
/specify -> review -> /plan -> review -> /tasks -> /analyze -> /implement
one task at a time
red test first
evidence, review, tick
Specify
Write the problem before the solution. Requirements get stable IDs. Acceptance scenarios are observable, not aspirational. If something is ambiguous, write [NEEDS CLARIFICATION: ...] and stop. Do not let the agent fill the gap with a guess.
Example, from specs/003-api-auth-dashboard/spec.md:
FR-006 On reconnect with
Last-Event-IDthe 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 noLast-Event-IDand MUST NOT be treated as an error: the client subscribes from$before it fetches history, and de-duplicates the overlaps byrequest_id, so a capture that lands between the two is shown rather than skipped.
This is a spec because it names the observable behavior, the failure case, and the ordering constraint. It does not prescribe the implementation. A reviewer can test it. An agent cannot quietly redefine "works" as "the happy path renders."
Plan
The plan is where design happens. It starts with a constitution check: one line per relevant principle explaining compliance, or a written exception approved by the user. Pulse's constitution is not decorative. It contains rules such as "ingest never blocks on slow systems", "every public credential route applies per-IP abuse controls before any credential-dependent lookup", and "tests must exercise the real framework lifecycle."
The plan also states:
- the design and the alternatives rejected;
- the files and modules that will change;
- interfaces, routes, Redis keys, and data changes;
- new dependencies and why the standard library or an existing package is not enough;
- the test approach: unit, integration with Testcontainers, end-to-end, load, or abuse;
- a threat model for every changed public route or external-data boundary;
- risks and how each is mitigated;
- a requirement coverage table mapping every
FR-to a place in the design and a test.
If a plan cannot name where a requirement is met and how it is tested, the task is not ready.
Tasks
Break the plan into tasks that each produce a reviewable change. Every task has:
- a requirement reference;
- the files it will touch;
- the failing test to write first;
- an exact check command and expected result;
- applicable abuse and dependency-failure cases;
- a note that evidence goes into
verification.md.
[P] marks tasks that are safe to run in parallel because they touch different files and do not depend on each other. Everything else is ordered.
The first task in a feature is usually infrastructure, not the visible feature. That is intentional: build the smallest vertical slice that lets later tests exercise the real system.
Analyze
Before implementation, read the spec, plan, and tasks against each other. Look for:
- requirements with no task;
- tasks with no requirement;
- contradictions between the documents;
- a test that cannot actually prove the acceptance scenario;
- a security or failure case mentioned in the spec but absent from the plan.
This is a consistency review, not another design phase. It is cheaper to find a missing requirement here than after the code is written.
Implement
One task per change. For each task:
- Write the failing test first, or the runnable check for pure configuration.
- Run it and look at the failure. The red output is information.
- Implement the smallest change that makes it pass.
- Run the task's named check, then the affected tests, then lint and typecheck.
- Exercise the applicable abuse and dependency-failure paths.
- Get an independent review. Resolve every required finding.
- Record exact commands, results, acceptance criteria, security cases, review verdict, and remaining risks in
verification.md. - Tick the task in
tasks.md.
The order is not ceremonial. The failing test proves the test can fail. The named check proves the intended command is green. Lint and typecheck catch the mistakes the tests do not. The independent review catches the tests that assert the wrong thing. The evidence lets a future reader decide whether to trust the claim.
4. Definition of done
A task is done when all of these are true:
- the task's named check passes;
- affected tests pass;
pnpm lintpasses;pnpm typecheckpasses;- applicable abuse and dependency-failure cases are covered;
- an independent review has no open required findings;
- exact commands, results, and remaining risks are in
verification.md; - the task is ticked in
tasks.md; - any new environment variable is in
.env.example.
This is stricter than "the feature works." It answers a different question: would another engineer, with no context, be able to reproduce the claim and see what is still uncertain?
5. Tests are not a phase; they are the design tool
The constitution requires test first. That is not because TDD is a religion; it is because a test written after the code tends to encode whatever the code already does, including its mistakes.
Pulse uses four levels:
| Level | What it proves | Example |
|---|---|---|
| Unit | a pure decision, parser, or state transition | apps/api/src/sse/hub.test.ts |
| Integration | the real framework, real Redis/Postgres, real migrations and routes | apps/api/src/stream.int.test.ts |
| End-to-end | the real services as processes, real webhook, real worker, real Next server | tests/e2e/dashboard.e2e.test.ts |
| Human/operational | a real browser, a load run, a chaos scenario, a seven-day soak | headless Chrome, scripts/load, scripts/chaos |
The higher levels are slower and more brittle, so they are used where the behavior only exists at that level. Unit tests cannot prove that a body limit runs before a route sends a response. An end-to-end test cannot prove that a 429 carries the right Retry-After on every branch. The plan chooses the level that can actually observe the requirement.
Tests must exercise the real lifecycle. A test that calls a handler directly does not prove that Fastify's parser, hooks, body limits, and preClose teardown behave correctly. A test that mocks Redis does not prove the Lua rate limiter is atomic. A test that only checks a response body does not prove a token stayed out of the logs.
The test suite also covers the ugly paths:
- malformed input and valid-looking unknown credentials;
- declared, chunked, and slowly streamed bodies;
- rate limiting before expensive work;
- dependency outages: Redis, Postgres, Loki, an alert provider;
- retries, concurrent workers, and duplicate delivery;
- secret exposure through responses, logs, metrics, alerts, and third-party payloads.
These are not edge cases bolted on at the end. They are part of the feature's definition.
6. Reviews are adversarial, not ceremonial
Every task gets an independent review before it is ticked. The reviewer is asked to find the places where the implementation and the evidence disagree. Useful review questions:
- Which requirement does this change satisfy, and where is the test?
- What is the failing-test output from before the fix?
- Does the test exercise the real framework lifecycle, or a convenient substitute?
- What happens on malformed input, an unknown token, an oversized chunked body, or a dependency outage?
- Could a caller choose their own rate-limit bucket, trust boundary, or response type?
- Is any token, hash, or internal identifier visible in a response, log, metric, or alert?
- What did the implementation change that the spec did not ask for?
- What is still untested, and is that risk written down?
Reviews have found real defects, not just style issues:
- In T015, an absolute-form request target bypassed the ingest host check. A raw-socket test now covers it.
- In T016,
nosniffwas defaulted rather than forced, so a signed-in owner could send an empty header and turn it off. The value is now forced, and the test sends the hostile input. - In T013, a TTL race in the public-status rate limiter could leave a Redis key without an expiry. The limiter became one atomic Lua step with an expiry test.
- In the live-feed work, a browser test found that the web proxy compressed the SSE stream and buffered events; a second browser test found that going Back rejoined a reader that had already been told to stop, orphaning the new subscriber.
The last two are important: they were found by exercising the system the way a user does, not by asking the unit tests whether they felt good. A review or a smoke test that finds nothing is not proof of correctness; it is one more observation.
When a review finds something required, fix it and review again. Optional findings may be deferred only with a written reason. "We'll remember" is not a reason.
7. Evidence is part of the product
verification.md is not a test log. It is the answer to "why should I believe this?"
A good entry includes:
## 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: ...
Two details make this useful. First, the counts and exit status are recorded, so a future reader can tell the difference between "passed" and "the command was not run." Second, remaining risks are not hidden. A document that claims perfection is less trustworthy than one that names the trade-off.
The same habit applies to the documents themselves. If reality diverges from the spec or plan, update the document first, then the code. If a bug reveals that a requirement was ambiguous, fix the requirement before choosing an implementation. The documents are not a historical record of what we once thought; they are the current contract.
8. Traceability: from requirement to evidence
The chain is intentionally unbroken:
solution document / decision
|
v
spec.md FR-005, FR-006, acceptance scenarios
|
v
plan.md requirement coverage table: where met + which test
|
v
tasks.md T0xx: files, failing test first, exact check
|
v
code + tests
|
v
verification.md: commands, results, review, risks
|
v
ticked task and a feature that can be explained
When a feature is reviewed, the reviewer can walk that chain in either direction. Start from a requirement and ask where it is tested. Start from a test and ask which requirement it serves. Start from a changed file and ask which task authorized it. If any link is missing, the change is not ready.
This is also the practical difference between planned work and vibe coding. Vibe coding has a diff. Planned work has a diff, a reason, a check, and a stated limit.
9. Scope control: one feature, one task, small diffs
Three rules keep the process from collapsing under its own ambition:
- Only the feature in
specs/CURRENTis in progress. Future features may have a spec, but their plans and tasks wait until the earlier features have taught us something. This is deliberately not waterfall planning. - One logical task per change. An unrelated defect discovered during implementation is recorded as separate spec-backed work, not folded into the current diff.
- No invented requirements. If the spec is ambiguous, add
[NEEDS CLARIFICATION: ...]and ask. An agent that guesses is not being helpful; it is moving the uncertainty somewhere harder to see.
This has a cost. It is slower than accepting every plausible diff. It also produces work that can be reviewed, reverted, and explained. The trade is deliberate: spend a little time writing the claim before spending a lot of time debugging a surprise.
10. Security and failure are first-class, not a later hardening phase
Security is not a checklist applied at the end. The constitution and the plan template force the question early:
- Who can call this route?
- What work happens before authentication or credential lookup?
- What is the resource bound: body size, rate, concurrency, timeout, response size?
- What happens when each dependency fails: fail open or fail closed?
- Where can a token, hash, or internal identifier leak: response, log, metric, alert, third party?
- Can an attacker choose a trust boundary, a rate-limit bucket, or an output encoding?
The T015 host guard is a small example. Ingest is public; the Host header is attacker controlled. The fix was not "check the string somewhere." It was an async Fastify hook that runs before the capture or heartbeat routes touch Redis, cache, or metrics; it handles absolute-form request targets; it answers the same 404 as an unknown token so the route's existence is not revealed; it compares lowercase hosts exactly; and it has a raw-socket test for the parser-level bypass.
The live-feed bugs are another example. They were not security bugs, but they were invisible to unit tests and visible to a user. The process did not say "the tests pass, so the feature is done." It said: exercise the real browser, reproduce the report, write the failing test, fix the cause, and record what remains.
11. Working with AI agents without surrendering the process
The process is designed for a human reviewer and an AI implementer. A few rules make that combination work:
- Load only the context for the current task. Read
AGENTS.md,constitution.md, the currentspec.md,plan.md, andtasks.md. Do not re-read the entire solution document; open only the section the plan cites. Long context sessions re-send everything and make the agent less reliable. - One task per session. A session that starts with "implement the whole feature" invites a giant diff and a shallow review. A session that starts with T016 has a check and a boundary.
- Ask for the failing test output before the fix. The red failure is evidence that the test is real and that the agent understands the requirement.
- Use the stronger model for specification, planning, and analysis. Use the faster model for implementation once the task and tests are clear. The expensive thinking happens where mistakes are cheapest to fix: in the documents.
- Stop loops early. If the agent edits the wrong files, restate the task from
tasks.mdand start a fresh session. Persistence is not a virtue when the context is wrong. - Never let the agent silently change the requirement. The answer to an ambiguous spec is a question, not a confident patch.
The human still owns the decisions and the final review. The agent is fast at drafting and tiring at guessing; the process puts each in the right role.
12. What "well planned" does not mean
It does not mean a 200-page design document written before anything runs. It does not mean every decision is final. It does not mean the plan is always right.
Pulse's own documents say the opposite. Features 004 to 007 originally had only a spec; their plans and tasks were generated just in time, after earlier features taught us something. The verification.md files record bugs, flakes, deferred risks, and assumptions that may not hold under different conditions. That is the point: planning is not the absence of uncertainty. It is the decision to make uncertainty visible, test the parts that matter, and write down what is left.
The opposite of vibe coding is not "perfect foresight." It is disciplined learning: make a claim small enough to test, test it in the environment where it matters, review the result, and carry the lesson forward.
13. Signals that you are drifting back to vibe coding
Use this as a self-check:
- There is no failing test before the fix.
- The change has no requirement or task reference.
- The test passes whether or not the feature works.
- "Works on my machine" is the only evidence.
- The diff includes unrelated refactors.
- Security and dependency failures are deferred to "hardening later."
- The spec is updated after the code to match whatever was built, without review.
- A required review finding is closed by editing the test to agree with the code.
- Remaining risks are omitted because the task is "done."
- The answer to "what does this prove?" is "the tests pass."
None of these is automatically fatal. Each one is a place where the process should slow down.
14. A checklist for adopting this on another project
- Write a short constitution: the ten to twenty rules your system must not violate.
- Keep a decision log. Record decisions that are hard to reverse.
- Pick one vertical slice. Write a spec with numbered requirements and observable acceptance scenarios. Put implementation out of scope.
- Write a plan that checks the constitution, names files and interfaces, states the threat model, and maps every requirement to a test.
- Break the plan into ordered tasks. Each task gets one failing test and one exact check.
- Implement one task at a time. Red, green, affected tests, lint, typecheck.
- Get an independent review. Fix every required finding. Review again if the fix is substantial.
- Record the commands, results, review verdict, and remaining risks.
- Exercise the system the way a user does: a browser, a load generator, a killed dependency, a restore, a seven-day soak. Tests are necessary, not sufficient.
- Keep one current feature pointer. Finish it before starting the next one.
- Update the documents when reality changes. The documents are the contract, not a museum.
15. Run the loop in this repository
The artifacts are already here:
| What | Where |
|---|---|
| Non-negotiable principles | specs/constitution.md |
| The loop and gates | specs/WORKFLOW.md |
| The current feature | specs/CURRENT |
| The spec, plan, tasks, evidence for a feature | specs/NNN-name/ |
| Agent rules | AGENTS.md |
| Architecture source of truth | docs/solution-document.md |
| The stack walkthrough | docs/the-stack.md |
The commands:
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 sequentially, not in parallel: both build the web app and will collide. The same rule is in AGENTS.md.
At the start of a session, read only AGENTS.md, constitution.md, and the current feature's spec.md, plan.md, and tasks.md. Then pick the first unticked task. If the task is not small enough to verify, the plan is not finished.
16. Closing thought
Planning is often sold as documentation. In practice it is a control system for uncertainty. The spec decides what "correct" means. The plan decides where the risk sits. The failing test decides whether the code actually changed the behavior. The review decides whether the test is honest. The evidence decides whether the next person should believe any of it.
That is the difference between a project that was vibe coded and one that was engineered: not the absence of AI, and not the absence of speed, but the presence of a trail from a claim to its proof.
Comments
Join the discussionNo comments yet. Be the first to comment.



