"Make the CSV import better" is a real kind of request. It shows up in a backlog after a few support tickets, and nobody can finish it. An engineer could rewrite the parser, add a preview screen, or support three more file formats, and the person who asked still might not see their problem fixed. A coding agent given the same sentence will happily do any of those.
This tutorial turns that sentence into one task with a single outcome and a test that decides it. It uses a template with two halves: one for the agent, one for the person running and accepting the work. The example is a small fixture I wrote for this post. The test output in it is real.
Step 1: find the observation behind the request
Vague requests are usually a summary of something specific. Ask the person who wrote it what they saw, and get one example.
In this example, the support lead comes back with this: customers who export contacts from a spreadsheet app with "CSV UTF-8" get Missing column: email, even though the first column is clearly called email. Saving the same data as plain CSV works.
That's a byte-order mark. Some spreadsheet apps write three bytes (EF BB BF) at the start of a UTF-8 CSV. The importer reads the first header as \uFEFFemail, which isn't email.
Now there's something to test.
Step 2: write the outcome so nobody has to interpret it
One sentence, no "better", "cleaner" or "more robust":
A UTF-8 CSV that starts with a byte-order mark imports the same rows as the same file without one.
Then add the three things that keep the task small:
- Check: the command that decides it, and what it should report.
- Keep: behaviour that already works and must not change.
- Out of scope: the nearby problems you're deliberately not solving yet.
The out-of-scope list matters more than it looks. "Better import" also covers semicolon-separated files, UTF-16 exports and quoted commas. Writing them down stops the agent from taking them on, and gives you three candidate tasks for later.
The template
Here's the whole template. The top block is the shared task card. The agent gets the full working context, and the human gets a short runbook for what only a person can do.
TASK CARD
Outcome: <one observable sentence>
Check: <command> -> <expected result>
Keep: <behaviour that must not change>
Out of scope: <nearby problems, listed on purpose>
Prepared by: <who wrote and owns this task>
Run by: <who claims it, with their own agent, checkout and credentials>
Accepted by: <who decides whether the delivery is done>
AGENT INSTRUCTIONS
Context: <files, functions, fixtures>
Change: <what may change>
Do not: <what must stay untouched>
Stop and ask: <conditions where the agent hands back control>
Deliver: <diff, commands run, their output, known limits>
HUMAN INSTRUCTIONS
Before: <how to start: branch, workspace, data you may use>
Watch for: <what only a person can notice or decide>
Verify: <the check you rerun yourself before delivery>
Escalate: <who to ask, and when>
The three roles are on the card on purpose. The person preparing the task is not always the person running it, and the person accepting it should be able to say no for a concrete reason. The runner uses their own access. If they don't have it, someone with the right permission grants it or takes the task. No one shares a login or token with the agent.
The worked handoff
Filled in for the import bug:
TASK CARD
Outcome: A UTF-8 CSV that starts with a byte-order mark imports
the same rows as the same file without one.
Check: npm test -> 4 of 4 pass, including
"UTF-8 file with a byte-order mark imports both rows"
Keep: plain CSV imports; quoted/capitalised headers import;
a file with no email column is still rejected with
"Missing column: email"
Out of scope: semicolon delimiters, UTF-16 files, quoted commas,
any UI change
Prepared by: support lead (wrote the repro) + backend engineer
Run by: backend engineer, own checkout, own coding agent
Accepted by: support lead, who re-imports the customer-style file
AGENT INSTRUCTIONS
Context: src/importContacts.js, test/import.test.js, fixtures/
Change: importContacts() only
Do not: change parseHeader() output for files without a BOM,
change error messages, add dependencies
Stop and ask: if the fix needs a CSV library or touches the UI
Deliver: diff, npm test output before and after, limits
HUMAN INSTRUCTIONS
Before: use the fixtures only, never a real customer export
Watch for: the agent "fixing" more than the BOM case
Verify: run npm test yourself; open the diff, expect one line
Escalate: anything about other encodings goes back to the
support lead as a new task
The fixture has four tests: the new case and the three "keep" behaviours. Before any change:
✖ UTF-8 file with a byte-order mark imports both rows
✔ plain UTF-8 file still imports
✔ quoted, capitalised header still imports
✔ missing email column is still rejected with the same message
ℹ tests 4
ℹ pass 3
ℹ fail 1
AssertionError [ERR_ASSERTION]: Missing column: email
That failing test is written before the fix, by whoever prepares the task. If it doesn't fail, you haven't reproduced the complaint, and the task isn't ready.
The fix is one line in importContacts(). It strips a leading BOM before splitting the file into lines:
- const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
+ const lines = text.replace(/^\uFEFF/, '').split(/\r?\n/).filter((l) => l.length > 0);
After:
✔ UTF-8 file with a byte-order mark imports both rows
✔ plain UTF-8 file still imports
✔ quoted, capitalised header still imports
✔ missing email column is still rejected with the same message
ℹ tests 4
ℹ pass 4
ℹ fail 0
One detail I ran into while building the fixture: my first version of the importer called trim() on every header, and trim() already removes \uFEFF, so the bug didn't reproduce. An importer that compares header names without trimming them shows the bug. That's another reason to write the failing test first instead of trusting a description of the bug.
What the delivery should contain
The runner's report back to the acceptor is short, because the task card already says what to look at:
Changed: src/importContacts.js (1 line)
Ran: npm test (before: 3/4, after: 4/4)
Not done: other encodings and delimiters (out of scope)
Known gap: only a BOM at the very start is removed
The support lead doesn't need to read the parser. They re-import the customer-style file, see two contacts, and accept. Or they find something else, and that goes back on the same task with a concrete reason, rather than into a new "make it better" ticket.
Where to keep the template
Put it where the task lives, not in a chat thread. Most AI agent task management software has a description field, and that's enough for the task card. The agent and human halves work best as two separate fields, because they have different readers. Wagglet's write-up on writing one task with agent instructions and a human runbook explains that split in more detail.
Limits
- The check proves one thing. Four passing tests show the BOM file imports and three existing behaviours didn't change. They say nothing about the import being "better" in any wider sense.
- The fixture is small and made up. The importer is an 18-line toy written for this post, not production code. It doesn't handle quoted commas at all, which is fine here only because that's listed as out of scope.
- The roles are an example. In a small team, one person may prepare and accept. Keep acceptance with someone other than the runner if you can.
- Shrinking the task is a judgment call. Someone decided the BOM was the complaint worth fixing first. If that guess is wrong, a perfectly bounded task still fixes the wrong thing. Confirm the observation before writing the card.
This post was written with AI assistance and reviewed by me. The fixture and its test output are real and were run for this post.
Top comments (0)