feat: adopt template repository Codex surfaces

This commit is contained in:
MerlinH
2026-06-30 00:16:11 +10:00
parent 4960fb2dea
commit e89981f0f5
238 changed files with 8377 additions and 2169 deletions
-160
View File
@@ -1,160 +0,0 @@
import { existsSync, mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, test } from "node:test";
import { expect } from "expect";
import { activeAgentsForMode, canonicalProjectConfigJson, guidanceConfigHash, projectConfigSchema, slugify } from "../src/config.js";
import { renderProjectCustomAgentToml, renderProjectRolePrompt, validateBaseAgents } from "../src/agents.js";
import { loadEngineConfigs } from "../src/engines.js";
import { packageAssetPath } from "../src/paths.js";
import { defaultProjectConfig, initProject } from "../src/projects.js";
import { studioRoleIds } from "../src/roles.js";
import { formatTemplateShow, listTemplates, readTemplate, selectTemplates, validateTemplateFiles } from "../src/templates.js";
describe("config, agents, and templates", () => {
test("slug, active agents, and canonical hash are deterministic", () => {
expect(slugify("My Game")).toBe("my-game");
expect(activeAgentsForMode("prototype")).toContain("qa-playtester");
const config = projectConfigSchema.parse(JSON.parse(readTemplate("project_config")));
const hash = guidanceConfigHash(config);
config.project.status = "frozen";
expect(guidanceConfigHash(config)).toBe(hash);
config.project.genre = "Strategy";
expect(guidanceConfigHash(config)).not.toBe(hash);
expect(canonicalProjectConfigJson(config).endsWith("\n")).toBe(true);
});
test("all base prompts and templates have required sections", () => {
expect(validateBaseAgents()).toEqual([]);
expect(validateTemplateFiles()).toEqual([]);
expect(listTemplates().map((t) => t.id).sort()).toEqual([
"accessibility_requirements",
"adr",
"analytics_setup",
"architecture_traceability",
"art_bible",
"art_direction",
"difficulty_curve",
"economy_model",
"engine_setup",
"feature_spec",
"game_feel_tuning",
"gdd",
"handoff",
"market_analysis",
"pitch_document",
"player_journey",
"playtest_report",
"postmortem",
"production_milestone",
"project_config",
"release_notes",
"risk_register",
"ship_check",
"sound_bible",
"sprint_plan",
"technical_design",
"test_evidence",
"test_plan",
"ui_ux_review",
"ux_spec",
"vertical_slice_report"
]);
for (const template of listTemplates()) {
expect(template.description.length).toBeGreaterThan(20);
expect(template.roles.length).toBeGreaterThan(0);
expect(template.workflows.length).toBeGreaterThan(0);
}
});
test("template selection is bounded", () => {
expect(selectTemplates("producer", "Create market overview")).toEqual(["market_analysis"]);
expect(selectTemplates("producer", "Create analytics plan")).toEqual(["analytics_setup"]);
expect(selectTemplates("qa-playtester", "Review validation readiness")).toEqual(["playtest_report"]);
expect(selectTemplates("producer", "handoff coordination")).toEqual(["handoff"]);
expect(selectTemplates("qa-playtester", "Review UI usability")).toEqual(["playtest_report", "ui_ux_review"]);
expect(selectTemplates("release-manager", "Check package")).toEqual(["ship_check", "release_notes"]);
expect(selectTemplates("technical-director", "Draft architecture decision and technical design")).toEqual(["adr", "technical_design", "architecture_traceability"]);
expect(selectTemplates("audio-director", "Create sound bible for audio style")).toEqual(["sound_bible"]);
expect(selectTemplates("accessibility-specialist", "Define accessibility requirements")).toEqual(["accessibility_requirements"]);
expect(selectTemplates("producer", "Plan sprint risks and release notes")).toEqual(["sprint_plan", "release_notes", "ship_check", "risk_register"]);
});
test("template show includes discoverability metadata before body", () => {
const output = formatTemplateShow("gdd");
expect(output).toContain("ID: gdd");
expect(output).toContain("Category: design");
expect(output).toContain("Path: templates/gdd_template.md");
expect(output).toContain("Description:");
expect(output).toContain("Roles: game-designer, creative-director");
expect(output).toContain("Workflows:");
expect(output).toContain("# Purpose");
});
test("engine reference prompt selection is active-engine scoped", () => {
const engines = loadEngineConfigs(packageAssetPath("engine_configs"));
const unity = defaultProjectConfig({ name: "Unity Prompt Game", engine: "unity", mode: "prototype", nonInteractive: true });
const gameplay = renderProjectRolePrompt("gameplay-programmer", unity, engines);
expect(gameplay).toContain("docs/engine-reference/unity/VERSION.md");
expect(gameplay).toContain("docs/engine-reference/unity/current-best-practices.md");
expect(gameplay).toContain("docs/engine-reference/unity/gameplay.md");
expect(gameplay).not.toContain("docs/engine-reference/unity/modules/audio.md");
expect(gameplay).not.toContain("docs/engine-reference/unreal/");
expect(studioRoleIds).toContain("godot-specialist");
const godot = defaultProjectConfig({ name: "Godot Specialist Game", engine: "godot", mode: "prototype", nonInteractive: true });
const specialist = renderProjectRolePrompt("godot-specialist", godot, engines);
expect(specialist).toContain("docs/engine-reference/godot/VERSION.md");
expect(specialist).toContain("docs/engine-reference/godot/current-best-practices.md");
expect(specialist).toContain("docs/engine-reference/godot/specialist.md");
expect(specialist).not.toContain("docs/engine-reference/godot/modules/audio.md");
expect(specialist).not.toContain("docs/engine-reference/unity/");
});
test("generated project role prompts render structured contracts and stay bounded", () => {
const engines = loadEngineConfigs(packageAssetPath("engine_configs"));
const config = defaultProjectConfig({ name: "Prompt Depth Game", engine: "godot", mode: "development", nonInteractive: true });
const release = renderProjectRolePrompt("release-manager", config, engines);
const producerToml = renderProjectCustomAgentToml("producer", config, engines);
for (const section of [
"## Responsibilities",
"## Inputs To Inspect",
"## Output Format",
"## Quality Gates",
"## Collaboration Notes",
"## Stop Conditions"
]) {
expect(release).toContain(section);
expect(producerToml).toContain(section);
}
expect(release).toContain("Release decision");
expect(release).toContain("Blocking issues");
expect(release).toContain("Validation evidence");
expect(producerToml).toContain("Sprint Planning");
expect(producerToml).toContain("Risk Management");
expect(producerToml).toContain("Scope Management");
expect(producerToml).toContain("Validation gate");
expect(release).toContain("Stop and report a blocker");
expect(release.length).toBeLessThan(15000);
expect(producerToml.length).toBeLessThan(15000);
expect(release).not.toContain("# Gameplay Programmer");
expect(release).not.toContain("templates/gdd_template.md");
expect(release).not.toContain("docs/engine-reference/unity/");
});
test("generated project prompts and AGENTS.md include only the active engine specialist", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-specialist-"));
const { projectRoot } = initProject({ name: "Godot Prompt Scope", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const prompts = path.join(projectRoot, ".codex", "prompts");
const agents = readFileSync(path.join(projectRoot, "AGENTS.md"), "utf8");
expect(existsSync(path.join(prompts, "godot-specialist.md"))).toBe(true);
expect(existsSync(path.join(prompts, "unity-specialist.md"))).toBe(false);
expect(existsSync(path.join(prompts, "unreal-specialist.md"))).toBe(false);
expect(agents).toContain("- godot-specialist: .codex/prompts/godot-specialist.md");
expect(agents).not.toContain("unity-specialist.md");
expect(agents).not.toContain("unreal-specialist.md");
});
});
-77
View File
@@ -1,77 +0,0 @@
import { describe, test } from "node:test";
import { expect } from "expect";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { behavioralEvaluationScenarios, evaluateBehavioralScenario, runBehavioralEvaluations } from "../src/behavioral-evaluation.js";
import { initProject } from "../src/projects.js";
const scenarioIds = behavioralEvaluationScenarios.map((scenario) => scenario.id);
describe("behavioral evaluation", () => {
test("defines deterministic local representative scenarios", () => {
expect(scenarioIds).toEqual([
"role.gameplay-programmer.contract",
"role.qa-playtester.contract",
"role.release-manager.contract",
"workflow.ship-check.release-readiness",
"workflow.playtest.issue-evidence",
"workflow.market-analysis.positioning"
]);
for (const scenario of behavioralEvaluationScenarios) {
expect(scenario.requiredPhrases.length).toBeGreaterThan(0);
expect(scenario.forbiddenPhrases).toEqual(expect.arrayContaining(["CODEX.md", "telemetry", "hidden memory"]));
expect(scenario.expectedContextCategories.length).toBeGreaterThan(0);
}
});
test("built-in role contract scenarios pass without hosted evaluators", () => {
const results = runBehavioralEvaluations().filter((result) => result.id.startsWith("role."));
expect(results).toHaveLength(3);
for (const result of results) {
expect(result.status).toBe("pass");
expect(result.localDeterministic).toBe(true);
expect(result.usesHostedEvaluator).toBe(false);
expect(result.usesLlmCall).toBe(false);
expect(result.missingRequiredPhrases).toEqual([]);
expect(result.presentForbiddenPhrases).toEqual([]);
expect(result.promptLength).toBeGreaterThan(500);
}
});
test("workflow scenarios pass against generated project prompts and selected templates", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-behavioral-"));
const { projectRoot } = initProject({ name: "Behavioral Game", engine: "godot", mode: "development", nonInteractive: true }, cwd);
const results = runBehavioralEvaluations({ projectRoot }).filter((result) => result.id.startsWith("workflow."));
expect(results).toHaveLength(3);
for (const result of results) {
expect(result.status).toBe("pass");
expect(result.missingRequiredPhrases).toEqual([]);
expect(result.missingContextCategories).toEqual([]);
expect(result.missingTemplateIds).toEqual([]);
expect(result.presentForbiddenTemplateIds).toEqual([]);
expect(result.presentForbiddenPhrases).toEqual([]);
}
});
test("workflow scenarios include selected templates without a project root", () => {
const results = runBehavioralEvaluations().filter((result) => result.id.startsWith("workflow."));
expect(results).toHaveLength(3);
for (const result of results) {
expect(result.status).toBe("pass");
expect(result.missingTemplateIds).toEqual([]);
expect(result.prompt).toContain("## Workflow Templates");
}
});
test("negative checks fail when required text is missing or forbidden drift appears", () => {
const scenario = behavioralEvaluationScenarios[0];
const missing = evaluateBehavioralScenario({ ...scenario, requiredPhrases: ["this phrase is intentionally absent"] });
expect(missing.status).toBe("fail");
expect(missing.missingRequiredPhrases).toEqual(["this phrase is intentionally absent"]);
const forbidden = evaluateBehavioralScenario({ ...scenario, forbiddenPhrases: ["Role: Gameplay Programmer"] });
expect(forbidden.status).toBe("fail");
expect(forbidden.presentForbiddenPhrases).toEqual(["Role: Gameplay Programmer"]);
});
});
+3 -3
View File
@@ -5,7 +5,7 @@ import { describe, test } from "node:test";
import { expect } from "expect";
import { generateParityMatrix, inventoryCcgsSurfaces, renderParityMatrixMarkdown, validateParityMatrix, writeParityReports } from "../src/ccgs-parity.js";
import { defaultProjectConfig } from "../src/projects.js";
import { generatedSkillDefinitions } from "../src/skills.js";
import { templateSkillDefinitions } from "../src/skills.js";
function fixtureRoot(): string {
const root = mkdtempSync(path.join(tmpdir(), "ccgs-fixture-"));
@@ -57,8 +57,8 @@ describe("CCGS parity audit", () => {
expect(readFileSync(path.join(out, "ccgs-surface-parity-matrix.md"), "utf8")).toContain(`- Total rows: ${matrix.rows.length}`);
});
test("upgraded generated skills are no longer thin wrappers", () => {
const definitions = generatedSkillDefinitions(defaultProjectConfig({ name: "Skill Depth Game", engine: "godot", mode: "prototype", nonInteractive: true }));
test("upgraded template skills are no longer thin wrappers", () => {
const definitions = templateSkillDefinitions(defaultProjectConfig({ name: "Skill Depth Game", engine: "godot", mode: "prototype", nonInteractive: true }));
expect(new Set(definitions.map((skill) => skill.name)).size).toBe(definitions.length);
expect(definitions.map((skill) => skill.name)).toEqual(expect.arrayContaining(["cgs-brainstorm", "cgs-map-systems", "cgs-design-system", "cgs-vertical-slice", "cgs-bug-report", "cgs-qa-plan", "cgs-release-checklist", "cgs-localize", "cgs-team-ui"]));
expect(definitions.find((skill) => skill.name === "cgs-vertical-slice")?.body).toContain("PROCEED / PIVOT / KILL");
-354
View File
@@ -1,354 +0,0 @@
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, before, describe, test } from "node:test";
import { expect } from "expect";
const repoRoot = process.cwd();
const cli = path.join(repoRoot, "dist", "cli.js");
const tempRoots: string[] = [];
const hash64 = "a".repeat(64);
function runCli(args: string[], cwd: string): string {
return execFileSync("node", [cli, ...args], { cwd, encoding: "utf8" });
}
function runCliFailure(args: string[], cwd: string): string {
try {
runCli(args, cwd);
} catch (error) {
const result = error as { stdout?: Buffer | string; stderr?: Buffer | string };
return `${result.stdout?.toString() ?? ""}${result.stderr?.toString() ?? ""}`;
}
throw new Error(`Expected CLI failure for ${args.join(" ")}`);
}
function initCliProject(prefix: string, name: string): { cwd: string; projectRoot: string } {
const cwd = mkdtempSync(path.join(tmpdir(), prefix));
tempRoots.push(cwd);
runCli(["init", "--name", name, "--engine", "godot", "--mode", "prototype", "--non-interactive"], cwd);
return { cwd, projectRoot: cwd };
}
before(() => {
execFileSync("npm", ["run", "build", "--silent"], { cwd: repoRoot, encoding: "utf8" });
});
afterEach(() => {
for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true });
});
describe("built CLI prompt surface", () => {
test("prints inlined project prompt, selected templates, and bounded broad context from temp cwd", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-cli-prompt-"));
tempRoots.push(cwd);
const repoProject = path.join(repoRoot, ".codex", "studio.json");
const repoProjectExisted = existsSync(repoProject);
try {
execFileSync("node", [cli, "init", "--name", "CLI Prompt Game", "--engine", "godot", "--mode", "design", "--non-interactive"], { cwd, encoding: "utf8" });
const projectRoot = cwd;
const marketPrompt = execFileSync("node", [cli, "run", "market-analyst", "--project", projectRoot, "--print-prompt", "Assess competitors"], {
cwd,
encoding: "utf8"
});
expect(marketPrompt).toContain("# Project Role Prompt: .codex/prompts/market-analyst.md");
expect(marketPrompt).toContain("Project: CLI Prompt Game");
expect(marketPrompt).toContain("Template: market_analysis");
expect(marketPrompt).not.toContain("Template: analytics_setup");
const dryRun = execFileSync("node", [cli, "run", "producer", "--project", projectRoot, "--dry-run", "--allow-broad-context", "Plan milestone"], {
cwd,
encoding: "utf8"
});
expect(dryRun).toContain("- design/gdd.md");
expect(dryRun).toContain("- production/timeline.md");
expect(dryRun).toContain("- docs/market-overview.md");
expect((dryRun.match(/- \.codex\/prompts\//g) ?? [])).toHaveLength(1);
expect(existsSync(repoProject)).toBe(repoProjectExisted);
} finally {
if (!repoProjectExisted) rmSync(repoProject, { recursive: true, force: true });
}
});
test("approval grant lists and revokes scoped task approvals", () => {
const { cwd, projectRoot } = initCliProject("ogs-cli-approval-", "Approval Game");
writeFileSync(
path.join(projectRoot, ".codex", "tasks.json"),
`${JSON.stringify(
{
schemaVersion: 2,
tasks: [
{
id: "task-001",
title: "Implement jump feel",
role: "gameplay-programmer",
status: "ready",
files: ["design/gdd.md"],
writeFiles: ["source/player.gd"],
dependencies: [],
priority: 0,
notes: [],
createdAt: "2026-06-25T00:00:00.000Z",
updatedAt: "2026-06-25T00:00:00.000Z"
}
]
},
null,
2
)}\n`
);
const grant = runCli(
[
"approval",
"grant",
"--project",
projectRoot,
"--role",
"gameplay-programmer",
"--task",
"task-001",
"--scope",
"src/**/*.gd",
"--approved-by",
"lead",
"--expires-at",
"2099-01-01T00:00:00.000Z"
],
cwd
);
expect(grant).toContain("approval-001");
expect(grant).toContain("Implement jump feel");
expect(grant).toContain("src/**/*.gd");
const store = JSON.parse(readFileSync(path.join(projectRoot, ".codex", "approvals.json"), "utf8")) as {
records: Array<{ id: string; role: string; approvedFiles?: string[]; source: string }>;
};
expect(store.records).toHaveLength(1);
expect(store.records[0]).toMatchObject({
id: "approval-001",
role: "gameplay-programmer",
approvedFiles: ["source/player.gd"],
source: "approval-command"
});
const list = runCli(["approval", "list", "--project", projectRoot], cwd);
expect(list).toContain("approval-001");
expect(list).toContain("approved");
expect(list).toContain("authorizing");
const revoke = runCli(["approval", "revoke", "--project", projectRoot, "--approval-id", "approval-001"], cwd);
expect(revoke).toContain("Revoked approval-001");
const revokedList = runCli(["approval", "list", "--project", projectRoot], cwd);
expect(revokedList).toContain("approval-001");
expect(revokedList).toContain("revoked");
expect(revokedList).toContain("non-authorizing");
});
test("approval grant rejects empty broad and mismatched task scopes unless acknowledged", () => {
const { cwd, projectRoot } = initCliProject("ogs-cli-approval-", "Approval Scope Game");
expect(runCliFailure(["approval", "grant", "--project", projectRoot, "--role", "gameplay-programmer", "--task", hash64], cwd)).toMatch(
/scope/i
);
expect(
runCliFailure(
["approval", "grant", "--project", projectRoot, "--role", "gameplay-programmer", "--task", hash64, "--scope", "**/*"],
cwd
)
).toMatch(/broad.*allow-broad-scope/i);
expect(
runCliFailure(
["approval", "grant", "--project", projectRoot, "--role", "gameplay-programmer", "--task", hash64, "--scope", "**"],
cwd
)
).toMatch(/broad.*allow-broad-scope/i);
const broad = runCli(
[
"approval",
"grant",
"--project",
projectRoot,
"--role",
"gameplay-programmer",
"--task",
hash64,
"--scope",
"**/*",
"--allow-broad-scope"
],
cwd
);
expect(broad).toContain("approval-001");
expect(
runCliFailure(
[
"approval",
"grant",
"--project",
projectRoot,
"--role",
"not-a-role",
"--task",
hash64,
"--scope",
"src/**/*.gd"
],
cwd
)
).toMatch(/unknown studio role/i);
writeFileSync(
path.join(projectRoot, ".codex", "tasks.json"),
`${JSON.stringify(
{
schemaVersion: 1,
tasks: [
{
id: "task-001",
title: "Plan milestone",
role: "producer",
status: "ready",
files: ["production/timeline.md"],
notes: []
}
]
},
null,
2
)}\n`
);
expect(
runCliFailure(
[
"approval",
"grant",
"--project",
projectRoot,
"--role",
"gameplay-programmer",
"--task",
"task-001",
"--scope",
"src/**/*.gd"
],
cwd
)
).toMatch(/assigned to role producer.*does not match/i);
expect(
runCliFailure(
[
"approval",
"grant",
"--project",
projectRoot,
"--role",
"gameplay-programmer",
"--task",
"not-a-task",
"--scope",
"src/**/*.gd"
],
cwd
)
).toMatch(/unknown task/i);
});
test("approval list keeps expired approvals visible as non-authorizing", () => {
const { cwd, projectRoot } = initCliProject("ogs-cli-approval-", "Approval Expiry Game");
runCli(
[
"approval",
"grant",
"--project",
projectRoot,
"--role",
"gameplay-programmer",
"--task",
hash64,
"--scope",
"src/**/*.gd",
"--expires-at",
"2000-01-01T00:00:00.000Z"
],
cwd
);
const list = runCli(["approval", "list", "--project", projectRoot], cwd);
expect(list).toContain("approval-001");
expect(list).toContain("expired");
expect(list).toContain("non-authorizing");
});
test("approval grant accepts custom role ids for precomputed objective hashes", () => {
const { cwd, projectRoot } = initCliProject("ogs-cli-custom-approval-", "Custom Approval Game");
const grant = runCli(
[
"approval",
"grant",
"--project",
projectRoot,
"--role",
"custom-boss-designer",
"--task",
hash64,
"--scope",
"src/**/*.gd",
"--approved-by",
"lead"
],
cwd
);
expect(grant).toContain("approval-001");
expect(grant).toContain("role: custom-boss-designer");
const store = JSON.parse(readFileSync(path.join(projectRoot, ".codex", "approvals.json"), "utf8")) as {
records: Array<{ role: string; approvedGlobs: string[] }>;
};
expect(store.records[0]).toMatchObject({ role: "custom-boss-designer", approvedGlobs: ["src/**/*.gd"] });
});
test("run dry-run shows approval override advisory and sandbox provenance", () => {
const { cwd, projectRoot } = initCliProject("ogs-cli-policy-", "Policy Game");
const blocked = runCli(["run", "gameplay-programmer", "--project", projectRoot, "--dry-run", "Implement jump"], cwd);
expect(blocked).toContain("Eligibility: blocked");
expect(blocked).toContain("Write policy: read-only");
expect(blocked).toContain("Sandbox: read-only");
expect(blocked).toContain("Required approval:");
const override = runCli(["run", "gameplay-programmer", "--project", projectRoot, "--dry-run", "--approved-by-user", "Implement jump"], cwd);
expect(override).toContain("Eligibility: allowed");
expect(override).toContain("Write policy: override-write");
expect(override).toContain("Sandbox: danger-full-access");
expect(override).toContain("Provenance: override");
const constrained = runCli(["run", "gameplay-programmer", "--project", projectRoot, "--dry-run", "--approved-by-user", "--constrained-sandbox", "Implement jump"], cwd);
expect(constrained).toContain("Sandbox: workspace-write");
});
test("workflow recipes and task orchestrate are visible through the built CLI", () => {
const { cwd, projectRoot } = initCliProject("ogs-cli-orchestrate-", "Orchestrate Game");
const recipe = runCli(["workflow", "create-tasks", "vertical-slice", "--project", projectRoot, "--dry-run"], cwd);
expect(recipe).toContain("Workflow task recipe: vertical-slice");
const render = runCli(["workflow", "vertical-slice", "--project", projectRoot], cwd);
expect(render).toContain("# Codex Game Studio Session");
const created = runCli(["workflow", "create-tasks", "bugfix", "--project", projectRoot], cwd);
expect(created).toContain("Workflow task recipe: bugfix");
const orchestration = runCli(["task", "orchestrate", "--project", projectRoot, "--dry-run"], cwd);
expect(orchestration).toContain("Orchestration plan: 3 task(s), max concurrency 1");
});
});
+22 -10
View File
@@ -1,4 +1,4 @@
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { cpSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, test } from "node:test";
@@ -9,10 +9,22 @@ import { createTask, renderTaskRun } from "../src/tasks.js";
import { renderWorkflowPrompt } from "../src/workflows.js";
import { createContextManifest, selectContextEntries } from "../src/context-manifest.js";
function seedTemplateRoot(root: string): void {
for (const entry of ["AGENTS.md", ".codex/agents", ".codex/workflows", ".agents/skills"]) {
cpSync(path.join(process.cwd(), entry), path.join(root, entry), { recursive: true });
}
}
function initTemplateProject(options: Parameters<typeof initProject>[0], cwd: string): ReturnType<typeof initProject> {
seedTemplateRoot(cwd);
return initProject(options, cwd);
}
describe("Codex context files", () => {
test("runner, workflows, and task prompts use AGENTS.md", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-context-"));
const { projectRoot } = initProject({ name: "Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const prepared = prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement movement", dryRun: true }, cwd);
expect(prepared.contextFiles[0]).toBe("AGENTS.md");
@@ -32,7 +44,7 @@ describe("Codex context files", () => {
test("path-safe selector records required, missing, unsafe, and budgeted context", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-context-select-"));
const { projectRoot } = initProject({ name: "Selector Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Selector Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const outsideDir = mkdtempSync(path.join(tmpdir(), "ogs-outside-"));
const outside = path.join(outsideDir, "outside.md");
writeFileSync(outside, "outside");
@@ -64,7 +76,7 @@ describe("Codex context files", () => {
test("selector prioritizes required context over earlier optional context within file budget", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-context-required-budget-"));
const { projectRoot } = initProject({ name: "Required Budget Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Required Budget Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const result = selectContextEntries(projectRoot, [
{ sourcePath: "design/gdd.md", reason: "optional design reference" },
@@ -78,7 +90,7 @@ describe("Codex context files", () => {
test("selector omits oversized required context instead of bypassing character budgets", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-context-required-size-"));
const { projectRoot } = initProject({ name: "Required Size Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Required Size Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(path.join(projectRoot, "design", "huge-required.md"), "x".repeat(20_000));
const result = selectContextEntries(projectRoot, [
@@ -93,7 +105,7 @@ describe("Codex context files", () => {
test("selector rejects dotenv variants as secret-like paths", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-context-env-local-"));
const { projectRoot } = initProject({ name: "Env Local Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Env Local Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(path.join(projectRoot, ".env.local"), "TOKEN=secret\n");
const result = selectContextEntries(projectRoot, [
@@ -106,7 +118,7 @@ describe("Codex context files", () => {
test("implement workflow context contract does not mix read-only policy with writable permissions", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-workflow-contract-"));
const { projectRoot } = initProject({ name: "Workflow Contract Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Workflow Contract Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
const workflow = renderWorkflowPrompt(projectRoot, "bugfix");
@@ -119,7 +131,7 @@ describe("Codex context files", () => {
test("broad context selection records missing required files instead of widening reads", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-context-missing-"));
const { projectRoot } = initProject({ name: "Missing Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Missing Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
rmSync(path.join(projectRoot, "design", "gdd.md"));
mkdirSync(path.join(projectRoot, "notes"), { recursive: true });
writeFileSync(path.join(projectRoot, "notes", "unrequested.md"), "do not include");
@@ -136,7 +148,7 @@ describe("Codex context files", () => {
test("context manifest selects active engine references without unrelated engine docs", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-context-engine-reference-"));
const { projectRoot } = initProject({ name: "Unity Context Game", engine: "unity", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Unity Context Game", engine: "unity", mode: "prototype", nonInteractive: true }, cwd);
const manifest = createContextManifest(projectRoot, {
schemaVersion: 1,
@@ -171,7 +183,7 @@ describe("Codex context files", () => {
test("role runs and workflow prompts select task-relevant engine references without broad loading", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-context-engine-task-"));
const { projectRoot } = initProject({ name: "Godot Netcode Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Godot Netcode Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const run = prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement rollback networking input sync", printPrompt: true }, cwd);
expect(run.contextFiles).toContain("docs/engine-reference/godot/modules/networking.md");
-241
View File
@@ -1,241 +0,0 @@
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, test } from "node:test";
import { expect } from "expect";
import { appendApprovalRecord, canonicalObjectiveSha256 } from "../src/approvals.js";
import { customizationConfigPath, readProjectCustomization } from "../src/customization.js";
import { initProject, statusProject } from "../src/projects.js";
import { prepareRun } from "../src/runner.js";
import { formatTemplateShow, listTemplates } from "../src/templates.js";
import { validateProject } from "../src/validation.js";
import { renderWorkflowPrompt } from "../src/workflows.js";
function writeValidCustomPack(projectRoot: string, phase: "plan" | "implement" = "plan"): void {
mkdirSync(path.join(projectRoot, ".codex", "custom", "roles"), { recursive: true });
mkdirSync(path.join(projectRoot, ".codex", "workflows"), { recursive: true });
mkdirSync(path.join(projectRoot, "documentation", "templates"), { recursive: true });
writeFileSync(path.join(projectRoot, ".codex", "custom", "roles", "boss-designer.md"), "# Boss Designer Prompt\n\nDesign readable boss fights with phases, tells, counters, accessibility notes, and verification gates.\n");
writeFileSync(path.join(projectRoot, ".codex", "workflows", "custom-boss-review.md"), "# Boss Review Workflow\n\nUse phase readability, counterplay, accessibility, and test evidence to review the encounter.\n");
writeFileSync(path.join(projectRoot, "documentation", "templates", "boss-brief.md"), "# Purpose\n\nCapture a boss encounter brief.\n\n# Inputs\n\nProject fantasy and combat constraints.\n\n# Outputs\n\nPhases, tells, counters, risks, and tests.\n\n# Validation\n\nPlaytest the encounter for readability.\n");
writeFileSync(
customizationConfigPath(projectRoot),
`${JSON.stringify(
{
schemaVersion: 1,
policy: { merge: "extend-only" },
roles: [
{
id: "custom-boss-designer",
displayName: "Boss Designer",
promptFile: ".codex/custom/roles/boss-designer.md",
contextStrategy: "focused",
phase,
expectedOutputs: ["Boss encounter brief", "Readability risks", "Playtest checks"],
reviewChecklist: ["Boss phases are readable", "Counterplay is explicit", "Verification is concrete"]
}
],
templates: [
{
id: "custom-boss-brief",
category: "design",
path: "documentation/templates/boss-brief.md",
description: "Project-local boss encounter brief template.",
roles: ["custom-boss-designer"],
workflows: ["custom-boss-review"],
tags: ["boss", "encounter", "custom"],
requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"]
}
],
workflows: [
{
id: "custom-boss-review",
role: "custom-boss-designer",
phase: "plan",
objective: "Draft or review a boss encounter for readable phases, counterplay, accessibility, and test evidence.",
file: ".codex/workflows/custom-boss-review.md",
contextFiles: ["AGENTS.md", ".codex/studio.json", ".codex/custom/roles/boss-designer.md"],
templateIds: ["custom-boss-brief"],
aliases: ["boss-review"]
}
]
},
null,
2
)}\n`
);
}
describe("project-local customization packs", () => {
test("init seeds a reviewable customization config and status summarizes it", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-custom-init-"));
const { projectRoot } = initProject({ name: "Custom Seed", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
expect(existsSync(customizationConfigPath(projectRoot))).toBe(true);
expect(readProjectCustomization(projectRoot)).toMatchObject({ schemaVersion: 1, roles: [], workflows: [], templates: [], policy: { merge: "extend-only" } });
expect(statusProject(projectRoot, cwd)).toContain("custom roles: 0, workflows: 0, templates: 0");
expect(validateProject(projectRoot)).toContainEqual(expect.objectContaining({ id: "codex.customization.config", status: "pass" }));
});
test("valid local role workflow and template packs merge with built-ins without loading broad context", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-custom-valid-"));
const { projectRoot } = initProject({ name: "Boss Game", engine: "godot", mode: "development", nonInteractive: true }, cwd);
writeValidCustomPack(projectRoot);
const checks = validateProject(projectRoot);
expect(checks).toContainEqual(expect.objectContaining({ id: "codex.customization.role.custom-boss-designer", status: "pass" }));
expect(checks).toContainEqual(expect.objectContaining({ id: "codex.customization.workflow.custom-boss-review", status: "pass" }));
expect(checks).toContainEqual(expect.objectContaining({ id: "codex.customization.template.custom-boss-brief", status: "pass" }));
expect(checks.filter((check) => check.id.startsWith("codex.customization") && check.status === "fail")).toEqual([]);
expect(listTemplates(projectRoot).map((template) => template.id)).toContain("custom-boss-brief");
expect(formatTemplateShow("custom-boss-brief", { projectRoot })).toContain("Project-local boss encounter brief template");
const workflowPrompt = renderWorkflowPrompt(projectRoot, "custom-boss-review");
expect(workflowPrompt).toContain("Boss Designer");
expect(workflowPrompt).toContain("Draft or review a boss encounter");
expect(workflowPrompt).toContain("Boss Review Workflow");
expect(workflowPrompt).toContain("Use phase readability, counterplay, accessibility, and test evidence");
expect(workflowPrompt).toContain("Template: custom-boss-brief");
expect(workflowPrompt).not.toContain("Template: market_analysis");
const run = prepareRun("custom-boss-designer", { project: projectRoot, task: "Draft a boss fight brief with phase readability", printPrompt: true }, cwd);
expect(run.output).toContain("Role: Boss Designer");
expect(run.output).toContain("Role ID: custom-boss-designer");
expect(run.output).toContain("Boss Designer Prompt");
expect(run.output).toContain("Template: custom-boss-brief");
});
test("custom role runs render review and fix prompts when requested", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-custom-review-fix-"));
const { projectRoot } = initProject({ name: "Custom Review Fix", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
writeValidCustomPack(projectRoot, "implement");
const run = prepareRun("custom-boss-designer", { project: projectRoot, task: "Draft a boss fight brief with phase readability", dryRun: true, review: true, fix: true }, cwd);
expect(run.reviewPrompt).toContain("Read-only review: inspect diff and verification output; do not edit files.");
expect(run.reviewPrompt).toContain(".codex/prompts/qa-playtester.md");
expect(run.fixPrompt).toContain("# Project Custom Role Prompt: .codex/custom/roles/boss-designer.md");
expect(run.fixPrompt).toContain("Bounded Blockers:");
expect(run.reviewCodexCommand?.args).toEqual(expect.arrayContaining(["--sandbox", "read-only"]));
expect(run.output).toContain("Review Codex command:");
expect(run.output).toContain("Expected review JSON schema:");
expect(run.output).toContain("Fix prompt (max passes: 1):");
});
test("project-local workflows can render with built-in roles accepted by validation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-custom-built-in-workflow-"));
const { projectRoot } = initProject({ name: "Built In Workflow", engine: "godot", mode: "development", nonInteractive: true }, cwd);
mkdirSync(path.join(projectRoot, ".codex", "workflows"), { recursive: true });
writeFileSync(path.join(projectRoot, ".codex", "workflows", "custom-production-review.md"), "# Production Review\n\nReview scope, risks, owners, and validation gates.\n");
writeFileSync(
customizationConfigPath(projectRoot),
`${JSON.stringify(
{
schemaVersion: 1,
policy: { merge: "extend-only" },
roles: [],
templates: [],
workflows: [
{
id: "custom-production-review",
role: "producer",
phase: "plan",
objective: "Review production scope, risks, owners, and validation gates.",
file: ".codex/workflows/custom-production-review.md",
contextFiles: ["documentation/production/timeline.md"],
templateIds: [],
aliases: []
}
]
},
null,
2
)}\n`
);
expect(validateProject(projectRoot)).toContainEqual(expect.objectContaining({ id: "codex.customization.workflow.custom-production-review", status: "pass" }));
const workflowPrompt = renderWorkflowPrompt(projectRoot, "custom-production-review");
expect(workflowPrompt).toContain("Role: Producer");
expect(workflowPrompt).toContain("Role ID: producer");
expect(workflowPrompt).toContain("Production Review");
expect(workflowPrompt).toContain("Review production scope, risks, owners, and validation gates.");
});
test("custom implement roles can run under strict studio with a matching approval", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-custom-approval-"));
const { projectRoot } = initProject({ name: "Custom Approval", engine: "godot", mode: "development", studioMode: "strict-studio", nonInteractive: true }, cwd);
writeValidCustomPack(projectRoot, "implement");
const task = "Implement a boss encounter controller";
const approvedGlobs = ["source/**/*.gd"];
const objectiveSha256 = canonicalObjectiveSha256({
role: "custom-boss-designer",
objective: task,
approvedGlobs,
projectStage: "development",
studioMode: "strict-studio"
});
appendApprovalRecord(projectRoot, {
role: "custom-boss-designer",
objectiveSha256,
objective: task,
projectStage: "development",
studioMode: "strict-studio",
approvedGlobs,
approvedBy: "lead"
});
const run = prepareRun("custom-boss-designer", { project: projectRoot, task, dryRun: true, approvalScope: approvedGlobs }, cwd);
expect(run.output).toContain("Eligibility: allowed");
expect(run.output).toContain("Write policy: approved-write");
expect(run.output).toContain("Matching approval: true");
expect(run.output).toContain("Approval diagnostic:");
expect(run.output).toContain("approval-001: authorizing");
});
test("customization validation rejects a custom workflow with a missing file", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-custom-missing-workflow-"));
const { projectRoot } = initProject({ name: "Missing Workflow", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeValidCustomPack(projectRoot);
const workflowFile = path.join(projectRoot, ".codex", "workflows", "custom-boss-review.md");
expect(existsSync(workflowFile)).toBe(true);
unlinkSync(workflowFile);
const workflowCheck = validateProject(projectRoot).find((check) => check.id === "codex.customization.workflow.custom-boss-review");
expect(workflowCheck).toMatchObject({ status: "fail" });
expect(workflowCheck?.message).toContain("workflow file missing: .codex/workflows/custom-boss-review.md");
});
test("customization validation rejects unsafe paths and built-in id conflicts", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-custom-invalid-"));
const { projectRoot } = initProject({ name: "Invalid Custom", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(
customizationConfigPath(projectRoot),
`${JSON.stringify(
{
schemaVersion: 1,
policy: { merge: "extend-only" },
roles: [{ id: "producer", displayName: "Producer Override", promptFile: ".codex/custom/roles/producer.md", contextStrategy: "focused", phase: "plan", expectedOutputs: ["Override"], reviewChecklist: ["Override"] }],
templates: [{ id: "custom-escape-template", category: "design", path: "../escape.md", description: "Unsafe template path.", roles: ["producer"], workflows: ["vertical-slice"], tags: ["unsafe"], requiredSections: ["# Purpose"] }],
workflows: []
},
null,
2
)}\n`
);
const failures = validateProject(projectRoot).filter((check) => check.id.startsWith("codex.customization") && check.status === "fail");
expect(failures.map((failure) => failure.id)).toEqual(expect.arrayContaining(["codex.customization.role.producer", "codex.customization.template.custom-escape-template"]));
expect(failures.map((failure) => failure.message).join("\n")).toMatch(/built-in|escape|project root/i);
});
test("CLI templates and workflow commands can inspect project-local packs", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-custom-cli-"));
const { projectRoot } = initProject({ name: "CLI Custom", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeValidCustomPack(projectRoot);
const cli = path.join(process.cwd(), "src", "cli.ts");
const tsx = path.join(process.cwd(), "node_modules", ".bin", "tsx");
const templates = execFileSync(tsx, [cli, "templates", "list", "--project", projectRoot], { cwd, encoding: "utf8" });
expect(templates).toContain("custom-boss-brief");
const workflow = execFileSync(tsx, [cli, "workflow", "custom-boss-review", "--project", projectRoot, "--dry-run"], { cwd, encoding: "utf8" });
expect(workflow).toContain("Boss Designer");
expect(workflow).toContain("Template: custom-boss-brief");
});
});
+23 -32
View File
@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { cpSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, it } from "node:test";
@@ -11,6 +11,17 @@ import { readTemplate, templateRegistry } from "../src/templates.js";
import { validateProject } from "../src/validation.js";
import { renderWorkflowPrompt, workflowAliases, workflowRegistry } from "../src/workflows.js";
function seedTemplateRoot(root: string): void {
for (const entry of ["AGENTS.md", ".codex/agents", ".codex/workflows", ".agents/skills"]) {
cpSync(path.join(process.cwd(), entry), path.join(root, entry), { recursive: true });
}
}
function initTemplateProject(options: Parameters<typeof initProject>[0], cwd: string): ReturnType<typeof initProject> {
seedTemplateRoot(cwd);
return initProject(options, cwd);
}
const requiredRoles = [
"studio-orchestrator",
"producer",
@@ -164,9 +175,9 @@ describe("functionality gap pass", () => {
);
});
it("materializes project-specific prompts, activeRoles, and registry workflows", () => {
it("keeps template surfaces tracked while init records activeRoles and registry workflows", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-gap-"));
const { projectRoot } = initProject(
const { projectRoot } = initTemplateProject(
{ name: "Test Studio Game", engine: "godot", mode: "prototype", competitors: ["Mini Metro"], nonInteractive: true },
cwd
);
@@ -177,32 +188,16 @@ describe("functionality gap pass", () => {
expect(studio.workflows).toEqual(Object.keys(workflowRegistry));
expect(statusProject(projectRoot, cwd)).toContain(`active roles: ${activeAgentsForProject("prototype", "godot").join(", ")}`);
const promptBody = readFileSync(path.join(projectRoot, ".codex", "prompts", "market-analyst.md"), "utf8");
expect(promptBody).toContain("Project: Test Studio Game");
expect(promptBody).toContain("Role: Market Analyst");
expect(promptBody).toContain("Engine:");
expect(promptBody).toContain("Current Milestone:");
expect(promptBody).toContain("Expected Outputs");
expect(promptBody).toContain("Review Checklist");
expect(promptBody).toContain("Handoff");
expect(promptBody).toContain("Competitors: Mini Metro");
expect(promptBody).toContain("source-input-sha256");
expect(promptBody).toContain("rendered-body-sha256");
const networkPromptBody = readFileSync(path.join(projectRoot, ".codex", "prompts", "network-programmer.md"), "utf8");
expect(networkPromptBody).toContain("Role: Network Programmer");
expect(networkPromptBody).toContain("Context Strategy: broad");
expect(networkPromptBody).toContain("Expected Outputs");
expect(networkPromptBody).toContain("Review Checklist");
expect(existsSync(path.join(projectRoot, ".codex", "prompts"))).toBe(false);
expect(readFileSync(path.join(projectRoot, ".codex", "agents", "market-analyst.toml"), "utf8")).toContain('name = "market_analyst"');
for (const workflow of Object.keys(workflowRegistry)) {
expect(existsSync(path.join(projectRoot, workflowRegistry[workflow as keyof typeof workflowRegistry].file))).toBe(true);
expect(renderWorkflowPrompt(projectRoot, workflow as keyof typeof workflowRegistry)).toContain(workflow);
}
const workflowBody = readFileSync(path.join(projectRoot, ".codex", "workflows", "ui-ux-review.md"), "utf8");
expect(workflowBody).toContain("source-input-sha256");
expect(workflowBody).toContain("rendered-body-sha256");
expect(workflowBody).toContain("## Taxonomy");
expect(workflowBody).not.toContain("source-input-sha256");
expect(readFileSync(path.join(projectRoot, ".codex", "workflows", "release-checklist.md"), "utf8")).toContain("release-hotfix");
expect(readFileSync(path.join(projectRoot, ".codex", "workflows", "localization-plan.md"), "utf8")).toContain("localization-accessibility");
expect(existsSync(path.join(projectRoot, "project_orchestrator.md"))).toBe(false);
@@ -212,7 +207,7 @@ describe("functionality gap pass", () => {
it("inlines only selected package templates into workflow prompts", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-gap-"));
const { projectRoot } = initProject({ name: "Template Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Template Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
for (const workflow of Object.values(workflowRegistry)) {
for (const templateId of workflow.templateIds ?? []) {
@@ -266,7 +261,7 @@ describe("functionality gap pass", () => {
it("workflow shortcut CLI aliases render prompts only", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-gap-cli-"));
const { projectRoot } = initProject({ name: "Shortcut Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Shortcut Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const cli = path.join(process.cwd(), "src", "cli.ts");
const tsx = path.join(process.cwd(), "node_modules", ".bin", "tsx");
const help = execFileSync(tsx, [cli, "--help"], { encoding: "utf8" });
@@ -321,18 +316,14 @@ describe("functionality gap pass", () => {
}
});
it("validation reports stable IDs for missing expanded prompts and workflows", () => {
it("validation reports stable IDs for missing tracked agents and workflows", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-gap-val-"));
const { projectRoot } = initProject({ name: "Broken Gap Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
rmSync(path.join(projectRoot, ".codex", "prompts", "studio-orchestrator.md"));
const { projectRoot } = initTemplateProject({ name: "Broken Gap Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
rmSync(path.join(projectRoot, ".codex", "agents", "studio-orchestrator.toml"));
rmSync(path.join(projectRoot, ".codex", "workflows", "market-analysis.md"));
writeFileSync(path.join(projectRoot, ".codex", "workflows", "ui-ux-review.md"), "# UI UX Review\n");
const marketPrompt = path.join(projectRoot, ".codex", "prompts", "market-analyst.md");
writeFileSync(marketPrompt, readFileSync(marketPrompt, "utf8").replace("Project: Broken Gap Game", "Project: Wrong Game").replace("Role: Market Analyst", "Role: Wrong Role"));
const failures = validateProject(projectRoot).filter((check) => check.status === "fail").map((check) => check.id);
expect(failures).toContain("codex.role.studio-orchestrator.prompt.exists");
expect(failures).toContain("codex.role.market-analyst.prompt.project");
expect(failures).toContain("codex.role.market-analyst.prompt.role");
expect(failures).toContain("codex.agent.studio-orchestrator.exists");
expect(failures).toContain("codex.workflow.market-analysis.file.exists");
expect(failures).toContain("codex.workflow.ui-ux-review.sections");
});
+17 -9
View File
@@ -1,4 +1,4 @@
import { existsSync, readFileSync } from "node:fs";
import { cpSync, existsSync, readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
@@ -12,9 +12,17 @@ function tempRoot(prefix: string): string {
return mkdtempSync(path.join(tmpdir(), prefix));
}
function templateRoot(prefix: string): string {
const root = tempRoot(prefix);
for (const entry of ["AGENTS.md", ".codex/agents", ".codex/workflows", ".agents/skills"]) {
cpSync(path.join(process.cwd(), entry), path.join(root, entry), { recursive: true });
}
return root;
}
describe("project workflow", () => {
test("init configures the current repository root as the game root", () => {
const cwd = tempRoot("ogs-root-project-");
const cwd = templateRoot("ogs-root-project-");
const { projectRoot, config } = initProject({ name: "Test Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
expect(projectRoot).toBe(cwd);
expect(existsSync(path.join(cwd, "projects", "test-game"))).toBe(false);
@@ -43,14 +51,14 @@ describe("project workflow", () => {
);
const agents = readFileSync(path.join(projectRoot, "AGENTS.md"), "utf8");
expect(agents).toContain("# Test Game Game Studio");
expect(agents).toContain("# Codex Game Studio Template");
for (const section of ["## Project Goal", "## Engine", "## Commands", "## Coding Conventions", "## Asset Conventions", "## Studio Roles", "## Current Milestone", "## Verification", "## Rules"]) {
expect(agents).toContain(section);
}
expect(agents).not.toContain("CODEX.md");
expect(agents).not.toContain("NodeNext");
expect(agents).not.toContain("Truthmark");
expect(agents).toContain(guidanceConfigHash(config));
expect(agents).not.toContain(guidanceConfigHash(config));
expect(config.project.concept).toBe("Test Game concept");
expect(config.project.genre).toBe("Unspecified");
@@ -59,8 +67,8 @@ describe("project workflow", () => {
expect(config.project.competitors).toEqual([]);
});
test("init materializes Codex-native custom agents and repository skills", () => {
const cwd = tempRoot("ogs-surfaces-");
test("init preserves clone-visible Codex-native custom agents and repository skills", () => {
const cwd = templateRoot("ogs-surfaces-");
const { projectRoot } = initProject({ name: "Surface Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const agent = readFileSync(path.join(projectRoot, ".codex", "agents", "gameplay-programmer.toml"), "utf8");
expect(agent).toContain('name = "gameplay_programmer"');
@@ -68,8 +76,8 @@ describe("project workflow", () => {
expect(agent).toContain("developer_instructions = ");
expect(existsSync(path.join(projectRoot, ".codex", "agents", "gameplay-programmer.md"))).toBe(false);
expect(existsSync(path.join(projectRoot, ".codex", "agents", "godot-specialist.toml"))).toBe(true);
expect(existsSync(path.join(projectRoot, ".codex", "agents", "unity-specialist.toml"))).toBe(false);
expect(existsSync(path.join(projectRoot, ".codex", "agents", "unreal-specialist.toml"))).toBe(false);
expect(existsSync(path.join(projectRoot, ".codex", "agents", "unity-specialist.toml"))).toBe(true);
expect(existsSync(path.join(projectRoot, ".codex", "agents", "unreal-specialist.toml"))).toBe(true);
expect(existsSync(path.join(projectRoot, ".codex", "hooks.json"))).toBe(false);
expect(existsSync(path.join(projectRoot, ".codex", "rules"))).toBe(false);
@@ -124,7 +132,7 @@ describe("project workflow", () => {
});
test("status resume are read-only and freeze only changes operational status", () => {
const cwd = tempRoot("ogs-status-");
const cwd = templateRoot("ogs-status-");
const { projectRoot } = initProject({ name: "Freeze Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const studioPath = path.join(projectRoot, ".codex", "studio.json");
const before = readFileSync(studioPath, "utf8");
+45 -35
View File
@@ -1,4 +1,4 @@
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, test } from "node:test";
@@ -6,10 +6,21 @@ import { expect } from "expect";
import { initProject } from "../src/projects.js";
import { executeRunLifecycle, prepareRun } from "../src/runner.js";
function seedTemplateRoot(root: string): void {
for (const entry of ["AGENTS.md", ".codex/agents", ".codex/workflows", ".agents/skills"]) {
cpSync(path.join(process.cwd(), entry), path.join(root, entry), { recursive: true });
}
}
function initTemplateProject(options: Parameters<typeof initProject>[0], cwd: string): ReturnType<typeof initProject> {
seedTemplateRoot(cwd);
return initProject(options, cwd);
}
describe("runner", () => {
test("inspection modes do not write run cache files", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
const { projectRoot } = initProject({ name: "Inspect Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Inspect Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const runsDir = path.join(projectRoot, ".codex", "runs");
const before = readdirSync(runsDir);
@@ -25,7 +36,7 @@ describe("runner", () => {
test("strict studio blocks unapproved mutating role runs before run metadata writes", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-strict-"));
const { projectRoot } = initProject({ name: "Strict Game", engine: "godot", mode: "prototype", studioMode: "strict-studio", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Strict Game", engine: "godot", mode: "prototype", studioMode: "strict-studio", nonInteractive: true }, cwd);
const runsDir = path.join(projectRoot, ".codex", "runs");
const before = readdirSync(runsDir);
@@ -38,7 +49,7 @@ describe("runner", () => {
test("guided override and fast prototype record provenance metadata for allowed runs", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-policy-"));
const guided = initProject({ name: "Guided Game", engine: "godot", mode: "prototype", studioMode: "guided-studio", nonInteractive: true }, cwd);
const guided = initTemplateProject({ name: "Guided Game", engine: "godot", mode: "prototype", studioMode: "guided-studio", nonInteractive: true }, cwd);
const guidedDryRun = prepareRun("gameplay-programmer", { project: guided.projectRoot, task: "Implement movement", dryRun: true, approvedByUser: true }, cwd);
expect(guidedDryRun.output).toContain("Write policy: override-write");
expect(guidedDryRun.output).toContain("Sandbox: danger-full-access");
@@ -53,7 +64,7 @@ describe("runner", () => {
metadata: { provenance: "override", approvedByUser: true }
});
const fast = initProject({ name: "Fast Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-runner-policy-")));
const fast = initTemplateProject({ name: "Fast Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-runner-policy-")));
const fastRun = prepareRun("gameplay-programmer", { project: fast.projectRoot, task: "Implement movement", codexBin: path.join(cwd, "missing-codex") }, cwd);
const fastMetadata = JSON.parse(readFileSync(fastRun.metadataPath, "utf8")) as { eligibility: { writePolicy: string; metadata: { provenance: string } } };
expect(fastRun.output).toContain("Write policy: advisory-write");
@@ -62,7 +73,7 @@ describe("runner", () => {
test("mutating runs default to full access and require explicit constrained sandbox for workspace-write", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-sandbox-"));
const { projectRoot } = initProject({ name: "Sandbox Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Sandbox Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
const defaultRun = prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement movement", dryRun: true }, cwd);
expect(defaultRun.codexCommand.args).toEqual(expect.arrayContaining(["--sandbox", "danger-full-access"]));
@@ -73,54 +84,53 @@ describe("runner", () => {
expect(constrainedRun.codexCommand.args).toEqual(expect.arrayContaining(["--sandbox", "workspace-write"]));
});
test("inlines generated project role prompt", () => {
test("inlines runtime role context without generating prompt files", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
const { projectRoot } = initProject({ name: "Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
const promptPath = path.join(projectRoot, ".codex", "prompts", "market-analyst.md");
writeFileSync(promptPath, `${readFileSync(promptPath, "utf8")}\nUNIQUE_PROJECT_PROMPT_SENTINEL\n`);
const { projectRoot } = initTemplateProject({ name: "Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
const run = prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", printPrompt: true }, cwd);
expect(run.prompt).toContain("# Project Role Prompt: .codex/prompts/market-analyst.md");
expect(run.prompt).toContain("UNIQUE_PROJECT_PROMPT_SENTINEL");
expect(run.contextFiles).toContain(".codex/prompts/market-analyst.md");
expect(run.prompt).toContain("# Runtime Role Context: market-analyst");
expect(run.prompt).toContain("Project: Prompt Game");
expect(run.contextFiles).toContain(".codex/agents/market-analyst.toml");
expect(existsSync(path.join(projectRoot, ".codex", "prompts"))).toBe(false);
});
test("missing generated project role prompt fails clearly", () => {
test("missing tracked custom agent is reported as context blocker", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
const { projectRoot } = initProject({ name: "Missing Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
rmSync(path.join(projectRoot, ".codex", "prompts", "market-analyst.md"));
const { projectRoot } = initTemplateProject({ name: "Missing Agent Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
rmSync(path.join(projectRoot, ".codex", "agents", "market-analyst.toml"));
expect(() => prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", printPrompt: true }, cwd)).toThrow(
"Missing generated project role prompt: .codex/prompts/market-analyst.md"
);
const run = prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", printPrompt: true }, cwd);
expect(run.prompt).toContain(".codex/agents/market-analyst.toml: missing");
expect(run.contextFiles).not.toContain(".codex/agents/market-analyst.toml");
});
test("wrong-engine specialist run fails clearly before producing a prompt", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-specialist-"));
const { projectRoot } = initProject({ name: "Godot Specialist Run", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Godot Specialist Run", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
expect(() => prepareRun("unity-specialist", { project: projectRoot, task: "Review engine setup", printPrompt: true }, cwd)).toThrow(
/unity-specialist is not available for godot projects/i
);
const run = prepareRun("godot-specialist", { project: projectRoot, task: "Review engine setup", printPrompt: true }, cwd);
expect(run.prompt).toContain("# Project Role Prompt: .codex/prompts/godot-specialist.md");
expect(run.prompt).toContain("# Runtime Role Context: godot-specialist");
expect(run.prompt).toContain("docs/engine-reference/godot/specialist.md");
});
test("fix prompt includes project role prompt", () => {
test("fix prompt includes runtime role context", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
const { projectRoot } = initProject({ name: "Fix Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Fix Prompt Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
const run = prepareRun("market-analyst", { project: projectRoot, task: "Assess competitors", dryRun: true, fix: true }, cwd);
expect(run.fixPrompt).toContain("# Project Role Prompt: .codex/prompts/market-analyst.md");
expect(run.fixPrompt).toContain("# Runtime Role Context: market-analyst");
});
test("role runs inline selected templates", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-template-"));
const { projectRoot } = initProject({ name: "Template Run Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Template Run Game", engine: "godot", mode: "design", nonInteractive: true }, cwd);
const market = prepareRun("market-analyst", { project: projectRoot, task: "Assess positioning", printPrompt: true }, cwd);
expect(market.prompt).toContain("## Selected Templates");
@@ -148,7 +158,7 @@ describe("runner", () => {
test("broad context discovers bounded existing project files", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
const { projectRoot } = initProject({ name: "Broad Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Broad Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const narrow = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", dryRun: true }, cwd);
expect(narrow.contextFiles).not.toContain("design/gdd.md");
@@ -159,12 +169,12 @@ describe("runner", () => {
expect(broad.contextFiles).toContain("production/timeline.md");
expect(broad.contextFiles).toContain("docs/market-overview.md");
expect(broad.contextFiles).not.toContain("Broad context explicitly allowed by CLI flag.");
expect(broad.contextFiles.filter((file) => file.startsWith(".codex/prompts/"))).toHaveLength(1);
expect(broad.contextFiles.filter((file) => file.startsWith(".codex/agents/"))).toHaveLength(1);
});
test("broad context ignores directories and realpath escapes", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
const { projectRoot } = initProject({ name: "Bounded Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Bounded Context Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const gdd = path.join(projectRoot, "design", "gdd.md");
rmSync(gdd);
mkdirSync(gdd);
@@ -183,7 +193,7 @@ describe("runner", () => {
test("include artifact renders canonical project-relative paths", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-"));
const { projectRoot } = initProject({ name: "Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const run = prepareRun("producer", { project: projectRoot, task: "Plan next milestone", printPrompt: true, includeArtifact: ["design/../design/gdd.md"] }, cwd);
@@ -196,7 +206,7 @@ describe("runner", () => {
test("rejected included artifacts are not embedded in prompts", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-artifact-rejected-"));
const { projectRoot } = initProject({ name: "Rejected Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Rejected Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
mkdirSync(path.join(projectRoot, "dist"), { recursive: true });
writeFileSync(path.join(projectRoot, "dist", "bundle.js"), "REJECTED_BUNDLE_BODY");
@@ -210,7 +220,7 @@ describe("runner", () => {
test("oversized included artifacts are not embedded in prompts", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-runner-artifact-oversized-"));
const { projectRoot } = initProject({ name: "Oversized Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Oversized Artifact Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const artifactPath = path.join(projectRoot, "design", "large-artifact.md");
writeFileSync(artifactPath, `OVERSIZED_ARTIFACT_BODY\n${"x".repeat(20_000)}`);
@@ -224,7 +234,7 @@ describe("runner", () => {
test("review passes execute Codex with a read-only sandbox", async () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-review-"));
const { projectRoot } = initProject({ name: "Review Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Review Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
const log = path.join(cwd, "codex-invocations.jsonl");
const stub = path.join(cwd, "codex-stub.mjs");
writeFileSync(
@@ -248,13 +258,13 @@ console.log(JSON.stringify({ blockers: [], warnings: [], summary: "ok", needsFix
expect(invocations).toHaveLength(2);
expect(invocations[0].args).toEqual(expect.arrayContaining(["--sandbox", "danger-full-access"]));
expect(invocations[1].args).toEqual(expect.arrayContaining(["--sandbox", "read-only"]));
expect(invocations[1].input).toContain("# Project Role Prompt: .codex/prompts/qa-playtester.md");
expect(invocations[1].input).toContain("# Runtime Role Context: qa-playtester");
expect(invocations[1].input).toContain("Template: playtest_report");
});
test("implementation, review, and fix prompts share a bounded Context Contract", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-contract-"));
const { projectRoot } = initProject({ name: "Contract Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Contract Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd);
const run = prepareRun("gameplay-programmer", { project: projectRoot, task: "Implement movement", dryRun: true, review: true, fix: true }, cwd);
@@ -271,7 +281,7 @@ console.log(JSON.stringify({ blockers: [], warnings: [], summary: "ok", needsFix
expect(prompt).toContain("Omissions and Blockers:");
}
expect(run.reviewPrompt).toContain("Read-only review: inspect diff and verification output; do not edit files.");
expect(run.reviewPrompt).toContain(".codex/prompts/qa-playtester.md");
expect(run.reviewPrompt).toContain(".codex/agents/qa-playtester.toml");
expect(run.fixPrompt).toContain("Bounded Blockers:");
expect(run.fixPrompt).toContain("- Review and verification blockers will be supplied at execution time.");
});
@@ -0,0 +1,82 @@
import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, test } from "node:test";
import { expect } from "expect";
import { initProject } from "../src/projects.js";
import { validateTemplateSurfaces } from "../src/validation.js";
const repoRoot = process.cwd();
function trackedFiles(pattern: string): string[] {
return execFileSync("git", ["ls-files", pattern], { cwd: repoRoot, encoding: "utf8" })
.split("\n")
.filter(Boolean)
.sort();
}
function filesUnder(relativeDir: string): string[] {
const full = path.join(repoRoot, relativeDir);
if (!existsSync(full)) return [];
return readdirSync(full, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => path.join(entry.parentPath, entry.name).slice(repoRoot.length + 1).split(path.sep).join("/"))
.sort();
}
describe("template repository Codex surfaces", () => {
test("clone-visible game agents, workflows, and skills are tracked before init", () => {
const agents = trackedFiles(".codex/agents/*.toml");
const workflows = trackedFiles(".codex/workflows/*.md");
const skills = trackedFiles(".agents/skills/*/SKILL.md");
expect(agents).toEqual(expect.arrayContaining([".codex/agents/producer.toml", ".codex/agents/gameplay-programmer.toml", ".codex/agents/godot-specialist.toml"]));
expect(workflows).toEqual(expect.arrayContaining([".codex/workflows/vertical-slice.md", ".codex/workflows/bugfix.md", ".codex/workflows/qa-plan.md"]));
expect(skills).toEqual(expect.arrayContaining([".agents/skills/cgs-start/SKILL.md", ".agents/skills/cgs-bugfix/SKILL.md", ".agents/skills/cgs-vertical-slice/SKILL.md"]));
for (const file of [...agents, ...workflows, ...skills]) {
const body = readFileSync(path.join(repoRoot, file), "utf8");
expect(body.trim().length).toBeGreaterThan(80);
expect(body).not.toContain("@generated");
expect(body).not.toContain("generatedSurface");
}
});
test("game-facing root surface folders do not contain Truthmark maintenance surfaces", () => {
expect(filesUnder(".codex/agents").some((file) => /truth/i.test(file))).toBe(false);
expect(filesUnder(".agents/skills").some((file) => /truthmark/i.test(file))).toBe(false);
});
test("init preserves tracked template instruction bodies", () => {
const root = mkdtempSync(path.join(tmpdir(), "ogs-template-preserve-"));
const agentPath = path.join(root, ".codex", "agents", "producer.toml");
const workflowPath = path.join(root, ".codex", "workflows", "bugfix.md");
const skillPath = path.join(root, ".agents", "skills", "cgs-start", "SKILL.md");
const agentsPath = path.join(root, "AGENTS.md");
mkdirSync(path.dirname(agentPath), { recursive: true });
mkdirSync(path.dirname(workflowPath), { recursive: true });
mkdirSync(path.dirname(skillPath), { recursive: true });
writeFileSync(agentPath, 'name = "producer"\ndescription = "Sentinel producer"\ndeveloper_instructions = """\nDo not overwrite this tracked template agent.\n"""\n');
writeFileSync(workflowPath, "# Bugfix Workflow\n\n## Purpose\n\nDo not overwrite this tracked template workflow.\n");
writeFileSync(skillPath, "---\nname: cgs-start\ndescription: Sentinel start skill\n---\n\n# Start\n\nDo not overwrite this tracked template skill.\n");
writeFileSync(agentsPath, "# Sentinel Game Studio\n\n## Project Goal\n\nDo not overwrite this tracked root instruction file.\n");
const before = new Map([
[agentPath, readFileSync(agentPath, "utf8")],
[workflowPath, readFileSync(workflowPath, "utf8")],
[skillPath, readFileSync(skillPath, "utf8")],
[agentsPath, readFileSync(agentsPath, "utf8")]
]);
initProject({ name: "Template Preserve", engine: "godot", mode: "prototype", nonInteractive: true }, root);
for (const [file, body] of before) expect(readFileSync(file, "utf8")).toBe(body);
});
test("template-surface validation runs before project state exists", () => {
const checks = validateTemplateSurfaces(repoRoot);
expect(checks.filter((check) => check.status === "fail")).toEqual([]);
expect(checks).toEqual(expect.arrayContaining([expect.objectContaining({ id: "template.agents.tracked", status: "pass" }), expect.objectContaining({ id: "template.workflows.tracked", status: "pass" }), expect.objectContaining({ id: "template.skills.tracked", status: "pass" })]));
});
});
+4 -1
View File
@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, test } from "node:test";
@@ -23,6 +23,9 @@ describe("template root smoke", () => {
execFileSync("npm", ["run", "build", "--silent"], { cwd: repoRoot, encoding: "utf8" });
const root = mkdtempSync(path.join(tmpdir(), "ogs-template-root-"));
try {
for (const entry of ["AGENTS.md", ".codex/agents", ".codex/workflows", ".agents/skills"]) {
cpSync(path.join(repoRoot, entry), path.join(root, entry), { recursive: true });
}
execFileSync("node", [cli, "init", "--name", "Clone Root Game", "--engine", "godot", "--mode", "prototype", "--non-interactive"], { cwd: root, encoding: "utf8" });
execFileSync("node", [cli, "validate"], { cwd: root, encoding: "utf8" });
+61 -99
View File
@@ -1,4 +1,4 @@
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -9,12 +9,24 @@ import { packageAssetPath } from "../src/paths.js";
import { freezeProject, initProject } from "../src/projects.js";
import { workflowSourceInput } from "../src/projects.js";
import { runValidation, validateProject } from "../src/validation.js";
import { hashGeneratedBody, stableHash, stripGeneratedMetadata } from "../src/generated-surfaces.js";
import { stableHash } from "../src/generated-surfaces.js";
import { loadEngineConfigs } from "../src/engines.js";
import { engineReferenceRegistry } from "../src/engine-reference.js";
import { rolePackages } from "../src/roles.js";
import { behavioralEvaluationScenarios } from "../src/behavioral-evaluation.js";
function seedTemplateRoot(root: string): void {
for (const entry of ["AGENTS.md", ".codex/agents", ".codex/workflows", ".agents/skills"]) {
cpSync(path.join(process.cwd(), entry), path.join(root, entry), { recursive: true });
}
}
function initTemplateProject(options: Parameters<typeof initProject>[0], cwd = mkdtempSync(path.join(tmpdir(), "ogs-template-project-"))): ReturnType<typeof initProject> {
seedTemplateRoot(cwd);
return initProject(options, cwd);
}
const validApprovalRecord = {
id: "appr_test",
stage: "approved",
@@ -48,14 +60,14 @@ describe("validation", () => {
["Unreal Val", "ue5", "development"]
] as const) {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name, engine, mode, nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name, engine, mode, nonInteractive: true }, cwd);
expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toEqual([]);
}
});
test("missing required project artifacts fail", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Broken Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Broken Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
rmSync(path.join(projectRoot, "AGENTS.md"));
rmSync(path.join(projectRoot, "src", "project.godot"));
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
@@ -63,19 +75,19 @@ describe("validation", () => {
expect(failures.map((f) => f.id)).toContain("project.engine_file");
});
test("malformed AGENTS contract and prompt files fail validation", () => {
test("malformed AGENTS contract and custom agent files fail validation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Prompt Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Prompt Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(path.join(projectRoot, "AGENTS.md"), "# Broken\n");
writeFileSync(path.join(projectRoot, ".codex", "prompts", "producer.md"), "");
writeFileSync(path.join(projectRoot, ".codex", "agents", "producer.toml"), "");
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
expect(failures.map((f) => f.id)).toContain("codex.project.AGENTS.md.## Project Goal");
expect(failures.map((f) => f.id)).toContain("codex.prompt.producer");
expect(failures.map((f) => f.id)).toContain("codex.agent.producer.name");
});
test("empty timeline sections fail validation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Timeline Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Timeline Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(
path.join(projectRoot, "production", "timeline.md"),
"# Timeline\n\nTBD\n\n# Milestones\n\n# Risks\n\n- Scope risk.\n\n# Next Validation Gate\n\nRun validation.\n"
@@ -86,7 +98,7 @@ describe("validation", () => {
test("Unity validation fails when ProjectSettings marker is missing", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Broken Unity", engine: "unity", mode: "design", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Broken Unity", engine: "unity", mode: "design", nonInteractive: true }, cwd);
rmSync(path.join(projectRoot, "src", "ProjectSettings", "ProjectSettings.asset"));
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
expect(failures.map((f) => f.id)).toContain("project.engine_settings");
@@ -94,14 +106,14 @@ describe("validation", () => {
test("invalid studio json fails", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Stale Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Stale Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(path.join(projectRoot, ".codex", "studio.json"), "{ invalid json");
expect(validateProject(projectRoot)[0].status).toBe("fail");
});
test("invalid studio mode fails project validation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Studio Mode Val", engine: "godot", mode: "prototype", studioMode: "strict-studio", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Studio Mode Val", engine: "godot", mode: "prototype", studioMode: "strict-studio", nonInteractive: true }, cwd);
const studioPath = path.join(projectRoot, ".codex", "studio.json");
const studio = JSON.parse(readFileSync(studioPath, "utf8"));
studio.studioMode = "ceremony-platform";
@@ -117,7 +129,7 @@ describe("validation", () => {
test("malformed approval store fails project validation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Approval Val", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Approval Val", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(
path.join(projectRoot, ".codex", "approvals.json"),
`${JSON.stringify({ schemaVersion: 1, product: "codex-game-studio", records: [{ id: "bad", stage: "approved", approvedGlobs: ["../escape.ts"] }] }, null, 2)}\n`
@@ -133,14 +145,14 @@ describe("validation", () => {
test("context manifest schema and stale freshness metadata fail project validation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Manifest Val", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Manifest Val", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(path.join(projectRoot, ".codex", "context-manifest.json"), "{ invalid json");
expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toContainEqual(
expect.objectContaining({ id: "codex.project.context_manifest", message: expect.stringMatching(/invalid JSON/i) })
);
const { projectRoot: staleProject } = initProject({ name: "Stale Manifest Val", engine: "godot", mode: "prototype", studioMode: "strict-studio", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const { projectRoot: staleProject } = initTemplateProject({ name: "Stale Manifest Val", engine: "godot", mode: "prototype", studioMode: "strict-studio", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const metaPath = path.join(staleProject, ".codex", "context-manifest.meta.json");
const meta = JSON.parse(readFileSync(metaPath, "utf8"));
meta.studioMode = "guided-studio";
@@ -153,7 +165,7 @@ describe("validation", () => {
test("approval store symlink escape fails project validation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Approval Symlink Val", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Approval Symlink Val", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const outside = mkdtempSync(path.join(tmpdir(), "ogs-outside-"));
mkdirSync(path.join(projectRoot, "source"), { recursive: true });
symlinkSync(outside, path.join(projectRoot, "source", "outside-link"));
@@ -173,14 +185,14 @@ describe("validation", () => {
test("freeze status-only changes keep project validation green", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Freeze Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Freeze Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
freezeProject(projectRoot, cwd);
expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toEqual([]);
});
test("engine reference validation covers package metadata and project materialization", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-engine-reference-"));
const { projectRoot } = initProject({ name: "Unity Reference Valid", engine: "unity", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Unity Reference Valid", engine: "unity", mode: "prototype", nonInteractive: true }, cwd);
for (const file of [...engineReferenceRegistry.unity.requiredFiles, ...engineReferenceRegistry.unity.moduleFiles, ...engineReferenceRegistry.unity.pluginFiles]) {
expect(existsSync(path.join(projectRoot, engineReferenceRegistry.unity.projectPath(file)))).toBe(true);
@@ -197,7 +209,7 @@ describe("validation", () => {
test("missing materialized engine reference fails project validation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-engine-reference-"));
const { projectRoot } = initProject({ name: "Broken Reference Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Broken Reference Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
rmSync(path.join(projectRoot, "docs", "engine-reference", "godot", "plugins.md"));
expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toContainEqual(
@@ -205,99 +217,49 @@ describe("validation", () => {
);
});
test("wrong-engine specialist prompts are absent and active specialist prompt is validated", () => {
test("tracked template custom agents are validated across engine specialists", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-specialist-"));
const { projectRoot } = initProject({ name: "Godot Specialist Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Godot Specialist Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toEqual([]);
rmSync(path.join(projectRoot, ".codex", "prompts", "godot-specialist.md"));
rmSync(path.join(projectRoot, ".codex", "agents", "godot-specialist.toml"));
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
expect(failures.map((f) => f.id)).toContain("codex.role.godot-specialist.prompt.exists");
expect(failures.map((f) => f.id)).not.toContain("codex.role.unity-specialist.prompt.exists");
expect(failures.map((f) => f.id)).toContain("codex.agent.godot-specialist.exists");
expect(failures.map((f) => f.id)).not.toContain("codex.agent.unity-specialist.absent");
});
test("validation fails when wrong-engine specialist prompts are materialized", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-specialist-"));
const { projectRoot } = initProject({ name: "Wrong Specialist Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
writeFileSync(path.join(projectRoot, ".codex", "prompts", "unity-specialist.md"), "# Wrong specialist\n");
test("template surface validation detects malformed tracked agents and workflows", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-template-shape-"));
const { projectRoot } = initTemplateProject({ name: "Template Shape Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const agent = path.join(projectRoot, ".codex", "agents", "producer.toml");
writeFileSync(agent, readFileSync(agent, "utf8").replace(/developer_instructions\s*=\s*"""[\s\S]*?"""/, "developer_instructions_missing = true"));
expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toContainEqual(
expect.objectContaining({ id: "codex.role.unity-specialist.prompt.absent" })
expect.objectContaining({ id: "codex.agent.producer.developer_instructions" })
);
const workflowProject = initTemplateProject({ name: "Template Workflow Shape Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-template-shape-"))).projectRoot;
const workflow = path.join(workflowProject, ".codex", "workflows", "ui-ux-review.md");
writeFileSync(workflow, readFileSync(workflow, "utf8").replace("## Purpose", "## Removed Purpose"));
expect(validateProject(workflowProject).filter((c) => c.status === "fail")).toContainEqual(
expect.objectContaining({ id: "codex.workflow.ui-ux-review.sections" })
);
});
test("generated surface hashes are stable for object key order", () => {
expect(stableHash({ b: 2, a: { d: 4, c: 3 } })).toBe(stableHash({ a: { c: 3, d: 4 }, b: 2 }));
test("template bodies do not require generated freshness metadata", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-template-metadata-"));
const { projectRoot } = initTemplateProject({ name: "Template Metadata Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const failures = validateProject(projectRoot).filter((c) => c.status === "fail").map((c) => c.id);
expect(failures).not.toContain("codex.role.market-analyst.prompt.freshness");
expect(failures).not.toContain("codex.workflow.ui-ux-review.freshness");
expect(failures).not.toContain("codex.workflow.ui-ux-review.body");
});
test("generated surface validation detects freshness, body tampering, and legacy metadata", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot } = initProject({ name: "Freshness Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const rolePrompt = path.join(projectRoot, ".codex", "prompts", "market-analyst.md");
writeFileSync(rolePrompt, readFileSync(rolePrompt, "utf8").replace(/source-input-sha256: [a-f0-9]+/, "source-input-sha256: bad"));
expect(validateProject(projectRoot).filter((c) => c.status === "fail").map((c) => c.id)).toContain("codex.role.market-analyst.prompt.freshness");
const { projectRoot: bodyProject } = initProject({ name: "Body Freshness Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const bodyPrompt = path.join(bodyProject, ".codex", "prompts", "market-analyst.md");
writeFileSync(bodyPrompt, `${readFileSync(bodyPrompt, "utf8")}\nTampered body.\n`);
expect(validateProject(bodyProject).filter((c) => c.status === "fail").map((c) => c.id)).toContain("codex.role.market-analyst.prompt.body");
const { projectRoot: metadataBodyProject } = initProject({ name: "Metadata Body Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const metadataBodyPrompt = path.join(metadataBodyProject, ".codex", "prompts", "market-analyst.md");
writeFileSync(metadataBodyPrompt, `${readFileSync(metadataBodyPrompt, "utf8")}\n<!-- source-input-sha256: deadbeef -->\n`);
expect(validateProject(metadataBodyProject).filter((c) => c.status === "fail").map((c) => c.id)).toContain("codex.role.market-analyst.prompt.body");
const { projectRoot: rendererProject } = initProject({ name: "Renderer Drift Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const rendererPrompt = path.join(rendererProject, ".codex", "prompts", "market-analyst.md");
const changedBody = readFileSync(rendererPrompt, "utf8").replace("# Market Analyst", "# Market Research Analyst");
const changedHash = hashGeneratedBody(stripGeneratedMetadata(changedBody));
writeFileSync(rendererPrompt, changedBody.replace(/rendered-body-sha256: [a-f0-9]+/, `rendered-body-sha256: ${changedHash}`));
expect(validateProject(rendererProject).filter((c) => c.status === "fail").map((c) => c.id)).toContain("codex.role.market-analyst.prompt.body");
const { projectRoot: workflowProject } = initProject({ name: "Workflow Freshness Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const workflow = path.join(workflowProject, ".codex", "workflows", "ui-ux-review.md");
writeFileSync(workflow, readFileSync(workflow, "utf8").replace(/source-input-sha256: [a-f0-9]+/, "source-input-sha256: bad"));
expect(validateProject(workflowProject).filter((c) => c.status === "fail").map((c) => c.id)).toContain("codex.workflow.ui-ux-review.freshness");
const { projectRoot: workflowBodyProject } = initProject({ name: "Workflow Body Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const workflowBody = path.join(workflowBodyProject, ".codex", "workflows", "ui-ux-review.md");
writeFileSync(workflowBody, `${readFileSync(workflowBody, "utf8")}\nTampered workflow body.\n`);
expect(validateProject(workflowBodyProject).filter((c) => c.status === "fail").map((c) => c.id)).toContain("codex.workflow.ui-ux-review.body");
const { projectRoot: legacyProject } = initProject({ name: "Legacy Surface Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const legacyPrompt = path.join(legacyProject, ".codex", "prompts", "market-analyst.md");
writeFileSync(legacyPrompt, readFileSync(legacyPrompt, "utf8").replace(/^<!-- .* -->\n/gm, ""));
const legacyChecks = validateProject(legacyProject);
expect(legacyChecks).toContainEqual(expect.objectContaining({ id: "codex.role.market-analyst.prompt.freshness", status: "skip" }));
expect(legacyChecks.filter((c) => c.status === "fail").map((c) => c.id)).not.toContain("codex.role.market-analyst.prompt.freshness");
});
test("generated surface validation fails malformed metadata without treating it as legacy", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
const { projectRoot: malformedProject } = initProject({ name: "Malformed Surface Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const malformedPrompt = path.join(malformedProject, ".codex", "prompts", "market-analyst.md");
writeFileSync(malformedPrompt, readFileSync(malformedPrompt, "utf8").replace(/^<!-- generated-by: codex-game-studio surface=.* -->\n/m, "<!-- generated-by: codex-game-studio -->\n"));
const malformedFailures = validateProject(malformedProject).filter((c) => c.status === "fail").map((c) => c.id);
expect(malformedFailures).toContain("codex.role.market-analyst.prompt.freshness");
const { projectRoot: partialProject } = initProject({ name: "Partial Surface Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const partialWorkflow = path.join(partialProject, ".codex", "workflows", "ui-ux-review.md");
writeFileSync(partialWorkflow, readFileSync(partialWorkflow, "utf8").replace(/^<!-- rendered-body-sha256: .* -->\n/m, ""));
const partialFailures = validateProject(partialProject).filter((c) => c.status === "fail").map((c) => c.id);
expect(partialFailures).toContain("codex.workflow.ui-ux-review.body");
const { projectRoot: legacyProject } = initProject({ name: "Commentless Surface Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-")));
const legacyPrompt = path.join(legacyProject, ".codex", "prompts", "market-analyst.md");
writeFileSync(legacyPrompt, readFileSync(legacyPrompt, "utf8").replace(/^<!-- .* -->\n/gm, ""));
expect(validateProject(legacyProject)).toContainEqual(expect.objectContaining({ id: "codex.role.market-analyst.prompt.freshness", status: "skip" }));
});
test("generated skill content markers are validated", () => {
test("template skill content markers are validated", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-skill-depth-"));
const { projectRoot } = initProject({ name: "Skill Marker Val", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const { projectRoot } = initTemplateProject({ name: "Skill Marker Val", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const file = path.join(projectRoot, ".agents", "skills", "cgs-vertical-slice", "SKILL.md");
const body = readFileSync(file, "utf8");
const qualityGateStart = body.indexOf("## Quality Gates");
@@ -308,9 +270,9 @@ describe("validation", () => {
);
});
test("generated surface source input covers rendered engine and role display fields", () => {
test("template helper source input covers rendered engine and role display fields", () => {
const engines = loadEngineConfigs(packageAssetPath("engine_configs"));
const config = initProject({ name: "Hash Coverage Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-"))).config;
const config = initTemplateProject({ name: "Hash Coverage Game", engine: "godot", mode: "prototype", nonInteractive: true }, mkdtempSync(path.join(tmpdir(), "ogs-val-"))).config;
const baseRoleHash = stableHash(projectRolePromptSourceInput("market-analyst", config, engines));
const renamedEngines = { ...engines, godot: { ...engines.godot, display_name: "Renamed Godot" } };
expect(stableHash(projectRolePromptSourceInput("market-analyst", config, renamedEngines))).not.toBe(baseRoleHash);