Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion packages/skills/src/asset-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@
// 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";

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"];
Expand All @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion packages/skills/src/hub-asset-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
},
};
}
2 changes: 1 addition & 1 deletion packages/skills/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
77 changes: 77 additions & 0 deletions packages/skills/test/asset-history.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Binary file modified packages/skills/test/fakes.ts
Binary file not shown.
11 changes: 11 additions & 0 deletions packages/skills/test/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Loading