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
46 changes: 41 additions & 5 deletions src/commands/bench-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
* the instance — never here.
*
* `bench-kit update` upgrades the template zone-by-zone: `.bench-kit/` is
* replaced wholesale, workflows and skills are synced into the working
* tree as an uncommitted *proposal* (the company reviews `git diff` and
* decides), company content (`tasks/`, `evaluation-pool/`,
* `bench.config.yaml`) is never touched.
* replaced wholesale; workflows, skills and shared root files (AGENTS.md)
* are synced into the working tree as an uncommitted *proposal* (the
* company reviews `git diff` and decides); company content (`tasks/`,
* `evaluation-pool/`, `bench.config.yaml`) is never touched.
*/

import { spawn } from "node:child_process";
Expand Down Expand Up @@ -45,6 +45,13 @@ import { DEFAULT_TOOL, PROFILES } from "../lib/tool-profile";

export const TEMPLATE_REPO_URL = "https://github.com/przeprogramowani/10x-bench-kit";

/**
* Root-level template files that belong to the shared zone (like skills):
* update syncs them into the working tree as a reviewable proposal.
* `init` needs no special-casing — materialize copies the template root.
*/
export const SHARED_ROOT_FILES = ["AGENTS.md"];

/** The template's placeholder base-repo entry that init may replace. */
export const PLACEHOLDER_BASE_REPO = "demo-app";

Expand Down Expand Up @@ -400,13 +407,20 @@ export async function runBenchKitUpdate(
overwrite: true,
});

// Shared root files (AGENTS.md) — same proposal semantics as skills.
const shared: SyncCounts = { added: 0, updated: 0, unchanged: 0 };
for (const file of SHARED_ROOT_FILES) {
addSync(shared, syncFile(join(scratch, file), join(targetDir, file)));
}

output(
ctx,
[
`Updated the benchmark instance from template ${currentVersion} to ${newVersion}.`,
" .bench-kit/ replaced wholesale (runtime zone)",
` .github/workflows/ ${describeSync(workflows)}`,
` ${`${skillRootFor(toolId)}/`.padEnd(23)}${describeSync(skills)} — proposal, review before committing`,
` ${SHARED_ROOT_FILES.join(", ").padEnd(23)}${describeSync(shared)} — proposal, review before committing`,
" tasks/, evaluation-pool/, bench.config.yaml untouched (company zone)",
"Next: review 'git diff', run 'bench validate' (it flags any schema changes to fix), then commit via PR.",
].join("\n"),
Expand All @@ -419,7 +433,7 @@ export async function runBenchKitUpdate(
templateRef: updatedManifest.templateRef,
tool: toolId,
skillRoot: skillRootFor(toolId),
zones: { benchKit: "replaced", workflows, skills },
zones: { benchKit: "replaced", workflows, skills, shared },
},
);
} finally {
Expand Down Expand Up @@ -621,6 +635,28 @@ function describeSync(counts: SyncCounts): string {
return `${counts.added} added, ${counts.updated} updated, ${counts.unchanged} unchanged`;
}

/** Syncs a single file with the same add/overwrite semantics as syncDir. */
function syncFile(from: string, to: string): SyncCounts {
const counts: SyncCounts = { added: 0, updated: 0, unchanged: 0 };
if (!existsSync(from)) return counts;
if (!existsSync(to)) {
cpSync(from, to);
counts.added++;
} else if (readFileSync(from).equals(readFileSync(to))) {
counts.unchanged++;
} else {
cpSync(from, to);
counts.updated++;
}
return counts;
}

function addSync(into: SyncCounts, counts: SyncCounts): void {
into.added += counts.added;
into.updated += counts.updated;
into.unchanged += counts.unchanged;
}

/**
* Copies the clone into the target without git history. In repair mode
* existing files are never overwritten — company content is untouchable.
Expand Down
4 changes: 4 additions & 0 deletions tests/bench-kit-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function buildTemplateFixture(version = "0.1.0"): string {
);
mkdirSync(join(dir, "tasks", "demo"), { recursive: true });
writeFileSync(join(dir, "tasks", "demo", "prompt.md"), "demo prompt\n");
writeFileSync(join(dir, "AGENTS.md"), `# agents (${version})\n`);
writeFileSync(
join(dir, "bench.config.yaml"),
[
Expand Down Expand Up @@ -486,6 +487,8 @@ describe("10x bench-kit update", () => {
);
// Company zone untouched.
expect(readFileSync(join(target, "bench.config.yaml"), "utf8")).toContain("edited by company");
// Shared root files (AGENTS.md) synced like skills — a reviewable proposal.
expect(readFileSync(join(target, "AGENTS.md"), "utf8")).toBe("# agents (0.2.0)\n");

// Manifest survives the wholesale replacement, version-bumped.
const manifest = JSON.parse(readFileSync(join(target, ".bench-kit", "instance.json"), "utf8"));
Expand All @@ -500,6 +503,7 @@ describe("10x bench-kit update", () => {
expect(envelope.data.templateVersion).toBe("0.2.0");
expect(envelope.data.zones.benchKit).toBe("replaced");
expect(envelope.data.zones.skills.updated).toBe(1);
expect(envelope.data.zones.shared.updated).toBe(1);
});

it("is a no-op when the instance is already on the template version", async () => {
Expand Down