fix: address Copilot review round 7 on #2248

Four findings.

**The CLI dropped what it promised to print.** `getSkill` transforms the
`skills/get` envelope to a `SkillEntry`, so every other member the loose
schema accepted was gone before the CLI saw it — including the `ttlMs` /
`cacheScope` SEP-2640 explicitly leaves open. The code contradicted a comment
two lines above it saying a CLI whose contract is "print the result" must not
reshape one. `getSkillResult` returns the envelope whole; `getSkill` is a
one-line unwrap over it, which matches the callers: the UIs want the entry,
the CLI wants the result.

**The frontmatter verdict and the digest came from different reads.** The
preview fetch and the Verify fetch are separate `resources/read` calls, so a
resource changing between them could pair a verified digest with a
frontmatter verdict for other bytes. `VerificationState.entryText` now holds
the SKILL.md as the verification read it, written in the SAME state update as
the verdict — a separate write was clobbered, because `write` returns a fresh
object and drops anything not named in it. The preview stays the fallback so
a reader who has not clicked Verify still gets the check.

**The Conformance badge excluded the frontmatter findings** it renders, so it
said `0 error(s)` above a red mismatch. Digest and size mismatches stay in
their own badge: "the listing is wrong" and "the bytes are wrong" are
different answers.

**A repeated skill URI collided two React keys** in the TUI list, in a pane
whose job is to show both entries. Index-keyed like the manifest rows.

The web fixture also gains its collision pair in the served-file lookup —
without it they were handed another skill's SKILL.md, which the badge fix
correctly began reporting as a frontmatter error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND46Ci64DC3tzBy9aG81Nc
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-07 20:44:51 -04:00
commit d6b1e36e7e
2 changed files with 257 additions and 31 deletions

View file

@ -146,13 +146,14 @@ import {
import { buildClientExtensions } from "./extensions.js";
import {
DirectoryReadResultSchema,
GetSkillResultSchema,
GetSkillEnvelopeSchema,
ListSkillsResultSchema,
ModernDirectoryReadResultSchema,
ModernListSkillsResultSchema,
RESOURCES_DIRECTORY_READ_METHOD,
SKILLS_EXTENSION_KEY,
type DirectoryReadResult,
type GetSkillEnvelope,
SKILLS_GET_METHOD,
SKILLS_LIST_METHOD,
type SkillEntry,
@ -5609,11 +5610,28 @@ export class InspectorClient extends InspectorClientEventTarget {
/**
* One skill entry by URI (`skills/get`, SEP-2640). The result envelope is
* required — `GetSkillResultSchema` unwraps `{ skill }` and rejects an entry
* required — `GetSkillEnvelopeSchema` requires `{ skill }` and rejects an entry
* returned inline, so a non-conforming shape fails here rather than being
* silently normalized past the conformance checks.
*/
async getSkill(uri: string, metadata?: RequestMetadata): Promise<SkillEntry> {
return (await this.getSkillResult(uri, metadata)).skill;
}
/**
* `skills/get` as the server sent it — the `{ skill }` envelope **and any
* other members it carried**.
*
* Separate from {@link getSkill} because the callers differ: the UIs want the
* entry, while the CLI prints the result and must not reshape it. SEP-2640
* explicitly leaves open whether this result carries `ttlMs` / `cacheScope`,
* so a server may send them — and unwrapping to the entry discards exactly
* those (Copilot).
*/
async getSkillResult(
uri: string,
metadata?: RequestMetadata,
): Promise<GetSkillEnvelope> {
if (!this.client) {
throw new Error("Client is not connected");
}
@ -5622,14 +5640,13 @@ export class InspectorClient extends InspectorClientEventTarget {
uri,
...(effectiveMeta ? { _meta: effectiveMeta } : {}),
};
// `GetSkillResultSchema` unwraps the envelope, so there is nothing to
// unwrap here.
// The envelope is returned whole; `getSkill` is the one that unwraps.
try {
return await this.invokeMcpClient(
() =>
this.client!.request(
{ method: SKILLS_GET_METHOD, params },
GetSkillResultSchema,
GetSkillEnvelopeSchema,
this.getRequestOptions(this.progressTokenOf(metadata)),
),
{ method: SKILLS_GET_METHOD },

View file

@ -167,7 +167,21 @@ export const ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
* — which is exactly the failure this extension's support exists to *report*.
* A server that returns the entry inline now fails the parse, loudly.
*/
const GetSkillEnvelopeSchema = z.looseObject({ skill: SkillEntrySchema });
export const GetSkillEnvelopeSchema = z.looseObject({
skill: SkillEntrySchema,
});
/**
* The `skills/get` result as the server sent it, envelope and all.
*
* Exported alongside the unwrapping schema below because the two callers want
* different things: the UIs want the entry, while the CLI's job is to print
* **the result** — and the caching attributes SEP-2640 leaves open are members
* a `looseObject` accepts and the transform then discards, so unwrapping for
* everyone silently dropped them from a contract that promised not to reshape
* anything (Copilot).
*/
export type GetSkillEnvelope = z.infer<typeof GetSkillEnvelopeSchema>;
/**
* `skills/get` result, unwrapped to the entry it carries.