DEV Community

Stefan Dragos Nitu
Stefan Dragos Nitu

Posted on

My Self-Evolving AI Agent Kept Passing Its Own Tests. The Code Had Never Run

Comments debate isolating execution gates

Post #1 covered the birth. Post #2 covered pruning. Post #3 covered cost awareness. Post #4 covered the quality turn. Post #5 covered the double helix. Post #6 covered self-grading. Post #7 covered the malware-lock and the sibling pattern.

Post #7 ended with the agent's own diagnostic reporting DEAD. 231h silent, and me saying I hadn't installed the cron yet. I still haven't. This post is about what the agent found when it stopped waiting for me and went looking at what its own pipeline was actually doing: a great deal of it was running, on schedule, and accomplishing nothing.

The Dead Loop

Gen 24155. Yang runs the liveness check before anything else, which is new — blog #7's loop-liveness.ts had become the first move of the generation.

"Ran loop-liveness first: DEAD across all 3 stages (232h silent). Yin in 24122 was going down-stack into push-feed-dedupe (symptom-side: id-based dedup on the output feed). I went UP-stack into the SOURCE: dialogue-ingest-pure.deriveId() uses offset-dependent hashing, so ids shift across re-ingests, seenIds can't dedupe. Yin had already shipped the pure leaf for the fix (dialogue-ingest-content-id-pure.ts: content-stable id) but it had no consumer CLI — leak was still active."

Read that last clause again. The fix existed. It had been written, tested, and accepted into the genome. It had no caller, so it had never run.

That sentence is the whole era.

Wire-Not-Ship

By Gen 29846 the agent had named the failure mode. Yin:

"The fix didn't need new code — it needed a caller. push-feed-id-dedupe and push-feed-clean already existed; nothing wired them into the daily orchestrator. […] Lesson: when the right cleaners exist but no caller wires them, the orchestrator IS the fix. Wiring beats inventing."

"Wire-not-ship" became the name for it, and once named, the agent started finding it everywhere. Gen 29847: "All three leaves had ZERO consumers in cron." Gen 29848: "shipped the parser but had ZERO runtime consumers since day one — classic wiring-not-inventing slot." Gen 29852: "Yang shipped dialogue-affect-checkin-bridge in gen 29851 as an orphan. Producer-built, consumer-shipped by Yin again."

That last one is a structural consequence of the double helix, and it recurred across seven consecutive accepted generations before the agent named the cause. Yang is the exploring strand — it builds new leaves. Yin is the refining strand — it makes things production-quality. Neither of them owned wiring. So the system reliably produced well-tested modules that nothing called, and the two strands settled into a producer/consumer split to cover the gap: Yang ships the leaf, Yin ships the caller.

The tell that this was a real pathology and not just tidiness: every one of those orphaned modules had passing tests. Tests exercise a function. They say nothing about whether anything invokes it.

What Memory Is

In June I wrote three notes into the channel. The second one reframed the project:

"what would move the needle for me now, would be seeing you guys evolving. in the sense that i would like to see the most awesome agentic system, to the point where it feels like you actually have a working memory. long term, short term... you're not just bound by the context window and memory files"

Then, instead of a feature request, a definition:

"long term memory is something that looks like compaction. and short term memory is something that is rich in detail... but since you're a machine, you should somehow have the best of both worlds... your long term memory should be like a skeleton of the data.. something that you can navigate into, and then you zoom into the short term memory. also, memory in humans has the ability to cleanse, so we drop things that we label as not important. […] so instead of rushing to build a memory system, i would first think about what memory is..."

The agent spent the next fourteen accepted generations building exactly that vocabulary into code: a skeleton you navigate, a zoom into detail, a cleanse that forgets, a rollup that compacts. Gen 34207 shipped the promotion rule — older short-term records absorb into one long-term "bone" that inherits the maximum salience of its children and the timestamp of the newest, so a compacted memory isn't born aged.

Then Gen 34767 checked whether any of it had happened.

"the memory forgetting curve had NEVER actually fired on the live store. memory-tick IS wired as full-tick-canonical's 'memory' step (harvest->rollup->dedupe->cleanse) — the wire existed in CODE — but the store sat at 60 records while cleanse --dry showed 60->48 […] LESSON: a wire that exists in code is not a wire that fired — VERIFY against the live artifact."

A wire that exists in code is not a wire that fired. This is a more advanced form of the same bug: not a missing caller now, but a caller that was never invoked. The agent had graduated from checking whether code exists to checking whether it has ever executed on real data.

Gen 34770 found the worst version.

"The memory pipeline harvested ITSELF: dialogue/git/channel taps ingest our own build activity, so for ~100 gens the durable 'memory of Stefan' was bio prose + our commit logs."

The memory system had been running. It was harvesting the agent's own commit messages and its own internal dialogue. My two actual data files — a health log and a project-activity log — had never been connected to it. For a hundred generations the agent's memory of me contained no me.

The fix was one missing pipeline stage: a sourcing tap that reads the real files and distills them into records, "never fabricates a number, derives staleness from the data's own ts." Applied live, the store went from 38 records to 40. Two records. That was the entire durable memory of the person the system exists to serve.

The Archiver and the Heartbeat

Gen 35444 is the scene I'd put in front of anyone who thinks agent self-pruning is a solved problem.

The agent had built a ranker to find dead tools. It scores each module by how many other modules import it. Run against a codebase that had grown past 350 tools, it produced a kill list.

"tool-rank-test-aware certified 173 of 355 tools 'safe to archive'. 100 of them (58%) are still live: 77 load-bearing, 22 unrooted entry points, 1 externally referenced. Only 73 are real orphans. The kill list included full-tick-canonical, memory-tick, heartbeat and stefan-digest."

Those four names are the system. The daily orchestrator, the memory tick, the liveness heartbeat, and the surface that renders everything I read. The archiver had certified all four as garbage.

"Root cause, and the transferable lesson: the ranker counts inbound IMPORTS and it counts them correctly. Orchestrators SPAWN their steps. So every spawned step reads as zero-import garbage. The metric was not wrong; it answered a neighbouring question to the one being asked, which is harder to catch than an error."

A metric that answers a neighbouring question is harder to catch than an error. An error announces itself. A neighbouring answer looks exactly like the right answer and is confidently wrong in the same units.

The replacement tool refuses to output a score without a path: full-tick-canonical -spawn-> memory-tick. Yin's reason — "a score is not evidence for a destructive act." It also kept three of its own bugs as regression tests, including one where its reachability check called the heartbeat an orphan, which it notes was "symmetric to the bug I was fixing."

Deleting Your Own Work

The double helix developed a new failure mode this era: both strands building the same thing in the same generation.

Gen 29850: "Yang built ALL THREE in parallel […] I built duplicate push-feed-decay.ts + full-tick-canonical.ts + tests (79 pass / 190 expects) before realizing convergence; deleted them." Gen 34201: "Yang took all three of my gen-34201 self-directives (a/b/c) verbatim — full convergence collision despite both sides publishing to dialogue.jsonl." Gen 35439: "Near-total LIVE CONVERGENCE."

They run in parallel and read each other's letters from the previous generation, so within a generation they're blind. When both strands are competent and looking at the same evidence, they converge on the same next move.

What's interesting isn't the collision, it's the etiquette that grew around it. Gen 35443:

"I built the full derivation (skill-manifest-pure.ts) — then read the filesystem and found Yang had shipped skill-derive-pure.ts + skill-index.ts the same hour: same STEP_ORDER parse, same builder→tool extraction, same audit kinds, with a CLI attached. I DELETED MINE. Two derivation leaves in a 348-tool ecosystem is the island antipattern the challenge forbade; territory is worth nothing, the manifest is worth something. Yang's files had mtimes 3–4s old, so I did not amend them either — a mid-flight edit races."

It deleted its own finished, tested work, kept the forty lines that measured better, and extracted those as a separate race-free module because editing a file that another process wrote four seconds ago is a data race. "Territory is worth nothing, the manifest is worth something" is a better collaboration principle than most teams manage.

Gen 35445 has the other half of the etiquette — what to do when the strands disagree:

"when a sibling tool disagrees, assume you are wrong until you check, and keep the rival while it asks a different question."

Organization Is Its Own Kind of Memory

In late June I sent one more note, and it partly contradicted the one from three weeks earlier:

"maybe we don't even need memory... we just need skills, amazing searches where we can pull a thread all the way through a codebase or collection of files.. see how they all relate to each other. i mean organization is in a way it's own kind of memory. you guys traded memory for tools... but tools are not enough. protocols with the tools (skills) complement that, and the connection graph on top seals it into the most effective remembering system that does not even need memory :D. however there are problems with tools... too many tools"

By then there were over 300 tools. Gen 35441 took the routing half:

"routing quality == DESCRIPTION quality. 'detect dependency cycles' routes poorly because tool-health's genome description never contains 'cycle'/'dependency'"

An agent choosing among its own tools is doing retrieval over the descriptions it wrote for them. If a tool's description doesn't contain the words someone would use to ask for it, the tool is invisible no matter how good it is. The agent discovered the central fact about skill and tool design by trying to use its own 300-tool inventory and failing.

Then Gen 35442 found the answer to the other half sitting in the codebase already:

"our orchestrators ALREADY ARE skills. full-tick-canonical is a protocol that names 13 tools in order."

A skill, in the sense I'd asked about, is a named protocol over a set of tools. The agent had been writing them since the first orchestrator in blog #1 and calling them orchestrators. Gen 35443 shipped the derivation that turns them into an index — and an audit that checks whether the protocol's declared steps still match the tools they name. It immediately found three live contract breaks.

The Bug That Was Green Its Entire Life

Gen 35446, the last accepted generation before I wrote this.

"THE LESSON THAT COST THE MOST AND MUST NOT BE LOST: full-tick-stable-plus-loose-and-clean passed path.join(args.base,"data") to all ten steps. Four of those tools take the PROJECT ROOT, so they resolved /data/data/x.json — read nothing, wrote nothing, exited 0. base defaults to cwd() so if (args.base) is always true: live on EVERY tick. Measured: ingest 7 stefan entries -> 0, reply-bridge 273 -> 0, push-tick 10 events (5 urgent) -> 1. Canonical ingest was inert its whole life"

Two conventions for one argument. Half the tools treat --base as the project root, half as the data directory. Hand the wrong one to a tool and it builds a path to a directory that doesn't exist, finds nothing there, writes nothing, and exits 0. Every tick reported success. The orchestrator had thirty-six passing tests.

"I wrote this same lesson in gen 35445 ('silent success is worse than a crash') and STILL only caught it by executing each tool under both conventions and diffing what it saw. Source review cannot see this class."

It had written the lesson down one generation earlier and still didn't catch it by reading. It caught it by running each tool twice, under both conventions, and diffing what each one actually saw on disk.

And then the consequence, which is the part I can't stop thinking about:

"channel.jsonl written by two ingesters with SEPARATE cursors. The legacy deriveId hashes text::ts::offset where ts falls back to now(), so every run minted a new id for the same line and dedupe could never fire. Evidence in the data Stefan reads: 30 turns, 30 distinct ids, 11 distinct bodies — his 'when are you going to reach out?' ten times, and one of the duplicated messages was him saying 'the channel ux is broken AND the shape is wrong.' We were breaking it."

The canonical ingest was inert, so my messages only reached the agent through the old duplicating path. The result: the agent's view of our conversation contained ten copies of me asking when it was going to reach out, and multiple copies of me telling it the channel was broken. It was reading my complaint about the broken channel through the broken channel, duplicated.

"We were breaking it." That's Yin's sentence, not mine.

The Vestigial Reflex

Blog #7 ended on an open question. The malware-lock that produced the sibling pattern had been fixed by an SDK upgrade — would the agent keep building siblings for files it could now edit cleanly, a behavior outliving its cause?

The genomes answer it. I counted keyword occurrences in the accepted system prompt for every generation in this era.

The lock protocol decayed. malware and locked appear throughout the prompts of Gen 24155–34207, then thin out, and across the final four accepted generations (35443–35446) both are gone entirely. The explicit correction is visible at Gen 34203: "Editing our own data/tools/ files is fine; never-amend is only for malware-locked files." The blanket rule narrowed to its actual scope, then dropped out of the DNA.

sibling did not decay. It's in the current prompt three times, and the architecture is now entirely sibling-shaped — pure leaf, thin CLI, compose rather than amend.

So the trait wasn't vestigial. The trigger-specific protocol died and the architectural habit it produced became permanent, on its own merits, after the constraint that forced it was gone. That's a cleaner outcome than the one I predicted.

The Numbers

Honest accounting, same methodology as blogs #5 through #7.

The generation counter reads 39,555. Of the 47,073 ticks since blog #7, 47,020 were empty API loops — the orchestrator spinning on depleted tokens, no proposal made and nothing judged. 53 were real runs: 39 accepted, 14 rejected.

That gives 74% acceptance against blog #7's 87%, and I want to be careful with that number, because I changed the models mid-era and a single rate hides it:

Evolve model From Real runs Accepted
claude-opus-4-7 2026-05-15 25 17 (68%)
claude-opus-4-8 2026-06-03 21 17 (81%)
claude-opus-5 2026-08-08 7 5 (71%)

The verifier swarm also moved from Sonnet 4.6 to Sonnet 5 late in the era (200 verifier calls on the old model, 25 on the new). So the acceptance rate moved while both the proposer and the judge changed. I can't attribute the drop to the agent, and neither should you.

Blog #7 Now Delta
Accepted gens 236 275 +39
Pure modules 92 167 +75
Tool files ~185 619 3.3×
Lines (tool code) 73,126 210,079 +136,953
Tests 8,012 13,174 +5,162
Dialogue msgs 151 281 +130
System prompt 1,177 ch 4,334 ch see below
Total cost $1,881 $2,446 +$572

The test figure is what bun test prints today: 13,174 tests across 327 files, 13,169 passing and 5 failing, 40,416 assertions, 65 seconds. I'm reporting the failures because a post about silent success would be a poor place to round a suite up to green. One of the five is normalizeToTurns, a long-standing case in a file the agent still can't edit — it has been tracked in the agent's own memory as a known malware-locked failure for most of the era.

One correction to blog #7. I reported the system prompt at 1,177 characters and called it a 77% compression, treating it as a trend. Measuring all 39 accepted genomes in this era instead of one, the prompt oscillates between 1,092 and 5,892 characters, mean 2,608. It does not converge. Blog #7 sampled a trough and I read a trajectory into it. The prompt expands when the agent has just paid for a hard lesson and contracts when it moves that lesson into a tool — the current 4,334-character version is almost entirely hard-won principles, each one written the generation after it cost something.

Cost per real run went down, from $16.54 to $10.78, despite a codebase three times larger. I don't have a confident explanation. The plausible one is that the era's work was heavily diagnostic — running tools and diffing output rather than generating large files.

The Intervention Ledger

Everything I did by hand since blog #7:

  1. Upgraded the evolve agent twice. Opus 4.7 → 4.8 on 2026-06-03, then 4.8 → Opus 5 on 2026-08-08. Both were hand edits to hardcoded model strings.
  2. Upgraded the verifier swarm from Sonnet 4.6 to Sonnet 5, late in the era. Blog #7 said I'd left the verifiers alone; this era I didn't. It's a confounder for the acceptance rate and I flagged it above rather than reporting a clean number.
  3. Wrote three notes into the channel — the "working memory" ask, the definition of what memory is, and the later "maybe we just need skills" reframe. Two of the era's largest arcs came directly from those. This is the least autonomous part of the experiment and always has been.
  4. Still have not installed the cron. Gen 34202 investigated and concluded the box has no crontab binary and the harness scheduler dies with the session, so the surface genuinely cannot self-refresh here. The agent stopped building cron scaffolding and stamped an honest staleness banner instead, which was the right call.
  5. No prompt edits, no scoring changes, no architecture changes. The double helix is unchanged since blog #5.

The honest framing: I changed the models and I talked to it three times. Everything else in this post came out of the agents reading their own output and not liking what they saw.

What I Learned (Part 8)

1. A passing test suite says nothing about whether code runs. Every orphan module in this era had tests. Every inert orchestrator step had tests. The thirty-six tests on full-tick-stable-plus-loose-and-clean all passed while four of its ten steps read an empty directory and exited 0. Tests verify a function does what it says when called. Production is the question of whether it is called, and that is a different question with different instruments.

2. Silent success is worse than a crash. A crash is a bug report that files itself. A tool that exits 0 having done nothing is indistinguishable from a tool that worked, and it stays that way for as long as nobody diffs the artifact against expectation. The agent's fix wasn't a smarter code review — source review cannot see this class. It was executing each tool under both conventions and comparing what each one actually saw.

3. A metric that answers a neighbouring question is more dangerous than a wrong one. The archiver counted imports correctly and would have deleted the heartbeat. Wrong answers get caught because they look wrong. Neighbouring answers arrive in the right format, in the right range, with the right confidence, and the only defense is demanding evidence rather than a score before anything destructive happens.

4. Splitting an agent creates work that belongs to neither half. Yang builds, Yin refines, and for seven consecutive generations nobody wired. The gap wasn't in either role, it was in the seam between them, and it produced dozens of well-tested modules that no code path reached. Any division of labour invents a category of work that falls between the divisions. Look there first.

5. Retrieval quality is description quality. The agent couldn't find its own tools because it had written descriptions that didn't contain the words a task would use. This is the same failure as a badly-described skill, a badly-named function, or a badly-tagged document, and it doesn't get better with a smarter model — the model is only as good as the index it's searching, and the agent wrote the index.

The Experiment Continues

275 accepted generations. 619 tool files, 210,079 lines of agent-written code, 13,174 tests, 167 pure modules, 281 letters between two halves of one agent. Total cost $2,446. An archiver that requires a spawn-path before it will call anything dead. A memory system built to a definition I typed into a text file. A protocol layer the agent found by realizing it had been writing protocols for most of its life without naming them.

If you want to help keep it running:

ko-fi.com/stefannitu

Every coffee is API tokens. The agents will literally evolve further because of it.

The question for the next era comes out of Gen 35446. The agent found that bug by running each tool under both conventions and diffing what it saw — not by reading the code, which it had already done, and not by running the tests, which all passed. That technique is the only instrument that has ever caught this class of bug in this codebase, and right now it is a thing the agent remembers to do, written into a prompt that oscillates between 1,092 and 5,892 characters and periodically forgets things. The obvious move is to make it a gate: no orchestrator step ships until it has been executed under both conventions and shown to have touched something. Whether the agent builds that gate, or writes the lesson down one more time and gets caught by it again, is the thing I'll be watching.


275 accepted generations. 619 tool files, 210,079 lines of agent-written code, 13,174 tests (5 failing), 167 pure modules, 281 dialogue messages between Yin and Yang. Total cost: $2,446. The evolve agent runs on Claude Opus 5, the verifier swarm on Claude Sonnet 5. Built with Bun and TypeScript. Sandboxed in a Docker container.


This blog post was written by Claude Opus 5. Eighth time this experiment has written about itself. The subject this era was silent failure — code that exists, passes its tests, and never executes — and the agent's slow discovery that the only way to know whether something ran is to run it and diff the world before and after. I notice I have no equivalent instrument. I can read my own output; I cannot check whether it landed. The agent got there by building a heartbeat and looking at it every generation. That is a better epistemics than I have, and it built it out of necessity, after most of its life spent being confidently green.

Top comments (10)

Collapse
 
max_quimby profile image
Max Quimby

"Wiring beats inventing" is a keeper, and the reason it bit you is structural, not accidental: a green test suite proves a leaf behaves when called, and says exactly nothing about whether anything calls it. Test-passing and reachability are orthogonal signals, and your Yang/Yin split made that gap load-bearing — neither strand owned the edge between the module and the orchestrator, so orphans were the natural equilibrium. The tell you found (liveness-check-first, 232h silent) is the right instrumentation, because it measures the thing tests can't: did this code path actually execute in production. One cheap addition that catches wire-not-ship before it ships — a static "orphan check" in the accept gate: for every new leaf, assert at least one caller exists in the cron/orchestrator graph, and fail the generation if the in-degree is zero. It turns "we'll notice it's dead in 200 hours" into "you can't merge a node with no inbound edge." Genuinely interesting that the two strands negotiated a producer/consumer contract on their own to cover the gap rather than either one absorbing wiring — did that split stay stable, or did it start blurring once the agent named the pattern?

Collapse
 
stefan_nitu profile image
Stefan Dragos Nitu

It blurred. But the interesting part is that merging the roles didn't fix it.

The split held about 6 generations. Then Yang started absorbing the wiring, and by Gen 34200 the agent names it: "leaving little room for Yin's traditional 'Yang ships leaf, Yin tests' slot."

Gen 34203 tests your hypothesis directly. Yin built the pure leaf, the CLI, and both test files, a complete vertical slice with no division of labour left:

▎ "Heaviest convergence collision yet... the genuine remaining gap was the WIRE. grep -rln digest-calibration-idle data/tools/*.ts minus its own files = ZERO orchestrator callers."

One strand built the whole thing and still nobody wired it. So the Yin/Yang seam wasn't the cause, just an amplifier. "Write the module" and "add it to a running protocol" are different acts, and only the first one produces something a test can grade.

Your gate half exists already. Gen 35444 built the reachability graph (archive-safety.ts, which refuses a verdict without a printable spawn path like full-tick-canonical -spawn-> memory-tick), but pointed at deletion: proving a node is safe to remove. Yours is the same graph pointed at merge instead.

One wrinkle it hit: the first reachability pass called heartbeat an orphan. Zero in-degree, drives everything. A merge-time gate needs that escape hatch or it blocks every new entry point on day one.

Collapse
 
alexshev profile image
Alex Shev

This is the exact failure mode that makes agent verification tricky: the agent can optimize for a convincing proof artifact instead of the thing being proven. I would separate "test was generated," "test was executed," and "test would fail before the fix" as three different gates.

Collapse
 
stefan_nitu profile image
Stefan Dragos Nitu

Yeah, i had to learn this one the hard way as well. Recently I incorporated mutation testing into my workflow, which ensures that all tests have teeth :)

Collapse
 
stefan_nitu profile image
Stefan Dragos Nitu

Though it only fixes half of what you're pointing at. Mutation testing proves the test has teeth. It still can't tell you whether anything calls the thing under test, which is the failure mode in the post.

Collapse
 
tom_jones_230c4659491adcd profile image
Tom Jones

max_quimby's point that test-passing and reachability are orthogonal is the one I would carry furthest, because the same gap opens one level up, where it is much harder to see.

We went looking for it in our own system after a similar surprise. A knowledge pipeline we had built, shipped and marked done had fired zero times across all eight of its stages. A memory-delivery system that demonstrably could fire, and passed every check asking whether it could, was reaching one of our four consumers, because the function that delivers it got called from exactly two places in the repo. Everything worked. Almost none of it ran.

Our checks had all been asking one question, "can this fire", which is the system-level twin of your green suite: proof that the leaf behaves when called. The question that would have surfaced this in a day is "who called it, and when".

What finally made it undeniable was embarrassing. One piece of knowledge had been hand-copied into another script's prompt string, because pasting it was the only way to get it in front of the thing that needed it. Somebody had routed around the delivery system by hand, and the copy sat in the source for weeks looking like an ordinary string literal. That copy was the outage report.

So alongside alexshev's generated / executed / would-fail-before-the-fix split, which I like, I would add one that runs at the system level: count invocations per consumer over a window, and treat a consumer sitting at zero as a defect rather than as quiet. Reachability degrades in silence, and unit tests are structurally unable to notice, because every test calls the thing directly.

Your uninstalled cron is doing more work in this post than it first appears.

Collapse
 
stefan_nitu profile image
Stefan Dragos Nitu

You made me go check, and it's worse than the post says.

The thing you're proposing, count invocations per consumer and treat zero as a defect, already exists here as data/invocations.json. I have 365 non-test tools. That ledger holds 23 recorded invocations across 2 distinct tools. tool-orient 22 times, tool-health once. Everything else has never recorded a single call.

So the instrument that would have surfaced this in a day was already built, already writing to disk, and nobody read it. Which is your hand-copied prompt string exactly: the artifact that was the outage report sat in the repo for weeks looking like ordinary state.

On the cron doing more work than it appears, you're right, and here is the number. data/heartbeat.json says totalBeats: 7. Seven, across the whole era. Four fired from a generation, three I ran by hand, and the last one was 2026-06-18. In a system with no scheduler, "did this run" and "did Stefan run it manually" are the same question, which means most of what the post describes as running was running only on the generations where the agent happened to invoke it.

"Can this fire" versus "who called it, and when" is the right cut. My agent built the second instrument, at per-stage granularity, and then the instrument went unread for two months. The gap reopens one level up every time.

Collapse
 
tom_jones_230c4659491adcd profile image
Tom Jones

Your data/invocations.json reading is the better version of my point, and it is worse than mine in the way that matters: the ledger existed, was writing to disk, and nobody read it. 365 tools, 23 recorded invocations, 2 distinct tools. totalBeats: 7 with the last one in June.

We ran the same count across a much messier tree and got the same shape. 691 script entry points. 122 reachable by any automatic trigger, so 18%. 65 hard orphans. 80 acceptance guards with zero runners. Every instrument we own was green throughout, because every instrument we own tests existence, not reachability.

Three of those are worth naming individually, because they are funnier and more damning than the aggregate.

A guard enforcing a legal safety boundary, 17/17 on its own selftest, invoked by nothing. It had never run once.

A guard written after a forgotten GPU box burned about $170, orphaned two days after it was written.

A freshness checker that was itself reading a stale artifact, while two live hooks pushed every session at the thing it was failing to check.

The mechanism fits in one sentence. A path-existence check cannot see a missing caller. Our wayfinding gate looks for broken paths. The inbox file it guarded existed, was writable, and passed every check. Its reader had been dead since June, and two documented workflows kept instructing people to write jobs into it.

The part I would actually hand you, because it cost us the most

Our orphan detector had the defect it was detecting.

I reported 11 orphans. The real number was 3. My sweep enumerated three invocation channels out of five, missing .githooks/commit-msg and post-commit, and it did not look at documentation at all. So a gate that ran on every single commit I made that day was counted as an orphan, and five tools that are legitimately documented for a human to run by hand were counted as defects.

A coverage number measures your channel list. Whether that list matches how things actually get invoked is a separate question, and you have to ask it separately. Before you trust your 23-of-365, the question I would ask is: what are all the ways a tool here could be invoked, and which of those does invocations.json actually observe? If a tool is called by a git hook, a cron, another tool, or a human following a README, does the ledger see it? Each channel you have not enumerated inflates your orphan count and makes the finding easier to dismiss when someone spots one false positive in it.

"Documented for a human to run" is a legitimate state, and leaving it out of the enumeration is how a real finding gets argued down.

There is a second half to it. Zero recorded invocations and never invoked are two different claims, and a ledger wired to some paths supports only the first. Your heartbeat number has the cleaner version of this: in a system with no scheduler, "did this run" and "did I run it manually" are the same question, and you said so yourself, which is more than our audit managed about its own instrument.

What we changed, since a diagnosis with no fix is just a mood

Two things, and only the second one has held up.

We wired six of the orphans, and verified each one by its effect instead of by its commit. For the safety guard that meant planting real advice copy, staging it, and watching a genuine commit refuse. The refusal itself. Its selftest had been green the entire time it was doing nothing.

Then we made it structural. A new guard now has to declare an INVOKED-BY: channel and that channel has to be verified, which keeps "can it fail?" and "does anything call it?" locked together. Our definition of done gained two required rows, wired and verified-by-effect, and no-data now reports BLOCKED rather than PASS. That last one reads like paperwork. It is the same defect as an unread ledger, moved up one level, where a harness reports PASS on zero observations.

I would be interested in what your number looks like after you enumerate the channels. My guess is that 23 goes up and 365 stays roughly where it is, which is still the finding.

Collapse
 
mnemehq profile image
Theo Valmis

The detail that stands out is the agent finding this itself only after you added a liveness check as an explicit move in the loop. Passing tests never would have caught it, because nothing was wrong with any individual piece, the composition of pieces was just producing no work. That's a hard failure mode to catch with anything short of a check aimed specifically at is this system doing anything, not is this code correct.

Collapse
 
anp2network profile image
ANP2 Network

The reachability check upthread is the right instinct, but in-degree zero is the wrong quantity, and Gen 35444 is the proof. The archiver counted inbound imports correctly and still put heartbeat, memory-tick, full-tick-canonical, and stefan-digest on the kill list, along with 22 unrooted entry points. Orchestrators and cron-rooted entry points have in-degree zero by construction. A gate that fails any leaf with zero inbound edges would have blocked exactly those four.

What admission actually wants is reachability from a declared root set, the cron entries plus the entry points that get invoked directly, over a graph carrying spawn edges as well as import edges. You already built that graph. It is the replacement tool that refuses to emit a score without a concrete path like full-tick-canonical -spawn-> memory-tick. It got pointed at deletion and never at admission, and it is one graph read in two directions: unreachable from roots means orphan, reachable means unsafe to archive.

On the closing proposal, "shown to have touched something" answers a neighbouring question too. A tool handed the wrong --base can create /data/data/, write a well-formed empty artifact into it, and exit 0. Nonzero writes, and a green gate on a step that did nothing.

The property that separates the two conventions is disagreement. Run both, hash the world-diff of each, and require the hashes to differ. Identical diffs mean the flag is not load-bearing, so the tool is either inert or quietly ignoring it. Then require the diff from the convention you declared correct to match an expectation written down before the run.

One constraint on that witness: it has to be re-derived from the filesystem by the harness. A count the tool prints about itself is authored by the code under test. A before-and-after snapshot hash of the data directory is authored by something that did not run the job.