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.
Spike B's job was to be wrong
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:
- The design said direct code deploy, a zip, no Docker. The CLI runs CDK underneath.
- It pinned Python 3.12. The generated runtime config said
PYTHON_3_14. - It described a synchronous handler returning a string. The shipped pattern is an async
@app.entrypointgenerator yielding events offstream_async(). - It specified
deploymentType: codeZip. The generated config is aruntimes[]array with anetworkModefield. - It planned CloudWatch log groups named
/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.
A tested control that never ran
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.
Then we hardened the throwaway
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.
The agents were correct. The question was mine.
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:
- Full rigor for code that survives.
- Working rigor for feature code.
- Spike rigor for anything tagged throwaway: does it work, yes or no, commit, move on. It lives in my steering files now, so it applies to the agents instead of only to my intentions.
I would rather encode the question than have to keep remembering to ask it.
The scrub failed because the documentation quoted the number
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.
Tests answer the questions you thought to ask
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.
Three habits I am keeping
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.
Top comments (12)
The byte-identity test has the same shape as the 28 that passed. It proves the vendored
log.pymatches the original, not that the deployed runtime imported it instead of the framework's built-in logger, which is the gap that made the redaction processor a control that never ran. The thing that did prove it was the accident:[redacted:document_content]showed up in CloudWatch, and that marker cannot exist unless your processor executed inside the runtime. Asserting that marker in the deployed log stream after each deploy is the check CI cannot be, as long as the smoke event carries a key worth redacting.You're right, and it stings a little. The byte-identity test answers a different question. It proves the two copies match, not that the runtime imported mine instead of the framework's.
The marker is the only thing that proved it ran. Adding that assertion to the post-deploy check.
Funny part: I already do this for a different control. My containment test invokes the deployed agent with a tool that is not on the allowlist and asserts the block shows up in the log stream. Same pattern. I applied it to the allowlist and not the logger.
This hits so close to home. The hardest thing to discipline when directing coding agents is their instinct to build enterprise-grade scaffolding around a throwaway spike.
When an LLM sees a prompt to "verify Bedrock invocation", it immediately wants to generate structured config layers, custom exception hierarchies, and retry policies before it even checks if the raw payload returns cleanly.
We started enforcing a strict rule for exploratory spikes: zero abstraction allowed in Phase 0. Single flat script, direct credentials check, single invocation, assert output, print receipt, exit. Once raw execution is proven against reality, only then do we let the agent touch architecture.
Thank you for sharing. Yes, this is something I'm starting to realize. Kiro and Claude go full bore unless I restrain them, and my mistake was taking their word on what best practice meant for a throwaway. Your zero abstraction approach is very helpful and would have saved me the sprawl.
The distinction between “the work is correct” and “this is the right work to be doing” is important.
An agent can rigorously implement and review the task it has been given while completely missing that the task deserves a different level of investment—or should not exist at all. That makes intent, constraints, and the definition of “enough” part of the engineering work, not just preparation for it.
I also like your idea of a rigor budget. The human shouldn’t have to second-guess every line, but someone does need to decide what level of evidence the decision warrants and whether the agents are still solving the original problem.
Thank you for reading! Yes, I'm still mulling over how to build towards or implement a rigor budget. I'm not sure if I can set gates or hard rules or... I use Kiro a lot for my IDE so maybe add a steering doc to help.
A steering doc could help, especially if it defines different levels of rigor based on whether the work is exploratory, temporary, or production-bound. The human still decides which level applies.
I'm going to try it on my next build. Interested to see how it goes!
This is a fantastic write-up. You’ve hit on the exact two failure modes the architecture community is currently circling.
Your logging module tests passing 28/28 while the code was never deployed is a textbook Empty Set Trap. The mechanical checks were green, but the population of executed code in production was zero. "A control that has never run in the environment it protects is not a control" — that is the exact boundary between testing syntax and testing semantics.
The duplicate URL bug is the other side of the same coin: a cryptographically perfect suite over wrong output. A duplicate URL is mechanically valid, but semantically wrong. No property test can catch it because they ask "is this a valid URL?" not "is this the correct representation of reality?"
Your conclusion that the human's role is to "decide" is spot on. Agents generate; humans define the semantic boundary. Great post.
Thank you! I appreciate it.
Empty Set Trap is a good name for it and I am stealing it. lol
One thing I am chewing on: would a "no duplicate document URLs per meeting" property have caught this? I think it might have, which makes me wonder whether the limit was the property tests or my own imagination about what could go wrong. Curious what you think.
And yes on the control. I went into this build determined not to fall into that exact trap, and fell in anyway.
You found the exact boundary, and your phrasing is better than mine: the limit was neither the property tests nor your imagination — it was the gap between them. Property testing as a technique covers any property you can state; the suite covered the properties you stated. "No duplicate document URLs per meeting" was stateable, so it was reachable — and the fact that you reached it two days later, from one comment, is the honest measure of how cheap it was. That is not a failure of the method; it is the method working: the hand-check found the miss, the miss became a property, the property now runs forever. The bug cost you twenty minutes and bought a permanent assertion — that trade is a win, and it is worth writing down exactly that way, because most people only publish the case where the method wins.
On Empty Set Trap: steal freely — it is yours now. Terms earn their keep by being useful in other people's mouths, and you just gave it its first independent deployment, plus the better closing line: "I went into this build determined not to fall into that exact trap, and fell in anyway." That sentence is the whole phenomenon — knowing about a trap is not immunity to it. It is the same reason checklists have items for things everyone knows
Some comments may only be visible to logged-in visitors. Sign in to view all comments.