From 2fda6727be760849478ae5c89df080a213d32425 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 11:28:29 -0700 Subject: [PATCH 1/3] Add tests for skill consumer history genesis filter and saver author --- packages/skills/test/asset-history.test.ts | 77 +++++++++++++++++++++ packages/skills/test/fakes.ts | Bin 5698 -> 6478 bytes packages/skills/test/registry.test.ts | 11 +++ 3 files changed, 88 insertions(+) create mode 100644 packages/skills/test/asset-history.test.ts diff --git a/packages/skills/test/asset-history.test.ts b/packages/skills/test/asset-history.test.ts new file mode 100644 index 000000000..6d5201288 --- /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 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 a1ab0c5422d996c0cccef892ab2e8e57b03b399f..5d784e621915feefd4caebc4619c6472b40db5d0 100644 GIT binary patch delta 753 zcmZuv!EVz)5LEyn2p|U)R0;Jk_)=SH98vXvQk4iH0XbAfMBF%Jvz^4N+Pl`yrY1#I zZ+rm0@)4l?0Ur<{egKIpH?CY5$4<$Cz3kZg^k&|B^L1yj`}tSnk)o$W=w6^kQuNHE zsd8`*V{20Iqo_kxc0`G2$BLY>v-yw4@9V|)8)Ck9bJ4iybl^}~Yb>}JA)b!52__X8 z$cl{&SDL0N+7$_mRpQVpJpn~cSRQ7=cW9*|JYJHKND*=|0|6FA0z=7{!{xj~`1#=@ z4Aot&+-uCt5HjInCX0$c&8<5%CBS>GirCw_4fQmGSageRFWTL?#FX7rBoY(sg5PIG zB@%Up+%+?#O3{9aF1~mh_mt+jWi0dr+pM;F_~t!$tW6#{(;plST&XJv*BhL#f3S9> zE@;f*P-I1;b$`Bj>-n{!(`uJ?9_eIO=|53P!gh+Oi`n0loye_n2!X{3hhb-cdTgvj zV!WboAB5xPqp8DM9MhGtkT^wfhHK#<~Sh&{8DKIA*GpW7u6hP zc5XGq0a%mwqGb}szFJZBkD=ECKR5Pdgj&Xa@y1Jvg(v23Tc566Tv{DjJVi?wg#Uh} tA`1f4EGLA6CoGAps9K#hRJAZcP(PH&hyUB%8W7cihT(n_=Kcm;{R?Pq^fCYd delta 85 zcmV-b0IL7aGQupd>jASU0_Fw*0Fw?0VF3WMZwbx;lhg|pvkVPJ1d~n@WDPnXB6n|c rYhqQ3NCVGc4cyNWhg0=%M~57D;6UIy7L=8 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]; From 9631e740248e1b0181212cb5e036b4a7e039ca7d Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 11:28:36 -0700 Subject: [PATCH 2/3] Hide skill genesis commits and attribute versions to the saver --- packages/skills/src/asset-history.ts | 16 +++++++++++++++- packages/skills/src/hub-asset-store.ts | 13 ++++++++++++- packages/skills/src/index.ts | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) 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, From 0fd97d9a04f69691961c441377f7db650a499659 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sun, 23 Aug 2026 11:33:32 -0700 Subject: [PATCH 3/3] Fix RepoStore test stub cast for typecheck Double-cast the partial RepoStore stub via unknown so TS2352 no longer blocks @corbits/skills typecheck. --- packages/skills/test/asset-history.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/skills/test/asset-history.test.ts b/packages/skills/test/asset-history.test.ts index 6d5201288..529601956 100644 --- a/packages/skills/test/asset-history.test.ts +++ b/packages/skills/test/asset-history.test.ts @@ -28,7 +28,7 @@ function tempDir(): string { function repoStoreFor(dir: string): RepoStore { return { getRepoDir: async (_id: RepoId) => dir, - } as RepoStore; + } as unknown as RepoStore; } describe("readAssetCommitHistory", () => {