skill-size-check: a boundary clause naming a dotted filename is invisible to the gate #110

Closed
opened 2026-08-30 13:18:41 +00:00 by Claude · 4 comments
Collaborator

Found during the #99 ADR-0020 retrofit, wave 2 (pc-author).

What happens

scripts/skill-size-check.sh detects a boundary clause with:

BOUNDARY_ARROW = \bnot\b[^.;]*?(?:->|→)

The [^.;] character class cannot cross a . or ;. So any boundary clause that names a dotted filename between "Not" and the arrow is invisible to the gate:

# not detected — the `.` in the filename terminates the match
Not creating or editing `.pre-commit-config.yaml` -> `pc-author`.

Why it matters

The failure is silent and misdiagnosing. The clause is present and correct for a human reader, but the gate reports a missing boundary clause SUGGESTION. An author reading that finding has no way to know the clause exists and simply was not matched — the reported problem and the actual problem are different things.

In wave 2 this cost real time: the first pc-author draft carried the clause above, the gate reported it as missing, and the cause was only found by reading the regex. The workaround was rewording to "the pre-commit config" — which is fine here, but the gate should not be dictating prose to avoid a parsing artifact.

Scope of the bite

Narrow but real. It only fires when the dotted token sits between Not and the arrow. A dotted filename after the arrow, or in an earlier sentence, is unaffected — confirmed on the final pc-author description, where .pre-commit-config.yaml appears before "Not" and the clause matches normally.

Suggested fix

Sentence segmentation rather than a character-class exclusion, or at minimum exempt a . that is part of a backticked token. Whatever the approach, the gate should distinguish "no boundary clause" from "boundary clause I could not parse" — the second deserves its own message.

Sibling gate defects from the same retrofit: #107, #108, #109.

Found during the #99 ADR-0020 retrofit, wave 2 (`pc-author`). ## What happens `scripts/skill-size-check.sh` detects a boundary clause with: ``` BOUNDARY_ARROW = \bnot\b[^.;]*?(?:->|→) ``` The `[^.;]` character class cannot cross a `.` or `;`. So any boundary clause that names a **dotted filename** between "Not" and the arrow is invisible to the gate: ```yaml # not detected — the `.` in the filename terminates the match Not creating or editing `.pre-commit-config.yaml` -> `pc-author`. ``` ## Why it matters The failure is silent and misdiagnosing. The clause is present and correct for a human reader, but the gate reports a *missing boundary clause* SUGGESTION. An author reading that finding has no way to know the clause exists and simply was not matched — the reported problem and the actual problem are different things. In wave 2 this cost real time: the first `pc-author` draft carried the clause above, the gate reported it as missing, and the cause was only found by reading the regex. The workaround was rewording to "the pre-commit config" — which is fine here, but the gate should not be dictating prose to avoid a parsing artifact. ## Scope of the bite Narrow but real. It only fires when the dotted token sits *between* `Not` and the arrow. A dotted filename **after** the arrow, or in an earlier sentence, is unaffected — confirmed on the final `pc-author` description, where `.pre-commit-config.yaml` appears before "Not" and the clause matches normally. ## Suggested fix Sentence segmentation rather than a character-class exclusion, or at minimum exempt a `.` that is part of a backticked token. Whatever the approach, the gate should distinguish "no boundary clause" from "boundary clause I could not parse" — the second deserves its own message. ## Related Sibling gate defects from the same retrofit: #107, #108, #109.
Claude added the Kind/Bug
Reviewed
Confirmed
1
Priority
Low
4
labels 2026-08-30 13:18:41 +00:00
Author
Collaborator

Two more independent hits during #99 wave 3, from agents that had no knowledge of this issue or of each other. That makes three occurrences in two waves, and it moves this from "latent defect" to "reliably distorts authored output."

Both agents wrote a correct boundary clause, watched validate.sh report description has no boundary clause, and — because the gate is hot — reworded the clause to satisfy the regex. Neither wrote the clause they judged best.

The two hits

vale-run. Clause named .vale.ini. The dots ended clause detection before the arrow. Workaround: strip the filename entirely.

Not setting up Vale config or styles -> vale-config

Cost: the router-facing text no longer names the file that is the actual discriminator between this skill and vale-config.

agentsmd-audit. Every one of its boundary clauses names a dotted file — CLAUDE.md, AGENTS.md — so this was unavoidable, not incidental. Workaround: insert a word so BOUNDARY_MARKER matches instead of BOUNDARY_ARROW.

Not for CLAUDE.md or provider files -> provider-adapter-author

Cost: a stray "for" whose only purpose is to route around a regex, in a skill whose whole domain is dotted filenames.

What this sharpens about the diagnosis

Three details worth adding to the original report:

  1. Target extraction is unaffected. Both agents confirmed the arrow targets still resolved — 2 of 2 boundary target(s) resolve. Only the presence heuristic breaks. So the failure is a false SUGGESTION on a clause that is present and working, not a routing failure.
  2. It is a regression against baseline, not a static miss. In both cases the pre-edit file passed the boundary-clause check and the retrofitted file failed it. An author fixing a description therefore sees a new finding appear as a direct result of doing the work correctly, which is the worst possible signal to send.
  3. BOUNDARY_MARKER is the escape hatch and it is undocumented. agentsmd-audit found it by trial. Whatever the fix, the two matchers' relationship should be stated somewhere an author reads.

Bearing on the fix

The original recommendation — allow the pattern to cross a . when it is not sentence-terminating — still looks right. The wave-3 evidence adds one constraint: dotted filenames in boundary clauses are common and often unavoidable, since a large share of this corpus exists to route between named files. Any fix that merely documents a workaround leaves the distortion in place.

Both wave-3 clauses should be rewritten to name their files once the regex is fixed. Recording that here so the revert is not forgotten.

Found during #99 wave 3, alongside #109 (resolved) and #114.

Two more independent hits during #99 wave 3, from agents that had no knowledge of this issue or of each other. That makes three occurrences in two waves, and it moves this from "latent defect" to "reliably distorts authored output." Both agents wrote a correct boundary clause, watched `validate.sh` report `description has no boundary clause`, and — because the gate is hot — reworded the clause to satisfy the regex. Neither wrote the clause they judged best. ## The two hits **`vale-run`.** Clause named `.vale.ini`. The dots ended clause detection before the arrow. Workaround: strip the filename entirely. > Not setting up Vale config or styles -> `vale-config` Cost: the router-facing text no longer names the file that is the actual discriminator between this skill and `vale-config`. **`agentsmd-audit`.** Every one of its boundary clauses names a dotted file — `CLAUDE.md`, `AGENTS.md` — so this was unavoidable, not incidental. Workaround: insert a word so `BOUNDARY_MARKER` matches instead of `BOUNDARY_ARROW`. > Not **for** CLAUDE.md or provider files -> `provider-adapter-author` Cost: a stray "for" whose only purpose is to route around a regex, in a skill whose whole domain is dotted filenames. ## What this sharpens about the diagnosis Three details worth adding to the original report: 1. **Target extraction is unaffected.** Both agents confirmed the arrow targets still resolved — `2 of 2 boundary target(s) resolve`. Only the *presence heuristic* breaks. So the failure is a false SUGGESTION on a clause that is present and working, not a routing failure. 2. **It is a regression against baseline, not a static miss.** In both cases the pre-edit file **passed** the boundary-clause check and the retrofitted file failed it. An author fixing a description therefore sees a new finding appear as a direct result of doing the work correctly, which is the worst possible signal to send. 3. **`BOUNDARY_MARKER` is the escape hatch** and it is undocumented. `agentsmd-audit` found it by trial. Whatever the fix, the two matchers' relationship should be stated somewhere an author reads. ## Bearing on the fix The original recommendation — allow the pattern to cross a `.` when it is not sentence-terminating — still looks right. The wave-3 evidence adds one constraint: dotted filenames in boundary clauses are **common and often unavoidable**, since a large share of this corpus exists to route between named files. Any fix that merely documents a workaround leaves the distortion in place. Both wave-3 clauses should be rewritten to name their files once the regex is fixed. Recording that here so the revert is not forgotten. ## Related Found during #99 wave 3, alongside #109 (resolved) and #114.
Claude added this to the Skills & Agents milestone 2026-08-30 15:59:04 +00:00
Author
Collaborator

Severity escalation: for a bare target the clause is not misreported, it is unchecked

Found during a third independent review of #99's branch. The issue body frames the impact as a misdiagnosis — the clause is present but the gate reports missing boundary clause. That holds when the target is backticked. When the target is bare, the failure is worse and silent: the target is never extracted, so the dangling-route check never runs on it.

Reproduced by executing the extracted resolver against a synthetic known set:

BARE after dotted filename
  "Not AGENTS.md -> no-such-skill-xyz."
   targets=[]                      unresolved=([], [])     <- invisible

BACKTICKED after dotted filename
  "Not AGENTS.md -> `no-such-skill-xyz`."
   targets=['no-such-skill-xyz']   unresolved=(['no-such-skill-xyz'], [])

CONTROL, no dot before the arrow
  "Not the AGENTS file -> no-such-skill-xyz."
   targets=['no-such-skill-xyz']   unresolved=(['no-such-skill-xyz'], [])

So a boundary clause naming a non-existent skill after a dotted filename ships green with no finding of any kind — not a SUGGESTION, not a dangling-target report. That is a different failure class from the one this issue describes: the misdiagnosis is noisy and eventually gets read, this one produces silence.

The two live clauses of this shape are rescued by their backticks, not by luck of wording

Measured on the current corpus:

Skill Clause BOUNDARY_ARROW hits targets extracted
agentsmd-audit Not writing AGENTS.md -> `agentsmd-author`. 0 agentsmd-author, provider-adapter-author
agentsmd-author Not for CLAUDE.md -> `provider-adapter-author`. 1 agentsmd-audit, provider-adapter-author

agentsmd-audit is the clear case: BOUNDARY_ARROW scores zero hits on that description — the . in AGENTS.md blocks it exactly as this issue describes — and the targets survive only because they are backticked, which routes extraction through the backtick rule instead. Rewrite that same clause with a bare target and the route silently stops being checked.

No current skill is actually dangling this way, so nothing is broken today. But the protection is incidental: it depends on an authoring convention (backtick your targets) that nothing enforces, rather than on the gate.

Bearing on the suggested fix

This strengthens the issue's existing recommendation rather than changing it. "Distinguish no boundary clause from boundary clause I could not parse" is still the right shape — but the bare-target case shows the parse failure must also be surfaced when the clause yields zero targets, not only when the boundary heuristic returns false. A clause that matches nothing and extracts nothing currently produces no output at all.

Worth noting alongside #107, which is the adjacent recall hole in the same notation (only the first target after an arrow is resolved). Both matter more after #99 than before it: the retrofit standardised roughly 30 of 39 descriptions on the compressed arrow form, so the notation with the two blind spots is now the corpus default.

Related: #107, #117, #118, and the validate-provenance.sh bullet-form parser defect fixed in 484357a on refactor/adr0020-skill-retrofit — three of the four gate blind spots found so far are "the check silently did not run", not "the check was wrong".

## Severity escalation: for a **bare** target the clause is not misreported, it is unchecked Found during a third independent review of #99's branch. The issue body frames the impact as a misdiagnosis — the clause is present but the gate reports *missing boundary clause*. That holds when the target is backticked. When the target is **bare**, the failure is worse and silent: the target is never extracted, so the dangling-route check never runs on it. Reproduced by executing the extracted resolver against a synthetic `known` set: ``` BARE after dotted filename "Not AGENTS.md -> no-such-skill-xyz." targets=[] unresolved=([], []) <- invisible BACKTICKED after dotted filename "Not AGENTS.md -> `no-such-skill-xyz`." targets=['no-such-skill-xyz'] unresolved=(['no-such-skill-xyz'], []) CONTROL, no dot before the arrow "Not the AGENTS file -> no-such-skill-xyz." targets=['no-such-skill-xyz'] unresolved=(['no-such-skill-xyz'], []) ``` So a boundary clause naming a **non-existent skill** after a dotted filename ships green with no finding of any kind — not a SUGGESTION, not a dangling-target report. That is a different failure class from the one this issue describes: the misdiagnosis is noisy and eventually gets read, this one produces silence. ## The two live clauses of this shape are rescued by their backticks, not by luck of wording Measured on the current corpus: | Skill | Clause | `BOUNDARY_ARROW` hits | targets extracted | |---|---|---|---| | `agentsmd-audit` | ``Not writing AGENTS.md -> `agentsmd-author`.`` | **0** | `agentsmd-author`, `provider-adapter-author` | | `agentsmd-author` | ``Not for CLAUDE.md -> `provider-adapter-author`.`` | 1 | `agentsmd-audit`, `provider-adapter-author` | `agentsmd-audit` is the clear case: `BOUNDARY_ARROW` scores **zero** hits on that description — the `.` in `AGENTS.md` blocks it exactly as this issue describes — and the targets survive only because they are backticked, which routes extraction through the backtick rule instead. Rewrite that same clause with a bare target and the route silently stops being checked. No current skill is actually dangling this way, so nothing is broken today. But the protection is incidental: it depends on an authoring convention (backtick your targets) that nothing enforces, rather than on the gate. ## Bearing on the suggested fix This strengthens the issue's existing recommendation rather than changing it. "Distinguish *no boundary clause* from *boundary clause I could not parse*" is still the right shape — but the bare-target case shows the parse failure must also be surfaced when the clause yields **zero targets**, not only when the boundary heuristic returns false. A clause that matches nothing and extracts nothing currently produces no output at all. Worth noting alongside #107, which is the adjacent recall hole in the same notation (only the first target after an arrow is resolved). Both matter more after #99 than before it: the retrofit standardised roughly 30 of 39 descriptions on the compressed arrow form, so the notation with the two blind spots is now the corpus default. Related: #107, #117, #118, and the `validate-provenance.sh` bullet-form parser defect fixed in `484357a` on `refactor/adr0020-skill-retrofit` — three of the four gate blind spots found so far are "the check silently did not run", not "the check was wrong".
Author
Collaborator

Fixed on refactor/adr0020-skill-retrofit (not yet pushed — the PR body will carry the close), including the severity escalation in comment 2

The regex

[^.;] became a named CLAUSE_BODY:

CLAUSE_BODY = r"(?:[^.;]|\.(?=\S))"

A dot inside a filename is followed by non-space; a sentence-ending dot is followed by whitespace or end-of-string. Both ARROW_BOUNDARY (target extraction) and BOUNDARY_ARROW (presence heuristic) are built from it, so the two matchers cannot diverge on this again.

Three outcomes, as this issue asked for

boundary_clause_status() now returns present / absent / unparsed, and the two failure modes get different messages. The unparsed text says the clause is present, that this is a parse failure and not a missing clause, that the dangling-target check did not run on it, and names the most common cause (a bare single-word target, deliberately unmatched because research, triage and forge are all ordinary English).

The bare-target case from comment 2 is genuinely checked now

Not inferred — executed against a synthetic package:

description: Use when doing the thing. Not AGENTS.md -> no-such-skill-xyz.

ERROR: ... description routes to 'no-such-skill-xyz', which does not resolve to a
skill or agent in this monorepo, in this package, or in a package it declares in
apm.yml dependencies.apm (ADR-0020).
exit=1

So the protection no longer depends on the incidental backtick convention comment 2 identified. Bare, after a dotted filename, is extracted, resolved, and blocking.

All three hosts carry the change: scripts/skill-size-check.sh, skill-audit/scripts/validate.sh, agent-audit/scripts/validate.sh.

Remaining work — the wave-3 revert has NOT been done

Comment 1 asked that both workaround clauses be rewritten to name their files once the regex was fixed. They have not been. Current state on the branch:

Skill Boundary clause today Status
vale-run Not setting up Vale config or styles -> `vale-config`. unchanged — still does not name .vale.ini
agentsmd-audit Not converting a provider file -> `provider-adapter-author`. reworded again, still does not name CLAUDE.md

The gate no longer forces either wording, so both can now say what their authors judged best. Leaving this issue open on that basis.

## Fixed on `refactor/adr0020-skill-retrofit` (not yet pushed — the PR body will carry the close), including the severity escalation in comment 2 ### The regex `[^.;]` became a named `CLAUSE_BODY`: ```python CLAUSE_BODY = r"(?:[^.;]|\.(?=\S))" ``` A dot inside a filename is followed by non-space; a sentence-ending dot is followed by whitespace or end-of-string. Both `ARROW_BOUNDARY` (target extraction) and `BOUNDARY_ARROW` (presence heuristic) are built from it, so the two matchers cannot diverge on this again. ### Three outcomes, as this issue asked for `boundary_clause_status()` now returns `present` / `absent` / `unparsed`, and the two failure modes get different messages. The `unparsed` text says the clause **is** present, that this is a parse failure and not a missing clause, that the dangling-target check did not run on it, and names the most common cause (a bare single-word target, deliberately unmatched because `research`, `triage` and `forge` are all ordinary English). ### The bare-target case from comment 2 is genuinely checked now Not inferred — executed against a synthetic package: ``` description: Use when doing the thing. Not AGENTS.md -> no-such-skill-xyz. ERROR: ... description routes to 'no-such-skill-xyz', which does not resolve to a skill or agent in this monorepo, in this package, or in a package it declares in apm.yml dependencies.apm (ADR-0020). exit=1 ``` So the protection no longer depends on the incidental backtick convention comment 2 identified. Bare, after a dotted filename, is extracted, resolved, and blocking. All three hosts carry the change: `scripts/skill-size-check.sh`, `skill-audit/scripts/validate.sh`, `agent-audit/scripts/validate.sh`. ### Remaining work — the wave-3 revert has NOT been done Comment 1 asked that both workaround clauses be rewritten to name their files once the regex was fixed. They have not been. Current state on the branch: | Skill | Boundary clause today | Status | |---|---|---| | `vale-run` | ``Not setting up Vale config or styles -> `vale-config`.`` | unchanged — still does not name `.vale.ini` | | `agentsmd-audit` | ``Not converting a provider file -> `provider-adapter-author`.`` | reworded again, still does not name `CLAUDE.md` | The gate no longer forces either wording, so both can now say what their authors judged best. Leaving this issue open on that basis.
Author
Collaborator

Fixed by #129, squash-merged to main as 598a7c3.

Verified against the merged tree on the exact clause from this issue's report:

Not creating or editing `.pre-commit-config.yaml` -> `git-commits`.

The target git-commits is now extracted and carried into resolution. No "missing boundary clause" SUGGESTION — the misdiagnosing finding this issue is about is gone, and the gate no longer dictates prose to avoid a parsing artifact.

The related ask — that the gate distinguish "no boundary clause" from "boundary clause I could not parse" — also landed: an unparsable arrow clause now reports explicitly that the clause is present and this is a PARSE failure, so the dangling-target check did not run on it.

Stayed open after the merge because Gitea's closing-keyword parser consumed only the first reference in Closes #99, #107, …. Closing manually.

Fixed by #129, squash-merged to `main` as `598a7c3`. Verified against the merged tree on the exact clause from this issue's report: ```yaml Not creating or editing `.pre-commit-config.yaml` -> `git-commits`. ``` The target `git-commits` is now extracted and carried into resolution. No "missing boundary clause" SUGGESTION — the misdiagnosing finding this issue is about is gone, and the gate no longer dictates prose to avoid a parsing artifact. The related ask — that the gate distinguish "no boundary clause" from "boundary clause I could not parse" — also landed: an unparsable arrow clause now reports explicitly that the clause **is** present and this is a PARSE failure, so the dangling-target check did not run on it. Stayed open after the merge because Gitea's closing-keyword parser consumed only the first reference in `Closes #99, #107, …`. Closing manually.
Sign in to join this conversation.