mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(skills): parse metadata only from YAML frontmatter
This commit is contained in:
1 parent
0f64f1f855
commit
258a1dd3b7
11 files changed
+410
-31
No files matched your search
@@ -3,6 +3,12 @@ import { dirname, join } from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||||
|
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
|
||||||
|
|
||||||
|
const { parseSkillManifestMetadata } = await importVerifierSource(
|
||||||
|
new URL("../src/domain/skillManifest.ts", import.meta.url).href,
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
|
||||||
const arguments_ = new Set(process.argv.slice(2));
|
const arguments_ = new Set(process.argv.slice(2));
|
||||||
for (const argument of arguments_)
|
for (const argument of arguments_)
|
||||||
@@ -25,8 +31,10 @@ const skill = await readFile(
|
|||||||
join(root, ".agents/skills/reverse-engineer-anything/SKILL.md"),
|
join(root, ".agents/skills/reverse-engineer-anything/SKILL.md"),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
const skillVersion = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(skill)?.[1];
|
const skillMetadata = parseSkillManifestMetadata(skill);
|
||||||
if (skillVersion === undefined)
|
if (!skillMetadata.ok) throw skillMetadata.error;
|
||||||
|
const skillVersion = skillMetadata.value.version;
|
||||||
|
if (skillVersion === null)
|
||||||
throw new Error("Missing reverse-engineer-anything skill metadata version");
|
throw new Error("Missing reverse-engineer-anything skill metadata version");
|
||||||
const versionOf = (name) => {
|
const versionOf = (name) => {
|
||||||
const value = lock.packages?.[`node_modules/${name}`]?.version;
|
const value = lock.packages?.[`node_modules/${name}`]?.version;
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { dirname, join } from "node:path";
|
|||||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
|
||||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||||
|
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
|
||||||
|
|
||||||
|
const { addSkillManifestToolCount } = await importVerifierSource(
|
||||||
|
new URL("../src/domain/skillManifest.ts", import.meta.url).href,
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
|
||||||
const arguments_ = new Set(process.argv.slice(2));
|
const arguments_ = new Set(process.argv.slice(2));
|
||||||
for (const argument of arguments_)
|
for (const argument of arguments_)
|
||||||
@@ -23,22 +29,15 @@ const { FULL_CATALOG_IDENTITY } = await import(
|
|||||||
const { PRODUCT_IDENTITY } = await import(
|
const { PRODUCT_IDENTITY } = await import(
|
||||||
`${pathToFileURL(join(root, "dist/identity.js")).href}?${cacheBust}`
|
`${pathToFileURL(join(root, "dist/identity.js")).href}?${cacheBust}`
|
||||||
);
|
);
|
||||||
if (/^\s{2}(?:tool_count|catalog_digest):/mu.test(current))
|
|
||||||
throw new Error(
|
|
||||||
"Catalog metadata belongs in the generated skill, not the authored source",
|
|
||||||
);
|
|
||||||
const versionLine = /^ {2}version: "[^"\r\n]+"$/mu;
|
|
||||||
if (!versionLine.test(current))
|
|
||||||
throw new Error("Missing authored skill version");
|
|
||||||
// A shipped skill must run the version it ships with, not whatever `@latest` resolves to.
|
// A shipped skill must run the version it ships with, not whatever `@latest` resolves to.
|
||||||
const latestSpecifier = PRODUCT_IDENTITY.packageSpecifier;
|
const latestSpecifier = PRODUCT_IDENTITY.packageSpecifier;
|
||||||
const pinnedSpecifier = PRODUCT_IDENTITY.registrationPackageSpecifier;
|
const pinnedSpecifier = PRODUCT_IDENTITY.registrationPackageSpecifier;
|
||||||
const source = current
|
const generated = addSkillManifestToolCount(
|
||||||
.replace(
|
current,
|
||||||
versionLine,
|
FULL_CATALOG_IDENTITY.counts.mcp_tools,
|
||||||
`$&\n tool_count: ${String(FULL_CATALOG_IDENTITY.counts.mcp_tools)}`,
|
);
|
||||||
)
|
if (!generated.ok) throw generated.error;
|
||||||
.replaceAll(latestSpecifier, pinnedSpecifier);
|
const source = generated.value.replaceAll(latestSpecifier, pinnedSpecifier);
|
||||||
const paths = await filePaths(sourceRoot);
|
const paths = await filePaths(sourceRoot);
|
||||||
// Rebuild this owned output directory so removed references cannot survive a build.
|
// Rebuild this owned output directory so removed references cannot survive a build.
|
||||||
if (!check) await rm(outputRoot, { recursive: true, force: true });
|
if (!check) await rm(outputRoot, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
|
||||||
|
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
chmod,
|
chmod,
|
||||||
@@ -22,6 +24,11 @@ import {
|
|||||||
} from "./lib/verifier-cleanup.mjs";
|
} from "./lib/verifier-cleanup.mjs";
|
||||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||||
|
|
||||||
|
const { parseSkillManifestMetadata } = await importVerifierSource(
|
||||||
|
new URL("../src/domain/skillManifest.ts", import.meta.url).href,
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
|
||||||
const { evaluateCodexEvents } = await tsImport(
|
const { evaluateCodexEvents } = await tsImport(
|
||||||
new URL("../tests/evaluation/model-evals/CodexAgentEval.ts", import.meta.url)
|
new URL("../tests/evaluation/model-evals/CodexAgentEval.ts", import.meta.url)
|
||||||
.href,
|
.href,
|
||||||
@@ -148,8 +155,10 @@ await withVerifierCleanup(
|
|||||||
join(skillDestination, "SKILL.md"),
|
join(skillDestination, "SKILL.md"),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
|
const skillMetadata = parseSkillManifestMetadata(installedSkill);
|
||||||
if (
|
if (
|
||||||
!installedSkill.includes(` version: "${PRODUCT_IDENTITY.skillVersion}"`)
|
!skillMetadata.ok ||
|
||||||
|
skillMetadata.value.version !== PRODUCT_IDENTITY.skillVersion
|
||||||
)
|
)
|
||||||
throw new Error("Setup did not install the current packaged REA skill");
|
throw new Error("Setup did not install the current packaged REA skill");
|
||||||
const requestedScenarioIds = new Set(
|
const requestedScenarioIds = new Set(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
|
||||||
|
|
||||||
import { mkdir } from "node:fs/promises";
|
import { mkdir } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { FULL_CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
|
import { FULL_CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
|
||||||
@@ -5,6 +7,11 @@ import { FULL_CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
|
|||||||
import { parseTarListing } from "./lib/verify-package-listing.mjs";
|
import { parseTarListing } from "./lib/verify-package-listing.mjs";
|
||||||
import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
|
import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
|
||||||
|
|
||||||
|
const { parseSkillManifestMetadata } = await importVerifierSource(
|
||||||
|
new URL("../src/domain/skillManifest.ts", import.meta.url).href,
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
|
||||||
/** Create a tarball and assert production packaging constraints. */
|
/** Create a tarball and assert production packaging constraints. */
|
||||||
export async function verifyPackagePack({ root, workspace, commands }) {
|
export async function verifyPackagePack({ root, workspace, commands }) {
|
||||||
const packDirectory = join(workspace, "pack");
|
const packDirectory = join(workspace, "pack");
|
||||||
@@ -55,11 +62,11 @@ export async function verifyPackagePack({ root, workspace, commands }) {
|
|||||||
"package/skills/reverse-engineer-anything/SKILL.md",
|
"package/skills/reverse-engineer-anything/SKILL.md",
|
||||||
])
|
])
|
||||||
).stdout;
|
).stdout;
|
||||||
|
const skillMetadata = parseSkillManifestMetadata(skill);
|
||||||
if (
|
if (
|
||||||
!skill.includes(
|
!skillMetadata.ok ||
|
||||||
` tool_count: ${String(FULL_CATALOG_IDENTITY.counts.mcp_tools)}\n`,
|
skillMetadata.value.toolCount !== FULL_CATALOG_IDENTITY.counts.mcp_tools ||
|
||||||
) ||
|
skillMetadata.value.hasCatalogDigest
|
||||||
skill.includes(" catalog_digest:")
|
|
||||||
)
|
)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"packaged skill metadata did not match the catalog shipped in the package",
|
"packaged skill metadata did not match the catalog shipped in the package",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { copyFile, lstat, mkdir, readFile } from "node:fs/promises";
|
|||||||
import { constants as fsConstants } from "node:fs";
|
import { constants as fsConstants } from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { err, ok, type Result } from "../domain/result.js";
|
import { err, ok, type Result } from "../domain/result.js";
|
||||||
|
import { parseSkillManifestMetadata } from "../domain/skillManifest.js";
|
||||||
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
|
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
|
||||||
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||||
@@ -698,17 +699,15 @@ export const readInstalledSkillIdentity = async (
|
|||||||
? inspection.value.manifest
|
? inspection.value.manifest
|
||||||
: inspection.error.manifest;
|
: inspection.error.manifest;
|
||||||
const issues = inspection.ok ? [] : inspection.error.issues;
|
const issues = inspection.ok ? [] : inspection.error.issues;
|
||||||
const toolCount =
|
const parsedMetadata =
|
||||||
content === undefined
|
content === undefined ? undefined : parseSkillManifestMetadata(content);
|
||||||
? undefined
|
const metadata = parsedMetadata?.ok
|
||||||
: /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1];
|
? parsedMetadata.value
|
||||||
|
: parsedMetadata?.error.metadata;
|
||||||
return {
|
return {
|
||||||
canonical: inspection.ok && inspection.value.canonical,
|
canonical: inspection.ok && inspection.value.canonical,
|
||||||
version:
|
version: metadata?.version ?? null,
|
||||||
content === undefined
|
toolCount: metadata?.toolCount ?? null,
|
||||||
? null
|
|
||||||
: (/^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null),
|
|
||||||
toolCount: toolCount === undefined ? null : Number.parseInt(toolCount, 10),
|
|
||||||
...(issues.length === 0 ? {} : { issues }),
|
...(issues.length === 0 ? {} : { issues }),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
addSkillManifestToolCount,
|
||||||
|
parseSkillManifestMetadata,
|
||||||
|
} from "./skillManifest.js";
|
||||||
|
|
||||||
|
const manifest = (metadata: string, body = "\n# Instructions\n") =>
|
||||||
|
`---\nname: reverse-engineer-anything\nmetadata:\n${metadata}---\n${body}`;
|
||||||
|
|
||||||
|
it.each(["'49'", '"49"'])(
|
||||||
|
"reads semantic metadata with version %s",
|
||||||
|
(version) => {
|
||||||
|
expect(
|
||||||
|
parseSkillManifestMetadata(
|
||||||
|
manifest(` version: ${version}\n tool_count: 0\n`),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
version: "49",
|
||||||
|
toolCount: 0,
|
||||||
|
hasToolCount: true,
|
||||||
|
hasCatalogDigest: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("does not read counterfeit metadata from the Markdown body", () => {
|
||||||
|
const content =
|
||||||
|
'---\nname: reverse-engineer-anything\n---\n\n```yaml\nmetadata:\n version: "counterfeit"\n tool_count: 42\n```\n';
|
||||||
|
expect(parseSkillManifestMetadata(content)).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
version: null,
|
||||||
|
toolCount: null,
|
||||||
|
hasToolCount: false,
|
||||||
|
hasCatalogDigest: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(addSkillManifestToolCount(content, 5)).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { message: "Missing authored skill version" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["-1", "1.5", "9007199254740992", '"7"', "null"])(
|
||||||
|
"retains a valid version when tool_count is invalid: %s",
|
||||||
|
(count) => {
|
||||||
|
expect(
|
||||||
|
parseSkillManifestMetadata(
|
||||||
|
manifest(` version: '49'\n tool_count: ${count}\n`),
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "SkillManifestMetadataError",
|
||||||
|
metadata: { version: "49", toolCount: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
" version: [\n",
|
||||||
|
" version: '49'\n version: '50'\n",
|
||||||
|
" version: '49'\n tool_count: 1\n tool_count: 2\n",
|
||||||
|
])("rejects invalid or duplicate YAML metadata", (metadata) => {
|
||||||
|
expect(parseSkillManifestMetadata(manifest(metadata))).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retains a valid count when the version is invalid", () => {
|
||||||
|
expect(
|
||||||
|
parseSkillManifestMetadata(manifest(" version: 49\n tool_count: 7\n")),
|
||||||
|
).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { metadata: { version: null, toolCount: 7 } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates bounded frontmatter without changing Markdown examples or comments", () => {
|
||||||
|
const body =
|
||||||
|
'\n# Instructions\n\n```yaml\nmetadata:\n version: "body-only"\n tool_count: 99\n```\n';
|
||||||
|
const content = manifest(" version: '49' # keep version comment\n", body);
|
||||||
|
const generated = addSkillManifestToolCount(content, 7);
|
||||||
|
expect(generated.ok).toBe(true);
|
||||||
|
if (!generated.ok) throw generated.error;
|
||||||
|
expect(generated.value.endsWith(body)).toBe(true);
|
||||||
|
expect(generated.value).toContain("version: '49' # keep version comment");
|
||||||
|
expect(parseSkillManifestMetadata(generated.value)).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
value: { version: "49", toolCount: 7 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["tool_count: 0", "catalog_digest: abc"])(
|
||||||
|
"rejects authored generated metadata: %s",
|
||||||
|
(field) => {
|
||||||
|
expect(
|
||||||
|
addSkillManifestToolCount(manifest(` version: '49'\n ${field}\n`), 7),
|
||||||
|
).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { message: expect.stringContaining("Catalog metadata belongs") },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("rejects unsupported YAML tags without treating their scalars as metadata", () => {
|
||||||
|
expect(
|
||||||
|
parseSkillManifestMetadata(manifest(" version: !custom '49'\n")),
|
||||||
|
).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
message: expect.stringContaining("Unsupported skill YAML frontmatter"),
|
||||||
|
metadata: { version: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { isMap, parseDocument, type Document } from "yaml";
|
||||||
|
import { err, ok, type Result } from "./result.js";
|
||||||
|
|
||||||
|
export interface SkillManifestMetadata {
|
||||||
|
readonly version: string | null;
|
||||||
|
readonly toolCount: number | null;
|
||||||
|
readonly hasToolCount: boolean;
|
||||||
|
readonly hasCatalogDigest: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SkillManifestMetadataError extends Error {
|
||||||
|
readonly _tag = "SkillManifestMetadataError" as const;
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
readonly metadata: SkillManifestMetadata,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyMetadata = (): SkillManifestMetadata => ({
|
||||||
|
version: null,
|
||||||
|
toolCount: null,
|
||||||
|
hasToolCount: false,
|
||||||
|
hasCatalogDigest: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** The frontmatter range owns YAML; body examples never supply metadata. */
|
||||||
|
const readFrontmatter = (
|
||||||
|
content: string,
|
||||||
|
): Result<
|
||||||
|
{
|
||||||
|
document: Document;
|
||||||
|
body: string;
|
||||||
|
},
|
||||||
|
SkillManifestMetadataError
|
||||||
|
> => {
|
||||||
|
const opening = /^---\r?\n/u.exec(content);
|
||||||
|
if (opening === null)
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
"Skill manifest has no YAML frontmatter",
|
||||||
|
emptyMetadata(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const remainder = content.slice(opening[0].length);
|
||||||
|
const closing = /^---(?:\r?\n|$)/mu.exec(remainder);
|
||||||
|
if (closing === null)
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
"Skill manifest YAML frontmatter is unterminated",
|
||||||
|
emptyMetadata(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const document = parseDocument(remainder.slice(0, closing.index), {
|
||||||
|
logLevel: "silent",
|
||||||
|
});
|
||||||
|
if (document.errors.length > 0)
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
`Invalid skill YAML frontmatter: ${document.errors.map((error) => error.message).join("; ")}`,
|
||||||
|
emptyMetadata(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (document.warnings.length > 0)
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
`Unsupported skill YAML frontmatter: ${document.warnings.map((warning) => warning.message).join("; ")}`,
|
||||||
|
emptyMetadata(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!isMap(document.contents))
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
"Skill YAML frontmatter must be a mapping",
|
||||||
|
emptyMetadata(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return ok({
|
||||||
|
document,
|
||||||
|
body: remainder.slice(closing.index + closing[0].length),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const metadataOf = (
|
||||||
|
document: Document,
|
||||||
|
): Result<SkillManifestMetadata, SkillManifestMetadataError> => {
|
||||||
|
const metadata = document.get("metadata", true);
|
||||||
|
if (metadata === undefined) return ok(emptyMetadata());
|
||||||
|
if (!isMap(metadata))
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
"Skill metadata must be a mapping",
|
||||||
|
emptyMetadata(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const version = document.getIn(["metadata", "version"]);
|
||||||
|
const toolCount = document.getIn(["metadata", "tool_count"]);
|
||||||
|
const facts: SkillManifestMetadata = {
|
||||||
|
version: typeof version === "string" && version.length > 0 ? version : null,
|
||||||
|
toolCount:
|
||||||
|
typeof toolCount === "number" &&
|
||||||
|
Number.isSafeInteger(toolCount) &&
|
||||||
|
toolCount >= 0
|
||||||
|
? toolCount
|
||||||
|
: null,
|
||||||
|
hasToolCount: metadata.has("tool_count"),
|
||||||
|
hasCatalogDigest: metadata.has("catalog_digest"),
|
||||||
|
};
|
||||||
|
const issues = [];
|
||||||
|
if (metadata.has("version") && facts.version === null)
|
||||||
|
issues.push("version must be a nonempty string");
|
||||||
|
if (facts.hasToolCount && facts.toolCount === null)
|
||||||
|
issues.push("tool_count must be a safe nonnegative integer");
|
||||||
|
return issues.length === 0
|
||||||
|
? ok(facts)
|
||||||
|
: err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
`Invalid skill metadata: ${issues.join("; ")}`,
|
||||||
|
facts,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseSkillManifestMetadata = (
|
||||||
|
content: string,
|
||||||
|
): Result<SkillManifestMetadata, SkillManifestMetadataError> => {
|
||||||
|
const frontmatter = readFrontmatter(content);
|
||||||
|
return frontmatter.ok ? metadataOf(frontmatter.value.document) : frontmatter;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Update only authored YAML frontmatter, retaining the instruction body. */
|
||||||
|
export const addSkillManifestToolCount = (
|
||||||
|
content: string,
|
||||||
|
toolCount: number,
|
||||||
|
): Result<string, SkillManifestMetadataError> => {
|
||||||
|
const frontmatter = readFrontmatter(content);
|
||||||
|
if (!frontmatter.ok) return frontmatter;
|
||||||
|
const metadata = metadataOf(frontmatter.value.document);
|
||||||
|
if (!metadata.ok) return metadata;
|
||||||
|
if (metadata.value.version === null)
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
"Missing authored skill version",
|
||||||
|
metadata.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (metadata.value.hasToolCount || metadata.value.hasCatalogDigest)
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
"Catalog metadata belongs in the generated skill, not the authored source",
|
||||||
|
metadata.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!Number.isSafeInteger(toolCount) || toolCount < 0)
|
||||||
|
return err(
|
||||||
|
new SkillManifestMetadataError(
|
||||||
|
"Generated tool_count must be a safe nonnegative integer",
|
||||||
|
metadata.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const { document, body } = frontmatter.value;
|
||||||
|
document.setIn(["metadata", "tool_count"], toolCount);
|
||||||
|
return ok(`---\n${document.toString({ lineWidth: 0 })}---\n${body}`);
|
||||||
|
};
|
||||||
@@ -205,6 +205,9 @@ const createBuildCacheFixture = async (): Promise<string> => {
|
|||||||
"scripts/clean-build-output.mjs",
|
"scripts/clean-build-output.mjs",
|
||||||
"scripts/generate-package-metadata.mjs",
|
"scripts/generate-package-metadata.mjs",
|
||||||
"scripts/lib/generated-file.mjs",
|
"scripts/lib/generated-file.mjs",
|
||||||
|
"scripts/lib/verifier-source-loader.mjs",
|
||||||
|
"src/domain/skillManifest.ts",
|
||||||
|
"src/domain/result.ts",
|
||||||
".agents/skills/reverse-engineer-anything/SKILL.md",
|
".agents/skills/reverse-engineer-anything/SKILL.md",
|
||||||
"src/generatedPackageMetadata.ts",
|
"src/generatedPackageMetadata.ts",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
import { copyFile, cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
@@ -17,10 +17,19 @@ async function fixture(eol = "\n") {
|
|||||||
for (const path of [
|
for (const path of [
|
||||||
"scripts/generate-skill-metadata.mjs",
|
"scripts/generate-skill-metadata.mjs",
|
||||||
"scripts/lib/generated-file.mjs",
|
"scripts/lib/generated-file.mjs",
|
||||||
|
"scripts/lib/verifier-source-loader.mjs",
|
||||||
|
"src/domain/skillManifest.ts",
|
||||||
|
"src/domain/result.ts",
|
||||||
]) {
|
]) {
|
||||||
await mkdir(dirname(join(root, path)), { recursive: true });
|
await mkdir(dirname(join(root, path)), { recursive: true });
|
||||||
await copyFile(join(repositoryRoot, path), join(root, path));
|
await copyFile(join(repositoryRoot, path), join(root, path));
|
||||||
}
|
}
|
||||||
|
await mkdir(join(root, "node_modules"));
|
||||||
|
await cp(
|
||||||
|
join(repositoryRoot, "node_modules/yaml"),
|
||||||
|
join(root, "node_modules/yaml"),
|
||||||
|
{ recursive: true },
|
||||||
|
);
|
||||||
const sourceRoot = join(root, ".agents/skills/reverse-engineer-anything");
|
const sourceRoot = join(root, ".agents/skills/reverse-engineer-anything");
|
||||||
const outputRoot = join(root, "skills/reverse-engineer-anything");
|
const outputRoot = join(root, "skills/reverse-engineer-anything");
|
||||||
await mkdir(join(sourceRoot, "references"), { recursive: true });
|
await mkdir(join(sourceRoot, "references"), { recursive: true });
|
||||||
@@ -81,7 +90,13 @@ it.each(["\n", "\r\n"])(
|
|||||||
expect(
|
expect(
|
||||||
await readFile(join(f.sourceRoot, "references/guide.md"), "utf8"),
|
await readFile(join(f.sourceRoot, "references/guide.md"), "utf8"),
|
||||||
).toBe(`# Guide${eol}Preserve evidence.${eol}`);
|
).toBe(`# Guide${eol}Preserve evidence.${eol}`);
|
||||||
await expect(f.generate("--check")).resolves.toMatchObject({ stderr: "" });
|
const checked = await f.generate("--check");
|
||||||
|
expect(
|
||||||
|
checked.stderr.replace(
|
||||||
|
/^\(node:\d+\) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time\r?\n\(Use `node --trace-warnings \.\.\.` to show where the warning was created\)\r?\n$/u,
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
).toBe("");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -143,3 +158,30 @@ it("rejects catalog-dependent metadata in the authored source", async () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("generates metadata from quoted frontmatter while preserving body metadata examples", async () => {
|
||||||
|
const f = await fixture();
|
||||||
|
const body =
|
||||||
|
'\n# Instructions\n\n```yaml\nmetadata:\n version: "body-only"\n tool_count: 99\n catalog_digest: example\n```\n';
|
||||||
|
await writeFile(
|
||||||
|
join(f.sourceRoot, "SKILL.md"),
|
||||||
|
"---\nname: reverse-engineer-anything\nmetadata:\n version: '33'\n---\n" +
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
await f.generate();
|
||||||
|
const generated = await readFile(join(f.outputRoot, "SKILL.md"), "utf8");
|
||||||
|
expect(generated.endsWith(body)).toBe(true);
|
||||||
|
expect(generated).toContain("version: '33'");
|
||||||
|
expect(generated).toContain("tool_count: 139");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects body-only versions instead of generating metadata inside examples", async () => {
|
||||||
|
const f = await fixture();
|
||||||
|
await writeFile(
|
||||||
|
join(f.sourceRoot, "SKILL.md"),
|
||||||
|
'---\nname: reverse-engineer-anything\n---\n\n```yaml\nmetadata:\n version: "body-only"\n```\n',
|
||||||
|
);
|
||||||
|
await expect(f.generate()).rejects.toMatchObject({
|
||||||
|
stderr: expect.stringContaining("Missing authored skill version"),
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -720,3 +720,26 @@ it("doctor verifies installed instructions and references even when metadata is
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reads installed frontmatter metadata without treating body examples as identity", async () => {
|
||||||
|
const home = await createTestTempDirectory("rea-installed-metadata-");
|
||||||
|
const installed = await installCanonicalSkill(home, ["claude_code"]);
|
||||||
|
expect(installed.ok).toBe(true);
|
||||||
|
const destination = skillDestinations(home, ["claude_code"])[0];
|
||||||
|
if (destination === undefined) throw new Error("Missing skill destination");
|
||||||
|
const manifest = join(destination.path, "SKILL.md");
|
||||||
|
await writeFile(
|
||||||
|
manifest,
|
||||||
|
"---\nname: reverse-engineer-anything\nmetadata:\n version: '49'\n tool_count: -1\n---\n\n```yaml\nmetadata:\n version: \"counterfeit\"\n tool_count: 42\n```\n",
|
||||||
|
);
|
||||||
|
expect(await readInstalledSkillIdentity(home, ["claude_code"])).toMatchObject(
|
||||||
|
{ version: "49", toolCount: null, canonical: false },
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
manifest,
|
||||||
|
'---\nname: reverse-engineer-anything\n---\n\n```yaml\nmetadata:\n version: "counterfeit"\n tool_count: 42\n```\n',
|
||||||
|
);
|
||||||
|
expect(await readInstalledSkillIdentity(home, ["claude_code"])).toMatchObject(
|
||||||
|
{ version: null, toolCount: null, canonical: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -64,6 +64,9 @@
|
|||||||
"package-lock.json",
|
"package-lock.json",
|
||||||
"scripts/generate-package-metadata.mjs",
|
"scripts/generate-package-metadata.mjs",
|
||||||
"scripts/lib/generated-file.mjs",
|
"scripts/lib/generated-file.mjs",
|
||||||
|
"scripts/lib/verifier-source-loader.mjs",
|
||||||
|
"src/domain/skillManifest.ts",
|
||||||
|
"src/domain/result.ts",
|
||||||
".agents/skills/reverse-engineer-anything/SKILL.md",
|
".agents/skills/reverse-engineer-anything/SKILL.md",
|
||||||
"src/generatedPackageMetadata.ts"
|
"src/generatedPackageMetadata.ts"
|
||||||
]
|
]
|
||||||
@@ -213,6 +216,7 @@
|
|||||||
"package-lock.json",
|
"package-lock.json",
|
||||||
"scripts/generate-skill-metadata.mjs",
|
"scripts/generate-skill-metadata.mjs",
|
||||||
"scripts/lib/generated-file.mjs",
|
"scripts/lib/generated-file.mjs",
|
||||||
|
"scripts/lib/verifier-source-loader.mjs",
|
||||||
".agents/skills/reverse-engineer-anything/**"
|
".agents/skills/reverse-engineer-anything/**"
|
||||||
],
|
],
|
||||||
"outputs": ["skills/**"]
|
"outputs": ["skills/**"]
|
||||||
|
|||||||
Reference in new issue
Block a user