A lesson in overcorrection, from an AI-assisted builder who is scared stiff of shipping spaghetti and slop.
I am building Porch Light, an agent that watches one city's public meeting agendas for one person. I do not type the code. I direct, Kiro builds, Claude reviews, and I validate and decide.
Block Zero was supposed to answer a yes-or-no question: does the stack work?
Can I invoke a model? Can I run a Strands agent? Can I deploy it to Bedrock AgentCore? Do my logs land where I think they land?
It should have been a two-hour spike. Prove the stack, commit, move on.
Instead, it took a little over a build day.
By the end of it, Kiro was writing a byte-identity test to protect a file inside a folder I had tagged [THROWAWAY]
that morning.
And I had approved it.
My task list has that tag for code that does not survive. I invented it specifically so I would stop polishing things I was about to delete.
Neither agent asked why a folder marked for deletion needed a test at all. Kiro wrote it. Claude reviewed it against my own security rules and approved it. Both were correct about the work in front of them.
Neither one was watching the tag.
I was watching the clock.
So, in the middle of the block, I typed:
Why is Block 0 going so slow? Did we over-gate?
That question is the reason this post exists.
The lesson was not that the agents were doing bad work. It was almost the opposite. They were doing defensible work, locally and rigorously, without asking whether that level of rigor belonged there at all.
That question was mine.
Why spike at all in a hackathon? The whole genre is move fast and find out. Just start building and see what happens.
I agree with that right up to the point where nobody in my setup is typing the code.
Developers who write their own code talk about friction. Something feels wrong under your hands, the shape of it fights you, and you stop before you can say why.
I have never had that.
I came to this directing agents, so I have never built that physical instinct, and the agent does not have it either. It will build confidently on a wrong assumption for hours because it reasons locally. It is not holding the deadline, and every individual step it takes can be correct.
Mine did exactly that.
So I have to manufacture the stop.
That is what a spike is for me: a short experiment, a pass or fail written down before I start, and a chance for the wrong assumption to surface while it is still cheap.
Because the cost of a wrong assumption is not flat. Finding out at Block 5 that AgentCore does not work the way I designed for means rewriting everything already stacked on top of it, with the deadline closer and my patience thinner.
Spike B was the AgentCore deploy. One task in the plan. In practice, it was "learn a product you have never used," and the product disagreed with my architecture document in five places:
PYTHON_3_14
.@app.entrypoint
generator yielding events off stream_async()
.deploymentType: codeZip
. The generated config is a runtimes[]
array with a networkMode
field./porchlight/dev/extractor
. AgentCore names them itself and does not ask. I did not type the architecture document either, but I approved every line of it, so the five wrong calls were mine.Kiro stopped and reported each discrepancy instead of quietly adapting.
That is exactly the behavior I wanted.
Five corrections, all cheap now and expensive at Block 5.
That part I would do again.
The deploy also exposed the finding that justified the whole block.
Porch Light reads public documents. Those documents go through a model. My logging module has a redaction processor that strips document content out of log events before they reach CloudWatch, because a security rule in my steering files says logs never contain document text.
Twenty-eight tests were passing.
Property tests over generated inputs. Case-insensitive key matching. Recursion into nested dicts and lists.
Green.
Then the deploy succeeded, and we inspected CloudWatch.
The log format was AgentCore's, not the one I had in mind or had written into the spec.
The spike had used the framework's built-in logger instead of importing the designed logging module.
So the redaction processor, the size cap, the context binding: none of it had ever executed inside the runtime it existed to protect.
The tests proved the code was correct.
They proved nothing about whether the code was there.
Kiro's report called this "expected." It was expected. It was also the largest hole in the block, and the word expected is exactly how a finding turns into a footnote.
The fix took four log lines and a redeploy.
Then CloudWatch showed the schema, a [truncated:1000]
marker on an oversized field, a [redacted:document_content]
marker on a document key, and, unexpectedly, a botocore credential message carrying my run_id
and component
.
Third-party library logs were inheriting my context inside the deployed runtime.
That is when the logging design started doing something instead of describing something.
A control that has never run in the environment it protects is not a control.
It is an intention with tests.
To get the logging module into the deploy package, we vendored it: a second copy of log.py
living inside deploy/spike_b/
, with a note in the docstring to keep it in sync manually.
Manual sync of a security control is not a control either.
So we wrote a sync script that copied the file verbatim, plus a test in the default suite asserting the two files were byte-identical. Drift would fail within one test run.
Then we found that uv.lock
did not govern the deployment at all. AgentCore resolved dependencies server-side from the deploy package's own pyproject.toml
, and my two manifests had already drifted apart.
One said `structlog>=26.1.0`
. The other said `>=24.4.0`
.
Both were unpinned floors on the library whose behavior supported my security control.
So: exact pins, and another test asserting the pins matched the lockfile.
Every individual decision was defensible.
Every individual decision was also protecting deploy/spike_b/
, a folder that stopped existing when Block Zero closed.
I have a rule that says stub, do not build. My PRD uses MUST
, STUB
, and NEVER
to control sequence and scope.
Block Zero treated everything as MUST
.
That was not an agent failure.
That was mine.
I do not want judges, my peers, or me a year from now opening the repo and finding something thrown together without care. I want to be taken seriously as a builder, and the fastest way to lose that is one look at the code.
I also came to this from operations, where the instinct is to build a control for the thing that happens once in a thousand times, and where nobody gets credit for the incident that did not happen.
That instinct is often right.
It also erects its own barriers.
Here it cost me build hours on a spike and consumed tokens like Jabba the Hutt working through a bowl of Klatooine paddy frogs.
No bueno.
The complicated part is that more than one thing is true at once. The rigor is not only fear. The PRD, and the MUST
, STUB
and NEVER
labels, are how I direct. They are the vision made specific enough for an agent to build from. Stub, do not build is a decision about sequence, not a polite way of never doing something.
I want the output to be good and I want it to be recognizably mine.
Both of those are true, and neither one of them tells you when to stop.
This is the part I keep turning over.
Vendoring the module was the available way to get it into the deploy package. Replacing a manual sync promise with a byte-identity test is better engineering than trusting a comment. Pinning a security-relevant dependency exactly is better than an unpinned floor.
Ask any of those questions in isolation and the answer is yes: do the rigorous thing.
Nobody asked the question that was not in isolation:
Should this folder be getting this treatment at all?
Both agents were reasoning locally and reasoning well.
Kiro proposed correct fixes to problems that were there. Claude reviewed them against my security rules and approved them.
Neither stepped back to check the tag on the folder, because neither was watching the calendar with any stake in it.
I was.
I had a submission date and a calendar I was already behind on.
That is not a story about agents being bad at their jobs. It is a story about what the human is for in this setup.
I direct. The agents generate. I validate and decide.
I have said that a lot. Block Zero was the first time I could point to the specific sentence that only I was positioned to say.
The durable version became a rigor budget:
I would rather encode the question than have to keep remembering to ask it.
One more from that block, and my favorite.
My AWS account ID was sitting in three tracked files, and the repo goes public for judging. An account ID is not a credential. It is closer to a street address: harmless alone, useful to someone aiming.
So we gitignored the deploy state file, gave the target config the .env
treatment, redacted the number out of the spec prose, and ran git-filter-repo
over the whole history.
The verification pass failed. The check kept finding the account ID.
It was in the documentation of the scrub. The number had been written into the prose explaining what we were removing, so the search kept hitting the sentence describing the search.
Block One was the product itself: the adapter that reads Ventura's agendas.
It went fast, partly because Block Zero had already found everything AgentCore was going to disagree with.
We put a ten-minute compliance gate at the front. Read the city's robots.txt
and terms of use before writing a single requirement.
Ten minutes bought three things.
Ventura is two vendors, not one: a CivicPlus front door over a Granicus agenda system, different companies and different hosts.
The Granicus host's robots.txt
reads Disallow: /
for everyone, so it is off limits. I obey that even though robots.txt
is a convention rather than law. A tool whose whole claim is trustworthy reading of public records cannot quietly override a public body's stated preference.
And the only structured API in the setup sits on the host we are not allowed to touch, which killed the shortcut I was hoping for.
Then the block ran clean.
Fifty-seven tests passing.
Property tests on the parser: never crash on malformed rows; every emitted meeting carries a valid date and at least one document URL; unparseable rows get surfaced instead of dropped.
All true.
All green.
The last task in the block was a hand-check: take a known week, pull what the parser produced, open the live site, and compare by eye.
Every single meeting had its documents listed twice.
The rows linked each file from both an icon and a text anchor, and the parser faithfully collected both.
The property tests could not catch it because every property they asserted was satisfied.
A duplicate URL is still a valid URL. The meeting still has a date. Nothing crashes.
Green suite. Wrong output.
The only thing standing between that bug and Block Two was a person looking at a screen.
It was the same shape as the redaction gap, produced by a different mechanism.
The redaction tests proved that the control worked but not that it was deployed.
The parser tests proved that the output satisfied its properties but not that the output was right.
A test answers the question you thought to ask when you wrote it.
Property tests answer a wider version of that same question.
Neither decides whether you asked the right question.
Two blocks left me with three habits, and they cost wildly different amounts.
The compliance gate: ten minutes before implementation. It killed a bad architectural shortcut before I built around it.
The hand-check: twenty minutes after the automated suite. It caught a bug a hundred more assertions about the wrong properties might not have found.
The rigor budget: hours learned the expensive way. Full rigor for things that survive, working rigor for features, spike rigor for throwaways.
The first asks whether I am allowed to build the thing the way I intend.
The second asks whether the thing I built is right.
The third asks how much engineering this piece of work deserves.
The agents can help answer all three.
But someone has to remember to ask.
That, increasingly, is what I think my job is.
I came to code from the courtroom: jury services to AI builder, self-taught and learning in public. I direct, the agents generate, I validate and decide. I build the Clew Suite and a handful of civic-tech tools.
I used to think the important part of that sentence was validate.
After these two blocks, I think it may be decide.
AI Assisted. Human Approved. Powered by NLP.