The method
How a human and an agent built a browser extension against an undocumented, changing target over five weeks, and what made it work. This is the part that transfers. The extension is in guide 02, the parsing in 03, the storage in 04 — but if you only read one of these, read this one, because the method is what produced the rest.
Written 2026-09-08, from a project at version 0.16.0: ~3,400 lines of extension source, 176 tests, 33 numbered lessons, 12 capture runs. Every incident described here happened, and the numbers quoted are measured rather than illustrative. These are deliberately generic — the original target is never named; it appears as "the target" and "the API". What transfers is the method, the architecture and the failure modes, not the parsers.
1. The shape of the collaboration
The division of labour was never negotiated. It fell out of what each side can actually do:
| The human | The agent | |
|---|---|---|
| has | a real browser, a real account, real data | the ability to read 100 MB of JSON and not get bored |
| does | drives the target, captures a run, reloads the build | writes the code, forms hypotheses, tests them |
| decides | what matters, what to try next | what the evidence supports |
| cannot | read a 3 MB payload by hand | see the screen, click anything, hold an account |
The agent never touches the target. Not as a safety rule — as an architecture. The extension records responses the browser already received and originates no requests of its own. That single constraint decided most of what follows: it means every fact in the dataset is something the target volunteered to a real user in a real session, and it means the agent's job is reading, never fetching.
The loop is one run long.
human browses with the recorder on → exports a run → hands the agent the file
↓
agent parses it, finds what is there and what is missing, writes it up
↓
agent ships a new version of the extension addressing what the run exposed
↓
human pulls, RELOADS, browses again → the next run tests the last fix
Each turn of the loop takes a day or two and produces two artefacts: a new version of the tool, and a permanent note about what was learned. Neither is optional. A run that produced code but no note leaves the next session to rediscover it; a run that produced a note but no code leaves the tool unable to capture the thing next time.
Why "phase 0" was the best decision in the project
The first build was deliberately stupid. It had no parsers, no model of the domain, no opinion about what the data meant. It recorded response bodies to disk and did nothing else. Its own README opened with "This is not the product."
That is the decision to copy. The instinct is to write the real extension — the one with the features — and discover the payload shapes as you go. Doing it the other way round means:
- Every parser you eventually write is written against captured evidence, not a guess.
- When a hypothesis is wrong, you re-derive from stored bodies for free instead of asking the human to reproduce a browsing session.
- The recorder keeps working when the target changes; only the parsers break.
Concretely, this paid off three times in five weeks. Twice a conclusion was overturned and the fix was a re-run of the deriver over data already in hand. Once a feature added in week five found events sitting in a capture from week three that the week-three code had walked straight past. None of that is possible if you only keep what you understood at the time.
Keep the evidence, derive the meaning. Two directories, two formats, one direction of travel: verbatim captures in, derived files out, and the derived files are always regenerable.
2. Evidence discipline
The project's stated rules, in the order they earn their keep:
Fail closed
Anything unrecognised is counted and skipped. Never guess an identity, never invent a timestamp. A record we could not attribute is reported as unattributed — not merged into whoever was nearest.
This sounds obvious and is violated constantly by well-meaning code. foo || 0 turns "we never saw this" into "this is zero". res || {} turns "no answer came back" into "an answer with nothing in it" — that one cost two capture runs, see guide 02. The rule is: missing is null, and null never becomes 0, '', or {}.
Verify against an artefact the system itself produces
This is the single highest-value habit in the project. Every claim that survived was checked against something the target emitted independently; every claim that collapsed was plausible and unchecked.
Worked example. Object ids turned out to embed a creation time in their high bits — shift right by a fixed number and you get epoch milliseconds. That is a derivation, not a field, so it needs proof. Three independent checks, all against the target's own output:
- Compared against a pagination cursor the target generated — 27 ms apart.
- Compared against the target's own rendered relative-age strings ("6 h", "2 d") — six for six.
- Later, an id minted by the server during a write, compared against the recorder's own clock at the moment of capture — 0.269 s, which is a network round trip.
Two independent clocks agreeing to within a round trip is about as good as this kind of evidence gets. One clock agreeing with itself is not evidence at all.
Read the markup, not the English
Where a payload renders a sentence with a name in it, the name's character range is usually marked in the payload — an attribute span, a component key, a region name. Read that. Parsing the sentence works until the string is localised, pluralised, or A/B tested.
The generalisation: prefer the structural marker the system uses to lay out the page over the human-readable text it lays out. When two things are indistinguishable by component and typography, look for the system's own name for the region containing them. It is nearly always there, because the system needs it too.
One sample gives you an example, not a grammar
A pattern written against a single captured example matched two of the next five cases. The identifier it parsed had three spellings in the wild and one of them was not a spelling variant at all — it was a different type of object arriving through the same endpoint.
Two distinct mistakes wearing the same coat, and they need opposite fixes: a spelling variant means loosen the pattern; a different type means branch, because loosening would have produced a parser that builds a valid-looking URL pointing at nothing.
When the sample is a structured identifier with a type in it, read the type as data rather than baking it into the pattern.
A completeness statistic is only evidence if completeness is independent of what you measure
The worst error in the project. A conclusion rested on a sample of 70 payloads "59 of which were whole". The 11 that were truncated were the only 11 large enough to contain the thing being looked for — the truncation limit selected against the evidence, perfectly.
"59 of 70 were whole" and "the 11 that were not are the only ones that could have held the answer" are the same sentence about the same run, and only one of them is worth saying.
Whenever a cap, a limit, a page size or a sample boundary is in play, ask whether it selects for the thing being tested. And when a fix for such a limit has shipped, check the build stamp of the run you are about to call the recapture — see guide 02, because this is exactly why builds stamp themselves.
Fail loudly at the boundaries, quietly in the middle
Skipping an unrecognised record is correct: it is counted in an unparsed tally and the run continues. But a delivery that fails must be loud, because there is no tally that will catch it. The distinction is whether the failure is about data you did not understand (quiet, count it) or about a mechanism that did not work (loud, stop).
3. Corrections stay where the mistake was
Every doc in this project carries its own errata. A falsified claim is not edited away — it is struck through or annotated in place, at the point it was made, with the correction next to it.
**CORRECTION (date).** The paragraph that used to stand here said X, on the stated guess
that Y. The first real capture answered it: they are not. Across 17 payloads, ...
Three reasons this is worth the ugliness:
- A confident wrong claim in a doc is a trap for the next reader, including the next session of the same agent. Deleting it removes the trap but also removes the warning.
- The reasoning that produced the error is reusable. "Statistic that selected against its own evidence" is a failure mode you will meet again; "we were wrong about payload shapes" is not.
- It calibrates the rest of the document. A doc with visible corrections is one you can trust the uncorrected parts of.
The project keeps a running "what we have been wrong about" section in its handover doc, and numbered lessons in a NOTES.md that is append-only. Thirty-three entries in five weeks. The numbers get cited from code comments — a parser that does something surprising says see NOTES #31 and the reasoning is one click away, permanently, instead of living in a chat log nobody will search.
Write the note when the lesson is fresh, not when the feature is finished. Half of these entries would not exist if they had been deferred to the end of the session.
4. Version every change, and make the build say what it is
Every behaviour change gets a version bump and a changelog entry that explains why, with the measured evidence in it, not just what changed. The changelog is the project's narrative spine; a reader can go from "why does this do that" to the run that caused it in one hop.
This became non-negotiable after a silent failure that cost ten days. Files on disk were updated twice, but a browser extension keeps running the build it loaded until it is explicitly reloaded — so the captures kept coming back as if the new code did not exist, looking exactly like a quiet week on the target.
The structural fix, and the transferable one: every artefact the tool produces states which build produced it. Not "when was it exported" — which build, loaded when. Diagnosing the original incident required inferring a version from the wording of an internal reason string, which worked and should never have been necessary. Provenance about the tool belongs beside provenance about the data.
Corollary: the UI shows the running build, and turns red when the build has been loaded for more than a few days. Both surfaces show it, because the one the human opens first is the one that has to tell them.
5. Tests, and what they are for here
The test suite is zero-dependency and grew to 176 tests. Its job is not coverage. Its job is to pin the lessons so they cannot be un-learned by a later edit.
Read the test names and you get the project's history:
extractor: the person is named by the page URL, not by the payload
extractor: a section heading is structure, not something the person wrote
extractor: with no skills control, nothing is guessed at
db: an export with no blob reports null contents, never an empty file
db: archiving a 113 MB run never evicts the run being archived
parser: a card with no id yields nothing, never the previous id
Each of those is a bug that happened, or a rule that was hard-won. A test named test('handles null input') pins a behaviour; a test named the label goes with the value pins a decision, and tells the next reader why the code looks the way it does.
Two practices worth stealing:
- Fixtures are hand-authored and synthetic, mirroring the structure of real payloads with obviously fake identities. Real captures stay in the private data store and never become test data. This is a privacy rule that turned out to be a design rule too: writing a synthetic fixture forces you to state what the structure actually is.
- The runner auto-discovers test files. It used to have a hand-maintained import list, and a new test file sat there for two versions passing vacuously because nothing imported it. The fix includes a guard that exits non-zero if it discovers zero files, because "no tests found" and "all tests passed" must never look the same.
6. Reproduce before you theorise
The most recent bug looked exactly like a storage-eviction problem: the caps were tight, a run had just added a very large file, and the newest file was missing. The theory fit every observed fact.
It was wrong. A fifteen-line script against the real storage function with realistic sizes disproved it in one run — eviction walks oldest-first and could not reach the newest entry. The reproduction cost less than the theorising had already cost, and without it the "fix" would have been a rewrite of eviction with the actual bug untouched.
When you have a theory that fits, the cheapest next move is usually to try to falsify it with fifteen lines, not to act on it. Especially when the theory is comfortable.
7. What to do when the session dies
Agent sessions are ephemeral; the work must not be. This project survived a container reset that destroyed every local file mid-task, and lost nothing, because of three habits:
- Commit and push at the end of every meaningful unit, not at the end of the session. The session does not get to choose when it ends.
- The handover doc is written for a stranger, and updated as part of the work rather than as a farewell. It states the data contract, what is settled, what is open, and what the project has been wrong about.
- Nothing important lives only in the conversation. If a decision matters, it is in a numbered note, a changelog entry, a test name, or a code comment — four places that travel.
The one thing that did not survive was access: the credentials to reach the stores were in the destroyed container. Expect to ask the human for those again, and design so that is the only thing you need to ask for. The general handover discipline this points to — HANDOVER.md for a stranger, the session loop, surviving a reset — belongs to teams.sgit.ai; the code/data split specific to a capture project is covered here in guide 04.
8. The failure modes to expect
Ranked by how much time they cost here. Full page: the six silent failures.
| Failure | Looks like | Guard |
|---|---|---|
| Running an old build | A quiet week on the target | Stamp the build into every artefact; show it in the UI |
| A limit that selects against your evidence | A clean, confident, wrong conclusion | Ask what the cap excludes before trusting a sample |
| A boundary that fails only when data gets big | Worked for months, then a corrupt file | Never pass bulk data through a channel with a size limit |
| A pattern written against one example | Silent under-collection; no error anywhere | Read types as data; count what you skip |
| A test file nothing imports | Green suite, zero assertions | Auto-discover; fail loudly on zero found |
| Deriving a fact from the thing it points at | Plausible timestamps that are all wrong | Verify against an artefact the system produced |
Every one of these is silent by nature. That is the pattern: in this kind of work, the expensive bugs do not throw. They return something reasonable-looking, and you find out weeks later when a number does not match a screenshot.
Which is the last rule, and maybe the first: the human looking at the real screen is a test oracle you cannot replicate. When they say "that doesn't look right", it is usually cheaper to believe them first and check second.