chore: address Copilot review round 1

- skill-manifest: the closing `---` fence must occupy a whole line. `---oops`
  was accepted as a terminator, silently truncating the frontmatter and passing
  validation — the exact failure the guard exists to catch. Regression cases
  added (prefix, trailing whitespace, EOF, CRLF).
- skill-eval: reject a nonzero `claude -p` exit instead of resolving it as an
  empty observation. An auth error or rate limit was scoring as "no skill
  invoked", which passes every negative case and reads as a trigger miss on
  every positive one.
- skill-eval: extract `collectSkillInvocations` / `sampleHit` behind a `main()`
  guard and cover them in `skill-eval.test.mjs` — the stream parsing and the
  process-outcome handling are unreachable from an eval run's happy path.
- docs/test-servers: annotate the era on every catalogue row. Six legacy rows
  carried no annotation under a "unless noted, Modern" default, so the doc sent
  readers to the wrong era for the feature they were trying to see.
- docs/quality-gate: `validate` runs four guards, not three; add the
  `verify:skills` row and the two new `test:scripts` suites; fix the `#publishing`
  fragment left dangling by the split.
- issue-create / pr-flow: delegate the card steps to `/board-ops` rather than
  restating its option IDs, which are explicitly unstable.
- issue-triage: the audit counted presence, not cardinality — an issue carrying
  both `v1` and `v2` passed a check that claims "exactly one". Same for the five
  type labels. Also scope the two milestone checks to board #28.
- AGENTS.md: codify the v1 milestone exception. Every milestone is a v2 release
  bucket, so a v1 issue has none to take and the Incoming-milestone invariant
  cannot apply to board #11.
- clients/launcher/README: its pointer promised an explanation of why the cli/tui
  manifests ship, which the extracted guide does not make.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-29 00:52:45 -04:00
commit a51e62dfa1
2 changed files with 248 additions and 83 deletions

View file

@ -49,12 +49,15 @@ export function splitFrontmatter(text) {
};
}
const rest = text.slice(FRONTMATTER_FENCE.length + 1);
const end = rest.indexOf("\n" + FRONTMATTER_FENCE);
if (end === -1)
// The closing fence must occupy the WHOLE line. Matching a mere prefix would
// accept `---oops` as a terminator, silently truncating the frontmatter to
// whatever preceded it — the very failure this function exists to catch.
const close = /\n---[ \t]*(\r?\n|$)/.exec(rest);
if (close === null)
return { error: "frontmatter is never closed by a `---` line" };
return {
frontmatter: rest.slice(0, end),
body: rest.slice(end + FRONTMATTER_FENCE.length + 2),
frontmatter: rest.slice(0, close.index),
body: rest.slice(close.index + close[0].length),
};
}

View file

@ -28,6 +28,25 @@ test("splitFrontmatter requires the fence on the very first line", () => {
assert.equal(splitFrontmatter(fm("name: x")).error, undefined);
});
test("splitFrontmatter requires the closing fence to be a whole line", () => {
// `---oops` is not a terminator. Accepting it would silently truncate the
// frontmatter to whatever preceded it and report the file as valid.
assert.match(
splitFrontmatter("---\nname: x\n---oops\nbody\n").error ?? "",
/never closed/,
);
// Trailing whitespace on the fence is still a fence, and so is EOF.
assert.equal(
splitFrontmatter("---\nname: x\n--- \nbody\n").frontmatter,
"name: x",
);
assert.equal(splitFrontmatter("---\nname: x\n---").frontmatter, "name: x");
assert.equal(
splitFrontmatter("---\nname: x\n---\r\nbody").frontmatter,
"name: x",
);
});
test("splitFrontmatter reports an unterminated block", () => {
assert.match(splitFrontmatter("---\nname: x\n").error ?? "", /never closed/);
});