A kill condition for a personal tool
I gave a personal tool 42 days and written conditions for killing it. Ended up stopping the clock on day three.
It all started with an itch. An itch to build, an itch to “optimize” my day to day. But this itch was different from the usual ones, and I made sure of it by attaching the kill condition to it.
Why bother with a kill condition at all
Personal tools don’t fail loudly. Nobody sends you a churn email about the app you built for yourself. It’s happened to me twice already: I open the thing a little less each week, I tell myself I’m “still using it sometimes,” and eventually it’s a folder I don’t click. Nothing ever calls it dead, so I never have to. Coming to that conclusion was not a fun time to say the least.
A kill condition is how you stop lying to yourself. Before I had any evidence, I wrote down the conditions under which I’d call the thing dead, and what I’d have to admit if they weren’t met. That way, when the evidence arrived, I wouldn’t get to move the goalposts, because I’d already written where they were.
All of that went on paper before I wrote any code.
The bet, and why it needed two metrics
Ivy’s premise: I capture raw text (“lunch 85dh, split with a friend, he paid”), an interpreter turns it into structured records, and the views over those records are specs the model authors, which I can then reshape by typing a command. “Group my spending by who I paid.” “Show only this month.” The interface is supposed to bend to me instead of the other way around.

The same spending view, before and after one command: “group my spending by who I paid.”


The idea of rearranging the interface post-development was just the natural next step for me after I tried building a scheduling app that gave each context of my life (work, leisure, workout) a time budget and depleted it while my focus sat in that context, with a habit tracker embedded, and some self-improvement nudges here and there. Twice. Both times I stopped using it after a few weeks because each time I didn’t like something, I had to open the code and change it. That friction was enough to make me stop using it, and I wanted to see if I could build around that.
That’s the primary bet: an LLM can reshape an interface from plain English reliably enough that when the defaults stop fitting, I reshape them instead of tolerating the misfit or abandoning the tool.
One metric can’t test that, because there are two ways to fail and they look different:
- Capture. Do I actually use it? If I stop dumping text into it, nothing else matters. The tool is dead regardless of how clever the views are. Wrongness is survivable here; abandonment isn’t.
- Reshaping. Do I actually reshape the views? Because if I capture faithfully but never reshape anything, the tool survived as an inbox and the actual bet failed. It just failed quietly, wearing the costume of success.
Passing on capture alone is the trap. So both had to be measured, separately.
The trap I could see coming
Here’s the problem I knew about in advance. A low reshape count has at least three explanations, and only one of them is failure:
- Defaults are bad, so I reshape constantly. (Bet passes.)
- Defaults are decent, so I reshape when life changes. (Marginal.)
- Defaults are good enough that I rarely need to reshape at all. (Reads as failure. Might be success.)
I’m user number one. I built the defaults, for my own life, by hand. So they fit unusually well by construction, which means world three was not a hypothetical for me, it was the likely one. And world three is a success by one reading and a kill by the metric. Same number, opposite verdicts.
I did not trust myself to pick between those readings after seeing the number. Once I’ve seen a result I don’t want, every instinct bends toward the interpretation that lets me keep going. So I wrote the tiebreaker down first.
The discriminator is a thing I call the signals harvest: a cursored, read-only endpoint that collects everything the app couldn’t do. Captures it parked because no schema fit, reshapes the validator rejected, and walls I hit. As such, this is the rule I ended up with before the collection started:
- Low reshapes, healthy capture, low correction volume, and a thin harvest. The seeds fit. Not a malleability failure.
- Low reshapes and a fat harvest of parked captures and rejections and walls. The command bar failed. That’s the real failure.
Same reshape count either way. The harvest tells me which world I’m in.

The metric that lied to me
While the clock was running I was reading the metrics code for an unrelated reason, and I found a bug in the exact number the kill condition depended on.
When the model emits a view spec, it gets validated before anything renders. An
illegal spec is rejected and the screen doesn’t change. The function computing my
kill metrics, getKillMetrics, was counting those rejected reshapes as reshapes.
// Before: every reshape_command event counted, rejected or not.
const reshapeEvents = dated.filter((event) => event.kind === 'reshape_command');
malleability: {
reshape_commands: reshapeEvents.length,
// ...
}
// After: attempts and successes split, and the gap between them
// becomes its own number.
const reshapesApplied = dated.filter((event) => event.kind === 'reshape_applied').length;
malleability: {
reshape_commands: reshapeEvents.length, // attempts, rejections included
reshapes_applied: reshapesApplied, // attempts that produced a new view version
rejection_rate: reshapeEvents.length === 0 ? 0 : reshapesRejected / reshapeEvents.length,
// ...
} So the half of my kill condition that measured the whole bet was reporting the bet’s failures as evidence it was working. Every time a reshape didn’t work, the success number ticked up.
It had been lying, in my favour, the entire time the clock ran. I only caught it by accident.
The fix was to stop reporting one number and report three: attempts, applied, and the rejection rate. “Did I try to reshape” and “did it work” are different questions, and the gap between them is the signal I actually wanted in the first place.

A test would have caught it, sure. But what I actually took away is that any instrument I build to tell me to quit is going to lean my way, because I built it and some part of me wants to pass. I treat my own metrics as biased in my favour now, by default.
Stopping on day three
I’d initially budgeted 42 days for this, but ended up using only three of them.
I stopped because I read Ink & Switch’s Malleable Software essay, and it told me my bet was worded backwards.
My bet said I’d reshape views “instead of accepting defaults,” which frames malleability as the opposite of good defaults. The essay’s argument is that malleability is a gentle slope from user to creator, and passive use of good defaults is the ground floor of that slope, not its enemy. A spreadsheet is useful before its first formula. HyperCard’s browse-only level is a supported place to stand, not a failure to climb. Tailoring is what happens when the defaults stop fitting, so the defaults had better be good enough that it takes a while.
That reframes my whole result. “Reshaping went quiet” stops being evidence the bet failed and starts being evidence the seeds fit, which is exactly what the thin signals harvest was already telling me.
And once the question changed, the remaining 39 days would have been measuring the old question. Running the clock to term just because I’d set it, after I knew it was asking the wrong thing, would have given me a number, not an answer. So I stopped it.
The verdict, stated plainly
Capture passed, with a caveat. My own condition said “still capturing daily after ~6 weeks,” and three days of daily capture is not six weeks. What I can honestly claim is that capture stuck immediately and never wobbled, which is a strong early signal, not a met condition.
Malleability is inconclusive, not failed. The reshaping did go quiet, the harvest came back thin, and the rule I wrote in advance says that reads as “the seeds fit” rather than “the bet failed.” Inconclusive is a worse outcome than a clean pass and a better one than a lie, and it’s the honest one.
Why v0.2 has no clock
v0.2 is open and I have not set a kill condition for it. On purpose.
A kill condition invented before I know what I’m betting on is just a number I picked because I’ll pass it. Right now I can’t state v0.2’s bet precisely enough to falsify it, so any condition I wrote today would be theatre. When the bet has a name, it gets its own clock, minuted like everything else.
What I’m actually chasing, in plain terms: I capture into Ivy every day and I’ve stopped reshaping it, and not because the command bar fails. Because I can’t imagine what to ask it for. The defaults fit, and a tool with a habitable ground floor and no visible stairs is still a trap. The open question is how to make the range of things a tool could become visible, without the tool nagging me. I don’t have an answer. That’s the next thing.
The whole reason I could be honest about any of this (stop early, call a result inconclusive, admit the metric lied) is that the rules were written down first. That’s the only thing standing between “I stopped because the question changed” and “I stopped because it was going badly and I needed a story.” Write the rule before you read the number. It’s the cheapest honesty you’ll ever buy.