Skip to content

v1.14.2 ·

The lock that only kept out its owner

The problem

Two things in this release were broken in the same way, and it is a way that is hard to notice: the state was wrong, and nothing said so.

The first was a lock. The framework lets two people work the same piece of work without treading on each other, and part of that is a check on who owns the phase you are trying to advance. The check compared the recorded owner against whoever was asking. Reasonable — except that the two names came from different places. When an agent claims work, it is recorded under a generated session identifier. When the transition command asked "who is doing this?", it read the operating system's username. Those two can never be equal, no matter who is right.

So the check did not fail sometimes. It failed every time it ran, and it failed against exactly the session that was entitled to proceed: the one holding the claim. It was hit twice in one day, on two different pieces of work, and both times the way past it was to blank the field or pass a flag that overrides the check entirely. A guard you get through by turning it off is not guarding anything.

Worse, the check had never once done its actual job. It only runs when an owner is recorded, and the tool itself never records one — it writes the field empty and leaves it empty. So on the ordinary path it was inert, and the only way to wake it up was for an agent to fill the field in by hand, which is exactly when it gave the wrong answer.

The second was an idea going missing.

Ideas for future work are one file each, with a short header at the top: an identifier, a title, a date. If the title starts with a backtick, the header does not parse. YAML reserves the backtick as a special character, so a bare value cannot begin with one — and the most natural way in the world to name a code problem is to put the function name in backticks.

The reader for those files returns nothing for a file it cannot parse. It also returns nothing for a file that does not exist. Every caller treats nothing the same way: skip it and move on. So one stray character removed an idea from the list you browse, from the lookup that finds which work it became, from the automation that files it as done, and from the repair tool that is supposed to notice this class of thing — with no message anywhere. Absence looked identical to never having written it.

Five of the first forty-nine ideas in this project were in that state. Some had been invisible for ten days. They were found by accident: a repair tool reported zero problems while one of the missing files sat plainly on disk in the folder it was reporting on. Every one of the five had been written by an agent, and every one had a backtick where the title began — because that is how you name a function.

The expensive part of both defects is not the wrong answer. It is that the wrong answer arrived silently. A blocked transition at least prints something, even if it names the wrong culprit. A missing idea prints nothing at all, and you cannot go looking for a file you have forgotten you wrote.

How it could be solved

Both reports came with a proposed fix. Neither fix was the one that shipped, and the reasons are worth separating.

The lock report said: retire the owner field. Two fields recording "who has this" is one too many, the claim already survives across working copies, so delete the other one. That reasoning is sound, and it was the first thing tried.

It was abandoned inside a single search. The field is referenced in more than a hundred places across thirty-one test files, nearly all of them incidental — a fixture setting it to empty because the type demands the key. Removing it meant touching every one of them. But the decisive objection was not the cost. It was that deleting the field would not have fixed the defect. The comparison was between two kinds of name that can never match; whichever field held the value, the comparison was still wrong. The report named the thing it could see. The mechanism was one level above it.

What shipped instead was fifteen lines in one file: when work is claimed, ask the claim who is holding it, and use that as the answer to "who is doing this?". Now the two names come from the same place and can agree. Nothing else moved — the check itself is untouched. The field stops being a lock and stays a label: nobody writes it, the status table displays it, and the check reaches it only through the claim.

There was a more direct-looking option, and it is a trap worth recording. Instead of reading the claim, resolve the current session's own identity and compare that. It reads better. It also breaks worse: the identity resolver falls back to minting a fresh identifier per process when it cannot do better, so a session that has not registered itself would get a new name on every command and lock itself out on every transition. That is strictly worse than the bug being fixed. Reading the claim needs nothing new, because the claim is already written down.

The missing-idea report proposed four steps. Only the first and last shipped.

Step one — always quote the title when writing one — is the only step that removes the cause. It is three keystrokes, and it has to be unconditional. There was already a conditional version in the codebase: quote the title if it contains a colon-space. That guard is exactly what let backtick titles through, because conditional escaping only works if you enumerate every dangerous character correctly, and backticks do not look dangerous. Quoting always requires no enumeration.

Step two — teach the file reader to distinguish "cannot parse" from "not there" — did not ship, and declining it is the interesting decision. It is the tidier design, and it would have touched every caller of that reader to buy reporting that only two of them want. One shared function that walks the folder and names every unreadable file gives both of those callers what they need and leaves the rest alone. The tell that this was the right shape: the repair tool had already written that walk privately, precisely because the reader could not express the difference. Duplication was the symptom; sharing it was the fix.

The last step is a repair, not just a warning. Quoting a title is mechanically safe in a way most automatic fixes are not: rewrite the one line, re-parse the whole header, and refuse to save unless it now parses. That final re-parse is the guard against a confident wrong answer. A file broken anywhere else is named in the report and left exactly as it was for a human to look at.

One deliberate restraint: a file the repair cannot fix is reported as "not applicable", not as "pending". Pending work makes the repair tool exit with a failure, and a file only a human can fix would make it fail forever. A tool that always fails is a tool people stop reading, which costs more than the broken file does.

How AIDLC solves it

The ownership gate now reads through the claim. getActorIdentity in packages/cli/src/commands/transition.ts resolves the live claim for the instance and returns its session, falling back to $USER / $USERNAME only when no live claim exists. The gate itself is untouched.

Roadmap titles are quoted unconditionally. The capture instructions in packages/content/skills/04-roadmap.md and the portable packages/content/resources/skills/roadmap-item/SKILL.md now say to quote every title, with the reason. rewriteFrontmatter in the roadmap-layout migration stopped quoting conditionally.

Unparseable items are detected once and reported. unparseableItems and quoteItemTitle moved into packages/cli/src/roadmap/paths.ts as the shared mechanism. A new confirm-tier migration, roadmap-item-titles, quotes a repairable title and names anything it cannot fix.