diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36d4425157..7d489205bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,15 @@ the same configuration. Pre-commit formats and lints staged files; pre-push runs `check:fast`. `docs:check` checks committed generated metadata. `docs:generate` regenerates those files, and the docs CI lane checks them. +The committed product catalog contains documented facts and their provider +identity, rather than full runtime schema hashes. The managed skill contains +instructions and inventory metadata; doctor compares its installed files with +the canonical bundle. Schema-only fixes should not change these files or the +skill commitment in the conformance manifest. Runtime schema identity remains +available through doctor and `binary_session`. If a branch conflicts only in +these generated files, merge the source changes first, then run +`npm run docs:generate` to regenerate from the merged sources; keep hand-written +skill instructions from both branches. Real-provider execution remains uncached; deterministic builds use Turbo. Local `npm test` runs every deterministic Vitest project without coverage or diff --git a/docs/product-catalog.json b/docs/product-catalog.json index 7a83ccb5fa..3f05b7e2b0 100644 --- a/docs/product-catalog.json +++ b/docs/product-catalog.json @@ -713,9 +713,6 @@ "mcp_prompts": 6 }, "digests": { - "tools_sha256": "0ac2316e6bd007d273c60f69e132341fc06a3dcb6879408ecfdd3271f30a8230", - "prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d", - "combined_sha256": "20cc1432b29f11c20206cbaeacafab927dcfd68c81a82cf198ad058f54f06eac", "providers_sha256": "954b499c76514890062d2b6c936d73cb3a9c45412d522a081125a325e81cdec5" } } diff --git a/docs/verification/managed-conformance-manifest.json b/docs/verification/managed-conformance-manifest.json index e5c28005c4..f83f01995f 100644 --- a/docs/verification/managed-conformance-manifest.json +++ b/docs/verification/managed-conformance-manifest.json @@ -1,5 +1,5 @@ { - "manifest_id": "ecm_bee7251323bddbe561003a45ddee6c43eb9e37839983dc89e438dca6d9e5fdec", + "manifest_id": "ecm_0abe486e63d1ebdeec6487dfd19a5bd92d47a90d84e60df96926fa66602552ea", "verifier": { "id": "rea-managed-conformance", "version": "1" @@ -13,7 +13,7 @@ "skill_digests": [ { "skill_id": "reverse-engineer-anything", - "sha256": "62f197bbd05ca97ff21cc69f45ccf776c152004f3c7f1bd589dfea0b8202db57" + "sha256": "0e6130b292e3cd3fe1da214b2b3ca5eba8661c81a5bf64679b7ef29828393f29" } ], "claims": [ diff --git a/scripts/generate-skill-metadata.mjs b/scripts/generate-skill-metadata.mjs index 3d4bc4bbca..f1bdbd769c 100644 --- a/scripts/generate-skill-metadata.mjs +++ b/scripts/generate-skill-metadata.mjs @@ -19,13 +19,12 @@ const withCount = current.replace( /^\s{2}tool_count:\s*\d+\s*$/mu, ` tool_count: ${String(CATALOG_IDENTITY.counts.mcp_tools)}`, ); -const digestLine = ` catalog_digest: "${CATALOG_IDENTITY.digests.combined_sha256}"`; -const source = /^\s{2}catalog_digest:/mu.test(withCount) - ? withCount.replace( - /^\s{2}catalog_digest:\s*"[a-f0-9]{64}"\s*$/mu, - digestLine, - ) - : withCount.replace(/^(\s{2}tool_count:\s*\d+\s*)$/mu, `$1\n${digestLine}`); +// Skill identity follows the installed instruction bundle. Runtime schema identity +// is still reported independently by doctor and binary_session. +const source = withCount.replace( + /^ {2}catalog_digest:[^\r\n]*(?:\r?\n|$)/mu, + "", +); await ensureGeneratedFile({ path, diff --git a/scripts/lib/product-catalog.mjs b/scripts/lib/product-catalog.mjs index 8c4aaae4f9..21a421b250 100644 --- a/scripts/lib/product-catalog.mjs +++ b/scripts/lib/product-catalog.mjs @@ -58,7 +58,7 @@ export const createProductCatalog = async (root) => { runtime_catalog: { counts: sources.catalogIdentity.CATALOG_IDENTITY.counts, digests: { - ...sources.catalogIdentity.CATALOG_IDENTITY.digests, + // Full runtime schema digests belong to server identity, not this facts projection. providers_sha256: providerCatalogDigest(providers), }, }, diff --git a/skills/reverse-engineer-anything/SKILL.md b/skills/reverse-engineer-anything/SKILL.md index f97a188a1d..79923da1f3 100644 --- a/skills/reverse-engineer-anything/SKILL.md +++ b/skills/reverse-engineer-anything/SKILL.md @@ -4,7 +4,6 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi metadata: version: "33" tool_count: 139 - catalog_digest: "20cc1432b29f11c20206cbaeacafab927dcfd68c81a82cf198ad058f54f06eac" --- # REA diff --git a/src/application/Doctor.fixture.ts b/src/application/Doctor.fixture.ts index 2c4725dfac..9f271ed221 100644 --- a/src/application/Doctor.fixture.ts +++ b/src/application/Doctor.fixture.ts @@ -26,7 +26,8 @@ export const createDoctorHostFixture = ( Promise.resolve({ version: PRODUCT_IDENTITY.skillVersion, toolCount: CATALOG_IDENTITY.counts.mcp_tools, - catalogDigest: CATALOG_IDENTITY.digests.combined_sha256, + catalogDigest: null, + canonical: true, }), ...overrides, }); diff --git a/src/application/Doctor.test.ts b/src/application/Doctor.test.ts index af7881c5bf..d6845245a1 100644 --- a/src/application/Doctor.test.ts +++ b/src/application/Doctor.test.ts @@ -177,6 +177,7 @@ describe("doctor installation identity", () => { version: "10", toolCount: null, catalogDigest: null, + canonical: false, }), }), ); diff --git a/src/application/Doctor.ts b/src/application/Doctor.ts index a4cf57e9cc..078db0d73d 100644 --- a/src/application/Doctor.ts +++ b/src/application/Doctor.ts @@ -14,6 +14,7 @@ import { readLinuxDistribution, type LinuxDistribution, } from "./LinuxHopper.js"; +import { canonicalSkillNeedsInstall } from "./SetupSkill.js"; import { CATALOG_IDENTITY } from "../catalogIdentity.js"; import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js"; import { @@ -95,11 +96,13 @@ export interface DoctorHost { ilspyCmdVersion?(path: string): Promise; } -/** Parsed identity committed by an installed REA skill. */ +/** Observed skill metadata and comparison with the packaged instruction bundle. */ interface InstalledSkillIdentity { readonly version: string | null; readonly toolCount: number | null; readonly catalogDigest: string | null; + /** Whether all managed instruction and reference files match this package. */ + readonly canonical: boolean; } /** Structured result returned by the read-only doctor workflow. */ @@ -139,6 +142,7 @@ interface DoctorIdentity { readonly skill: { readonly installed_version: string | null; readonly installed_tool_count: number | null; + /** Legacy catalog digest observed in older bundles; current bundles omit it. */ readonly installed_catalog_digest: string | null; readonly state: "aligned" | "stale" | "missing"; readonly remediation: string | null; @@ -253,7 +257,7 @@ const skillIdentityAligned = ( ): boolean => identity?.version === PRODUCT_IDENTITY.skillVersion && identity.toolCount === CATALOG_IDENTITY.counts.mcp_tools && - identity.catalogDigest === CATALOG_IDENTITY.digests.combined_sha256; + identity.canonical; const skillIdentityCheck = ( identity: InstalledSkillIdentity | undefined, @@ -507,6 +511,7 @@ const installedSkillIdentity = async ( const content = await readInstalledSkill(home); const countText = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1]; return { + canonical: !(await canonicalSkillNeedsInstall(home)), version: /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null, toolCount: countText === undefined ? null : Number.parseInt(countText, 10), diff --git a/tests/boundary/cli/productCatalog.test.ts b/tests/boundary/cli/productCatalog.test.ts index 3eb9f6528e..77fdec7df3 100644 --- a/tests/boundary/cli/productCatalog.test.ts +++ b/tests/boundary/cli/productCatalog.test.ts @@ -195,6 +195,11 @@ describe("canonical product catalog", () => { "project_managed_application_graph", "verify_managed_native_boundaries", ]); + // Runtime schema commitments must not fan out into checked-in documentation. + // Provider commitments cover the facts actually present in this projection. + expect(Object.keys(catalog.runtime_catalog.digests)).toEqual([ + "providers_sha256", + ]); expect(catalog.runtime_catalog.digests.providers_sha256).toBe( providerCatalogDigest(catalog.providers), ); diff --git a/tests/boundary/filesystem/setupSkill.test.ts b/tests/boundary/filesystem/setupSkill.test.ts index a0f0b47383..aa457b0571 100644 --- a/tests/boundary/filesystem/setupSkill.test.ts +++ b/tests/boundary/filesystem/setupSkill.test.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -7,6 +7,11 @@ import { canonicalSkillNeedsInstall, installCanonicalSkill, } from "../../../src/application/SetupSkill.js"; +import { + runDoctor, + systemDoctorHost, +} from "../../../src/application/Doctor.js"; +import { createDoctorHostFixture } from "../../../src/application/Doctor.fixture.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; import { skillReferenceIssues } from "../../../scripts/lib/docs-facts.mjs"; @@ -98,3 +103,62 @@ describe("canonical skill transaction", () => { expect(await skillReferenceIssues(dirname(destination))).toEqual([]); }); }); + +it("doctor verifies installed instructions and references even when metadata is current", async () => { + const home = await createTestTempDirectory("rea-skill-doctor-"); + const destination = join( + home, + ".agents/skills/reverse-engineer-anything/SKILL.md", + ); + const reference = join( + dirname(destination), + "references/evidence-workflows.md", + ); + const system = systemDoctorHost({ + environment: { HOME: home, USERPROFILE: home }, + }); + const host = createDoctorHostFixture({ + installedSkillIdentity: () => + system.installedSkillIdentity?.() ?? Promise.resolve(undefined), + }); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "missing", + ); + expect(await installCanonicalSkill(home)).toBe("installed"); + expect((await runDoctor(undefined, host)).identity?.skill).toMatchObject({ + state: "aligned", + installed_catalog_digest: null, + }); + const legacyDigest = "0".repeat(64); + await writeFile( + destination, + (await readFile(destination, "utf8")).replace( + ` tool_count: ${String(TOOL_CONTRACTS.length)}`, + ` tool_count: ${String(TOOL_CONTRACTS.length)}\n catalog_digest: "${legacyDigest}"`, + ), + ); + expect((await runDoctor(undefined, host)).identity?.skill).toMatchObject({ + state: "stale", + installed_catalog_digest: legacyDigest, + }); + expect(await installCanonicalSkill(home)).toBe("installed"); + for (const path of [destination, reference]) { + const canonical = await readFile(path, "utf8"); + await writeFile(path, `${canonical}\nLocally changed instructions.\n`); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "stale", + ); + expect(await installCanonicalSkill(home)).toBe("installed"); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "aligned", + ); + } + await rm(reference); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "stale", + ); + expect(await installCanonicalSkill(home)).toBe("installed"); + expect((await runDoctor(undefined, host)).identity?.skill.state).toBe( + "aligned", + ); +});