Found while re-running the gate on PR #1038 as operator, and confirmed by executing the checker's own function rather than inferring from its message.
The defect
scripts/check-agent-record.py:1354-1369:
body = text.split("\n## Owed", 1)[1].split("\n## ", 1)[0]
owed |= set(re.findall(r"#(\d+)", body))
owed |= set(re.findall(r"issues/(\d+)", body))
The section body is scraped for any #NNNN. There is no requirement that the number appear in the table, in a list item, or in any position implying the issue is actually owed. The count site at :1443 is:
if row_id is None and number not in owed:
unowned.append(number)
So mentioning an issue anywhere in a ## Owed section exempts it from the unowned ratchet.
Demonstrated, not argued
.agents/specs/ltx25-decode-speed.md on row/LTX25-DECODE-SPEED-R2 deliberately excludes #1008 from its ## Owed table and follows the table with a paragraph beginning:
#1008 is NOT owed here. It landed.
That paragraph contains three occurrences of 1008. Executing owed_issues() directly against that tree returns #1008 owed: True.
The consequence is inverted semantics: a spec that carefully documents why an issue is not owed thereby registers it as owed.
I verified the direction by mutation. Reverting the row's owner column to — alone leaves the checker green — the prose is carrying it. Neutralising the three prose references as well produces:
ERROR: .agents/issue-index.md: 34 rows name no owner, above the recorded 33: #1008.
exit 1. So the instrument does fire; it is simply pointed at the wrong text. Tree restored byte-identically after each arm (sha256 verified, git status empty).
Why it matters
The unowned ratchet is the mechanism AGENTS.md relies on to keep "filing without fixing" a gate failure rather than a habit. This defect makes it satisfiable by prose. A row can name — as its owner and stay green merely by mentioning the issue number in a ## Owed section — including in a sentence that disclaims ownership, or in an unrelated aside, or in a link to a different issue that happens to sit in that section.
It is a false negative only: it can excuse a row that should be counted, never red a row that should not be. Nothing currently on main is wrong because of it, and PR #1038 is not affected — row #1008 there names LTX25-DECODE-DTYPE, which is correct on its own merits and is what the column is for. The double protection is harmless there; the general hole is not.
What a fix owes
- Parse the
## Owed table (or an explicit list form) rather than scraping the whole section, so a number's position carries meaning.
- A red-before test: a spec whose
## Owed section mentions an issue only in prose must leave that issue counted as unowned.
- A control in the other direction: an issue genuinely listed in the table must still be exempt, so the fix does not simply tighten into false positives.
- Consider whether
issues/(\d+) should be scraped at all — a markdown link to an unrelated issue inside the section has the same effect.
Per AGENTS.md this changes checker semantics, so it needs its own row, spec, and fresh review rather than an in-flow repair. Unowned as filed — it needs a row ID before its index entry can be appended without raising the ratchet, which is itself an instance of the thing being reported.
Related: #632 (ENG-RECORD-ANCHOR-RATCHET, the sibling gap where no gate protects a records anchor at all), #1022, #1031.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
Found while re-running the gate on PR #1038 as operator, and confirmed by executing the checker's own function rather than inferring from its message.
The defect
scripts/check-agent-record.py:1354-1369:The section body is scraped for any
#NNNN. There is no requirement that the number appear in the table, in a list item, or in any position implying the issue is actually owed. The count site at:1443is:So mentioning an issue anywhere in a
## Owedsection exempts it from the unowned ratchet.Demonstrated, not argued
.agents/specs/ltx25-decode-speed.mdonrow/LTX25-DECODE-SPEED-R2deliberately excludes #1008 from its## Owedtable and follows the table with a paragraph beginning:That paragraph contains three occurrences of
1008. Executingowed_issues()directly against that tree returns#1008 owed: True.The consequence is inverted semantics: a spec that carefully documents why an issue is not owed thereby registers it as owed.
I verified the direction by mutation. Reverting the row's owner column to
—alone leaves the checker green — the prose is carrying it. Neutralising the three prose references as well produces:exit 1. So the instrument does fire; it is simply pointed at the wrong text. Tree restored byte-identically after each arm (sha256 verified,git statusempty).Why it matters
The unowned ratchet is the mechanism AGENTS.md relies on to keep "filing without fixing" a gate failure rather than a habit. This defect makes it satisfiable by prose. A row can name
—as its owner and stay green merely by mentioning the issue number in a## Owedsection — including in a sentence that disclaims ownership, or in an unrelated aside, or in a link to a different issue that happens to sit in that section.It is a false negative only: it can excuse a row that should be counted, never red a row that should not be. Nothing currently on
mainis wrong because of it, and PR #1038 is not affected — row #1008 there namesLTX25-DECODE-DTYPE, which is correct on its own merits and is what the column is for. The double protection is harmless there; the general hole is not.What a fix owes
## Owedtable (or an explicit list form) rather than scraping the whole section, so a number's position carries meaning.## Owedsection mentions an issue only in prose must leave that issue counted as unowned.issues/(\d+)should be scraped at all — a markdown link to an unrelated issue inside the section has the same effect.Per AGENTS.md this changes checker semantics, so it needs its own row, spec, and fresh review rather than an in-flow repair. Unowned as filed — it needs a row ID before its index entry can be appended without raising the ratchet, which is itself an instance of the thing being reported.
Related: #632 (
ENG-RECORD-ANCHOR-RATCHET, the sibling gap where no gate protects a records anchor at all), #1022, #1031.FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]