diff --git a/packages/skills/src/asset-history.ts b/packages/skills/src/asset-history.ts index 4c41367ab..47f38021b 100644 --- a/packages/skills/src/asset-history.ts +++ b/packages/skills/src/asset-history.ts @@ -3,6 +3,11 @@ // resolves for a given `{ kind, id }`. Both `@corbits/skills`' skill assets // and `@corbits/agent-directory`'s workflow-kind definition assets read // their history through this one walk. +// +// Consumer history omits the hub's genesis commit — every `createAsset` → +// `initRepo` path writes an "Initialize repository" scaffolding commit +// authored as interchange-hub. That is substrate, not a person-saved +// version, so it never surfaces in the version list a saver reads back. import fs from "node:fs"; import git from "isomorphic-git"; @@ -10,6 +15,13 @@ import type { RepoId, RepoStore } from "@intx/hub-sessions"; import type { SkillCommit } from "./asset-store"; +/** The hub genesis commit message — substrate scaffolding, not a save. */ +export const ASSET_GENESIS_COMMIT_MESSAGE = "Initialize repository"; + +export function isAssetGenesisCommit(message: string): boolean { + return message.trim() === ASSET_GENESIS_COMMIT_MESSAGE; +} + export async function readAssetCommitHistory(input: { readonly repoStore: RepoStore; readonly kind: RepoId["kind"]; @@ -28,9 +40,11 @@ export async function readAssetCommitHistory(input: { } const commits: SkillCommit[] = []; for (const entry of entries) { + const message = entry.commit.message.trim(); + if (isAssetGenesisCommit(message)) continue; commits.push({ commitSha: entry.oid, - message: entry.commit.message.trim(), + message, author: entry.commit.author.name, committedAtIso: new Date( entry.commit.author.timestamp * 1000, diff --git a/packages/skills/src/hub-asset-store.ts b/packages/skills/src/hub-asset-store.ts index 83388cbcf..e410de9ba 100644 --- a/packages/skills/src/hub-asset-store.ts +++ b/packages/skills/src/hub-asset-store.ts @@ -158,12 +158,23 @@ export function createHubSkillAssetStore( }, async history(assetId) { - return readAssetCommitHistory({ + // RepoStore always commits as `interchange-hub` (fixed git identity). + // Only the skill's author may save, so attribute every consumer-visible + // version to that principal — never the machine account. + const commits = await readAssetCommitHistory({ repoStore, kind: SKILL_ASSET_KIND, assetId, ref: DEFAULT_ASSET_REF, }); + const row = await db.query.asset.findFirst({ + where: eq(assetTable.id, assetId), + }); + const author = row?.creatorPrincipalId; + if (author === undefined || author === null || author === "") { + return commits; + } + return commits.map((commit) => ({ ...commit, author })); }, }; } diff --git a/packages/skills/src/index.ts b/packages/skills/src/index.ts index d82c5d322..fcbe70fd6 100644 --- a/packages/skills/src/index.ts +++ b/packages/skills/src/index.ts @@ -8,7 +8,7 @@ export { type SkillCaller, } from "./access"; export { createDrizzleSkillAccessStore } from "./access-store"; -export { readAssetCommitHistory } from "./asset-history"; +export { readAssetCommitHistory, isAssetGenesisCommit } from "./asset-history"; export { skillMdPath, type SkillAssetRow, diff --git a/packages/skills/test/asset-history.test.ts b/packages/skills/test/asset-history.test.ts new file mode 100644 index 000000000..529601956 --- /dev/null +++ b/packages/skills/test/asset-history.test.ts @@ -0,0 +1,77 @@ +// Consumer history must hide the hub's genesis commit — a person who just +// created a skill should see their "Create …" version, never the substrate's +// "Initialize repository" scaffolding authored as interchange-hub. +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, describe, expect, test } from "bun:test"; +import git from "isomorphic-git"; + +import { readAssetCommitHistory } from "../src/asset-history"; +import type { RepoId, RepoStore } from "@intx/hub-sessions"; + +const dirs: string[] = []; + +afterEach(() => { + for (const dir of dirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +function tempDir(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skill-history-")); + dirs.push(dir); + return dir; +} + +function repoStoreFor(dir: string): RepoStore { + return { + getRepoDir: async (_id: RepoId) => dir, + } as unknown as RepoStore; +} + +describe("readAssetCommitHistory", () => { + test("hides the Initialize repository genesis from consumer history", async () => { + const dir = tempDir(); + await git.init({ fs, dir, defaultBranch: "main" }); + await fs.promises.writeFile(path.join(dir, ".gitignore"), "keys/\n"); + await git.add({ fs, dir, filepath: ".gitignore" }); + await git.commit({ + fs, + dir, + message: "Initialize repository", + author: { + name: "interchange-hub", + email: "hub@interchange.local", + }, + }); + await fs.promises.mkdir(path.join(dir, "triage"), { recursive: true }); + await fs.promises.writeFile( + path.join(dir, "triage", "SKILL.md"), + "---\nname: triage\ndescription: Sort issues.\n---\n\nBody.\n", + ); + await git.add({ fs, dir, filepath: "triage/SKILL.md" }); + await git.commit({ + fs, + dir, + message: "Create triage", + author: { + name: "interchange-hub", + email: "hub@interchange.local", + }, + }); + + const history = await readAssetCommitHistory({ + repoStore: repoStoreFor(dir), + kind: "skill", + assetId: "asset_1", + ref: "main", + }); + + expect(history.map((entry) => entry.message)).toEqual(["Create triage"]); + expect( + history.some((entry) => entry.message === "Initialize repository"), + ).toBe(false); + }); +}); diff --git a/packages/skills/test/fakes.ts b/packages/skills/test/fakes.ts index a1ab0c542..5d784e621 100644 Binary files a/packages/skills/test/fakes.ts and b/packages/skills/test/fakes.ts differ diff --git a/packages/skills/test/registry.test.ts b/packages/skills/test/registry.test.ts index 63b65bd9d..1c66a8cb1 100644 --- a/packages/skills/test/registry.test.ts +++ b/packages/skills/test/registry.test.ts @@ -239,6 +239,17 @@ describe("versions", () => { expect(versions[0]?.message).toBe("Create triage"); }); + test("after create, consumer history is one real version authored by the saver", async () => { + await publish("tenant"); + const versions = await registry.versions(AUTHOR, "triage"); + expect(versions).toHaveLength(1); + expect(versions[0]?.message).toBe("Create triage"); + expect(versions[0]?.author).toBe(AUTHOR.principalId); + expect( + versions.some((version) => version.message === "Initialize repository"), + ).toBe(false); + }); + test("restore re-commits an older version and marks it current", async () => { await publish("tenant"); const first = (await registry.versions(AUTHOR, "triage"))[0];