AI Dev
Lesson 2Prompting and context
A model is only as good as the context it sees — and the prompt is only the last part of that context
Lesson 1 established the loop. This lesson is about the step you run most often inside it: asking for a change.
Two things determine what you get back, and most people only think about one of them.
The prompt
What you are asking for, why, and how you will know it worked. Roughly a paragraph.
The context
Every file, rule and earlier message the model can see. Often thousands of lines, and the part you control least deliberately.
A model can only be as good as the context it sees. A perfect prompt against the wrong files produces a confident, well-structured, wrong answer.
The structured prompt
Vague requests get plausible-looking guesses. A structured prompt removes the guessing by answering the questions the agent would otherwise answer for itself.
Role: Senior <stack> engineer working in <this codebase>.
Goal: <one line: the exact intent>
Business
Context: <why this matters — the user problem, the rule,
downstream impact, who's affected>
Technical
Context: <relevant files @X @Y, current architecture,
framework/versions, the pattern to follow>
Current: <what happens today>
Desired: <target behavior>
Constraints: <what NOT to touch; perf/security limits;
no new deps; style rules>
Acceptance: <checkable success conditions / tests that must pass>
Output: <plan first & wait for approval | diff only |
list assumptions first | ...>You will not write all nine fields for a one-line fix. The point is that each one, when omitted, is a decision you have silently handed to the model.
| Field | What it prevents |
|---|---|
| Role | Generic answers pitched at the wrong level of expertise |
| Goal | Solving a nearby problem instead of yours |
| Business context | Technically valid changes that break the actual rule |
| Technical context | Inventing a pattern when one already exists in the repo |
| Current / Desired | Ambiguity about whether this is a bug or a feature |
| Constraints | Scope creep, new dependencies, refactors you did not ask for |
| Acceptance | “Done” being a matter of opinion |
| Output | Getting 400 lines of code when you wanted a plan |
Business context is the field people skip
Technical context tells the agent how the code works. Business context tells it what would count as a wrong answer. The second is what stops technically clean, functionally wrong changes.
Goal: Stop sending the welcome email twice.
Business
Context: Users on the legacy plan get migrated by a nightly job that
re-runs the signup hook. Sending twice looks like a bug to
the user and trips our spam threshold with the provider.
Under no circumstances should a genuinely new signup miss
the email — a duplicate is embarrassing, a miss is a churn
event.That last sentence tells the agent which way to fail. Without it, an agent optimising for "no duplicates" may add a guard that also suppresses legitimate first sends — a strictly worse bug, and one your tests probably do not cover.
Precision beats vague
The same request, twice.
Vague
“Add caching to the user service so it’s faster.”
Which calls? Cached where — memory, Redis, HTTP? Invalidated when? What is “faster” enough? The agent will pick, and it will pick something reasonable-looking and probably wrong for you.
Precise
“Cache getUserProfile() in the existing Redis client (@src/cache/redis.ts), keyed by user id, 5-minute TTL. Invalidate on any write in @src/services/user.ts. Do not touch the auth path. Existing tests must pass and add one for invalidation.”
The precise version names the function, the mechanism, the existing module to reuse, the key, the TTL, the invalidation trigger, the boundary, and the acceptance condition. Every one of those was a decision. You made eight of them instead of zero.
Give the agent a way to check its own work
This is the highest-leverage sentence in the lesson: an agent with a test can iterate; an agent without one can only guess and stop.
Name the command
“Run npm test -- user.spec.ts until it passes” gives it a loop. “Make sure it works” does not.
Write the test first when the behaviour is subtle
A failing test is an unambiguous specification. It is also the one artefact the agent cannot argue with.
Say which checks must stay green
Otherwise “make this test pass” is satisfiable by weakening a different one — and agents do exactly that.
Work in small steps, and watch it
Lesson 1 made the case structurally. In practice it means three habits:
- One coherent change per turn. If your prompt contains the word "and" twice, it is probably two prompts.
- Read the diff as it lands, not after twelve files have changed. Cursor shows edits as they happen; that is a feature, not noise.
- Hit Stop early. The moment an agent starts editing a file you did not expect, stop it. The cost of stopping is one message. The cost of not stopping is a revert and a re-read.
Before you prompt: the checklist
Run this in about ten seconds
- Can I state what “done” looks like in one sentence?
- Did I pick the right tool — Tab, Inline, Ask, Agent or Plan?
- Did I give business context (why) and technical context (@files, the pattern to follow)?
- Did I state constraints and what not to touch?
- Did I give it a way to verify — tests, build, or a run command?
- For a big or ambiguous task, did I use Plan mode first?
Six questions. Any "no" is a prediction about how the diff will disappoint you.
A prompt library
Most work falls into a handful of shapes. Keeping a template per shape removes the blank-page problem and stops you forgetting the constraints field at 5pm.
| Shape | The field that matters most |
|---|---|
| New feature | Business context — what would count as wrong |
| Bug fix | Current vs Desired, plus a reproduction |
| Refactor | Constraints — behaviour must not change |
| Test generation | Acceptance — which edge cases must be covered |
| Code review | Output — findings only, ranked, no rewrites |
| Security review | Role and technical context — the threat model |
| Documentation | Role — who is reading this and what do they know |
| Understand before changing | Output — explanation only, change nothing |
Two worth writing out, because their failure modes are the sharpest.
Refactor — the danger is silent behaviour change:
Goal: Extract the retry logic from @src/api/client.ts into a
reusable helper.
Constraints: Behaviour must be identical — same backoff, same jitter,
same error types propagated. No new dependencies. Do not
change any public signature.
Acceptance: Every existing test passes untouched. If you need to modify
a test to make it pass, stop and tell me instead.
Output: Diff only.That last acceptance line is the whole trick: it converts "tests pass" from something the agent can satisfy by editing tests into something it can only satisfy by preserving behaviour.
Understand before changing — the danger is it starts editing:
Goal: Explain how session invalidation works across
@src/auth/session.ts and @src/middleware/auth.ts.
Output: Explanation only. Change no files. List anything that
looks inconsistent, and any assumption you had to make.Context management
Every message, file and rule the model can see occupies the same finite window. Three things follow.
The window fills up across a conversation
A long chat is not a neutral container. Each turn adds the previous answer, the files it touched, and the tool output it produced.
| As a chat grows | What that does to the answer |
|---|---|
| Earlier turns stay in the window | Superseded decisions still influence output |
| Contradictions accumulate | “Use Redis” from turn 2 fights “keep it in memory” from turn 9 |
| Signal is diluted | The relevant file is one of forty things in view |
| The window overflows | Something gets dropped — and you do not choose what |
The remedy is a fresh chat, not a longer explanation. When an agent starts contradicting itself or reverting its own earlier work, that is the signal. Start again, state the current goal, and reference only the files that matter now.
At-symbols: scoping context deliberately
| Reference | Pulls in | Use when |
|---|---|---|
| @file | One specific file | You know exactly what is relevant — the default |
| @folder | A directory | A change spans a small, cohesive module |
| @Codebase | Semantic search across the index | You genuinely do not know where the code lives |
| @Docs | Indexed external documentation | A library’s current API matters |
| @rule-name | A specific rule file | You want a manual rule applied this once (Lesson 3) |
Reaching for @Codebase on every prompt is the context-dumping anti-pattern. It is a search tool for when you are lost, not a substitute for knowing your repository.
What Cursor indexes, and how to control it
Cursor builds an embedding index of your repository so it can retrieve semantically relevant code rather than relying on filename matches. That index is what @Codebase searches and what the agent falls back on when you have not scoped the context yourself.
You get two controls, and they are not the same thing.
| File | Effect | Still usable if referenced? | Reach for it when |
|---|---|---|---|
| .cursorignore | Full block — hidden from all AI features | No | Secrets, credentials, sensitive data |
| .cursorindexingignore | Index only — excluded from search | Yes, when you explicitly reference it | Large generated or vendored files |
Both use .gitignore syntax, and Cursor honours your existing .gitignore automatically.
# .cursorignore — never visible to any AI feature
.env
.env.*
secrets/
**/*.pem
config/production.yaml# .cursorindexingignore — keep the index clean, stay referenceable
dist/
build/
coverage/
**/*.generated.ts
public/vendor/The distinction matters. Putting a huge generated client in .cursorignore means you cannot @ it when you genuinely need it. Putting your .env in .cursorindexingignore means it is still one explicit reference away from the model.
What to leave out
More context is not better context. Each of these actively degrades the answer, by crowding out signal or by teaching the agent the wrong pattern.
Exclude
- Generated files and build artefacts
- node_modules and vendored code
- Unrelated services
- Historical experiments
- Deprecated code paths
- Secrets and config values
Include
- The files the change will touch
- One example of the pattern to follow
- The interface or contract at the boundary
- The test that defines success
- Which files are explicitly off-limits
Deprecated code is the subtle one. An agent shown an old pattern and a new one has no reliable way to tell which you prefer, and will often follow whichever appears more often. Deleting dead code is a context-engineering act as much as a housekeeping one.
Load the context before you ask for changes
A workflow worth adopting deliberately: spend the first turn or two building context, and ask for nothing.
Turn 1 — @src/billing/ @docs/billing-rules.md
Explain how proration works today. Change nothing.
Turn 2 — What are the edge cases around mid-cycle plan downgrades?
Still change nothing.
Turn 3 — Now: <the structured prompt for the actual change>Why it works
The retrieved context is already in the window. By turn 3 the relevant code is loaded, so the change request is answered against real code rather than a fresh, partial retrieval.
You get to audit its understanding for free. If the turn-1 explanation is wrong, you have found a misunderstanding before it became a diff — the cheapest possible moment.
The edge cases surface before implementation. Turn 2 routinely produces a case you had not considered, which then belongs in your Acceptance field.
How to do it
- Reference the files and docs with
@, and ask for an explanation only - Ask a follow-up about edge cases or invariants — still read-only
- Correct anything it got wrong, explicitly
- Only then issue the structured prompt for the change
This costs two cheap turns and routinely saves a revert. It pairs naturally with Plan mode for larger work.
Recap
Lesson 3 removes the repetition: instead of restating your standards in every prompt, encode them once as rules the agent inherits.
Check your understanding
1 / 10What does the Business Context field give the agent that Technical Context does not?