docs: address Copilot review round 6 on #2203

The truncation guard rejected valid frontmatter. It assumed the only way
a raw scalar can exceed its parsed value is a comment, but YAML also
strips presentation syntax, so `description: &copy Some text` and
`description: !!str Some text` were both reported as truncated — naming
an unquoted `#` the value does not contain.

The length check is now gated on an actual comment marker: `#` at the
start of the scalar or preceded by whitespace, which is where YAML begins
a comment. Anchors and tags fall out before the length is consulted.
Regression cases cover both, and assert the genuine `#` case still fails
so the fix cannot pass by disarming the guard.

Also corrects the checklist, which still said an unquoted `#` lets every
check pass. That predated the verifier; verify:skills rejects it now, and
the sentence was telling authors the gate is blind to something it
catches.

No skill description or eval case changed, so skills:eval is unchanged
from round 4's 63/63. local:gate passes, 34/34. 391/391 script tests.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-01 11:44:57 -04:00
commit 5d3571597a
3 changed files with 41 additions and 1 deletions

View file

@ -234,7 +234,10 @@ break.
description containing `:` or `#` is **quoted**. An unquoted colon loads the
body with an _empty_ description, so `/name` still works while the skill can
never auto-fire again; an unquoted `#` is worse-behaved, truncating the
description from that point on so it stays non-empty and every check passes.
description from that point on so it stays *non-empty* — which is why it went
unnoticed until `board-ops` lost both its board numbers. `verify:skills` now
rejects that truncation, so the gate catches it; quote the value rather than
relying on the check to tell you.
2. `disable-model-invocation` is declared explicitly; default it to `false`.
3. Description is action-first, with `Use when …; when …; when …`.
4. **If the skill is model-invoked**, `evals/evals.json` carries **at least

View file

@ -192,6 +192,14 @@ export function parseSkill(dirName, text) {
const collapse = (s) => s.replace(/\s+/g, " ").trim();
const rawText = collapse(parts.join(" "));
const keptText = collapse(parsed);
// A raw scalar can legitimately be longer than the parsed value without any
// text being lost: YAML strips presentation syntax such as an anchor
// (`&name`) or a tag (`!!str`) from the value. Comparing lengths alone
// reported those as truncation, with a message blaming a `#` that is not
// there. Only look at the length when the scalar actually contains a comment
// marker — a `#` at the start or preceded by whitespace, which is precisely
// where YAML begins a comment (Copilot).
if (!/(^|\s)#/.test(rawText)) continue;
if (rawText.length > keptText.length) {
errors.push(
"`" +

View file

@ -504,3 +504,32 @@ test("the truncation guard spans a multiline plain scalar", () => {
assert.deepEqual(ok.errors, []);
assert.equal(ok.description, "Covers boards and the option-deletion hazard.");
});
test("the truncation guard ignores anchors and tags, which are not `#`", () => {
// YAML strips presentation syntax from the parsed value, so `&anchor` and
// `!!tag` make the raw scalar legitimately longer than what it parses to.
// A bare length comparison called those truncation and blamed a `#` that was
// nowhere in the text.
const mk = (value) =>
[
"---",
"name: alpha",
`description: ${value}`,
"disable-model-invocation: false",
"---",
"body",
].join("\n");
const anchored = parseSkill("alpha", mk("&copy Some valid text."));
assert.deepEqual(anchored.errors, []);
assert.equal(anchored.description, "Some valid text.");
const tagged = parseSkill("alpha", mk("!!str Some tagged text."));
assert.deepEqual(tagged.errors, []);
// The real case must still fail, so the gate is not simply disarmed.
assert.match(
parseSkill("alpha", mk("Covers board #28 (v2).")).errors.join(" "),
/`description` is truncated/,
);
});