feat: add Open GameStudio Codex port

This commit is contained in:
MerlinH
2026-05-27 22:58:48 +00:00
parent 38fd33394a
commit f8530077d9
12 changed files with 113 additions and 33 deletions
+1 -1
View File
@@ -12,4 +12,4 @@ Do not load all agents or all templates for a single role task.
`src/agents.ts` is the single owner for generated project `AGENTS.md`.
Direct Codex execution, telemetry, planner/next, ownership enforcement, and parallel orchestration are future-only.
Direct Codex execution is a first-class path via `open-gamestudio run <agent> --exec`. Telemetry, planner/next, ownership enforcement, and parallel orchestration are future-only.
+13 -7
View File
@@ -2,7 +2,7 @@
Open GameStudio is a Node/TypeScript CLI package for creating and managing local, agent-assisted game projects. It provides project scaffolding, engine-aware configuration, base agent prompts, reusable templates, bounded prompt packets, and validation gates that keep generated project artifacts predictable.
The package is an agent workflow layer for game making. It does not host a service, execute assistants directly, or require a specific model provider. The current CLI prepares the project structure and prompt context that Codex or another agent can use from your own environment.
The package is a Codex-first agent workflow layer for game making. It keeps project state local and inspectable while integrating directly with `codex exec` for role-specific work. The CLI prepares bounded prompt packets, invokes Codex when requested, and preserves deterministic validation gates around generated artifacts.
## Why This Exists
@@ -11,12 +11,12 @@ Open GameStudio started as a port motivated by a simple need: make the game-stud
Claude Game Studio deserves real kudos for proving that role-based game-development workflows can be practical and useful. Open GameStudio is inspired by that idea, but it is an independent implementation with different priorities:
- CLI and package first, with deterministic npm scripts for local development.
- Provider and model agnostic, with no direct Claude, Codex, or assistant lock-in.
- Codex-native by default, with direct `codex exec` integration for role-specific work.
- Generated game projects live under `projects/<slug>/`.
- Engine configs, templates, and base agents are package assets.
- Validation is explicit and hard-failing instead of advisory.
- Prompt packets are prepared for external agents instead of spawning a provider process.
- Direct Codex execution, telemetry, planner/`next`, ownership enforcement, changed-file tracking, and parallel orchestration are future-only.
- Prompt packets are optimized for Codex and can be executed directly with `--exec`.
- Telemetry, planner/`next`, ownership enforcement, changed-file tracking, and parallel orchestration are future-only.
The goal is not to clone another tool. The goal is to make the workflow contract inspectable, portable, and easy to run in normal developer tooling.
@@ -76,14 +76,20 @@ npm run validate
npm run validate -- --project projects/my-game
```
Prepare a bounded prompt packet for an agent:
Run a project agent through Codex:
```sh
npm run build --silent
node dist/cli.js run market_analyst --project projects/my-game --task "Create the initial market overview." --exec
```
Inspect the generated prompt packet without executing Codex:
```sh
node dist/cli.js run market_analyst --project projects/my-game --task "Create the initial market overview." --dry-run
```
The `run` command prepares context and output paths. In this build, you execute Codex or another agent separately and point it at the generated prompt packet.
The `run` command writes a bounded prompt packet and, with `--exec`, immediately invokes `codex exec` in the project root. Use `--dry-run` or `--print-prompt` when you want to inspect the exact Codex context first.
## CLI Commands
@@ -94,7 +100,7 @@ The `run` command prepares context and output paths. In this build, you execute
- `validate`: run repository or project validation and exit nonzero on failure.
- `templates list`: list packaged template IDs.
- `templates show <template-id>`: print a packaged template.
- `run <agent>`: prepare one bounded prompt packet for a project agent.
- `run <agent>`: prepare one bounded Codex prompt packet for a project agent; add `--exec` to invoke `codex exec` immediately.
## Project Layout
+1 -1
View File
@@ -15,4 +15,4 @@ npm run validate
Keep generated projects under `projects/<slug>/`.
The first build intentionally excludes planner commands, direct Codex execution, telemetry, parallel orchestration, changed-file tracking, and ownership enforcement.
The first build includes direct Codex execution through `run --exec`. It still intentionally excludes planner commands, telemetry, parallel orchestration, changed-file tracking, and ownership enforcement.
+4 -4
View File
@@ -8,16 +8,16 @@ npm exec open-gamestudio -- status --project projects/my-game
npm exec open-gamestudio -- validate --project projects/my-game
```
Prepare a bounded prompt packet:
Run a role agent directly through Codex:
```bash
npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview."
npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview." --exec
```
Manual external Codex command; `open-gamestudio` does not spawn Codex in the first build:
Inspect the bounded Codex prompt packet first:
```bash
codex exec --cd projects/my-game "Read .gamestudio/runs/<run-id>/prompt.md and perform the requested task."
npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview." --dry-run
```
Discover templates:
+3 -1
View File
@@ -12,4 +12,6 @@ Legacy validation depended on Python and shell assumptions. This port is TypeScr
Intentional omissions for the first build: no interactive `menu`, no `startover`, no generated `project_orchestrator.md`, no exact `template_info.md`, no eager competitor reports during init, and no upstream license/authorship/citation parity documents.
Future-only features are not implemented in this build: planner/`next`, telemetry, direct Codex execution, parallel orchestration, changed-file tracking, prompt-size metrics, and hard output-ownership enforcement.
Codex-native difference: `run --exec` invokes `codex exec` directly against the generated bounded prompt packet instead of requiring a separate manual command.
Future-only features are not implemented in this build: planner/`next`, telemetry, parallel orchestration, changed-file tracking, prompt-size metrics, and hard output-ownership enforcement.
+4 -8
View File
@@ -1,18 +1,14 @@
# Migration From Claude-Oriented Game Studio
Use the canonical TypeScript CLI:
Use the canonical TypeScript CLI with direct Codex execution:
```bash
npm exec open-gamestudio -- init --name "My Game" --engine godot --mode prototype --non-interactive --competitor "Mini Metro"
npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview."
npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview." --exec
```
Manual external Codex command; `open-gamestudio` does not spawn Codex in the first build:
```bash
codex exec --cd projects/my-game "Read .gamestudio/runs/<run-id>/prompt.md and perform the requested task."
```
For inspection-only runs, omit `--exec` or add `--dry-run` to view the generated Codex prompt packet and metadata path before execution.
Intentional differences: no interactive menu, no `startover`, no exact `template_info.md`, no eager competitor reports during init, and no generated `project_orchestrator.md`.
Future-only features are not implemented: `open-gamestudio next`, `run --exec`, telemetry, parallel orchestration, changed-file tracking, and ownership enforcement.
Future-only features are not implemented: `open-gamestudio next`, telemetry, parallel orchestration, changed-file tracking, and ownership enforcement.
+3 -3
View File
@@ -2,15 +2,15 @@
Validation exits nonzero when any check fails.
Repo validation checks package scripts, build output, NodeNext import specifiers, package assets, engine configs, base agents, templates, package packing, and installed-bin asset loading.
Repo validation checks package scripts, build output, NodeNext import specifiers, package assets, engine configs, base agents, templates, package packing, installed-bin asset loading, and direct Codex execution exposure.
Project validation checks schema-valid config, active agents, engine source root, engine project file, materialized agents, project `AGENTS.md` provenance and config hash, market seed, starter GDD, timeline sections, and read-only `status`/`resume` behavior.
Absence checks:
CLI surface checks:
```bash
npm exec open-gamestudio -- run --help | grep -- "--exec"
! npm exec open-gamestudio -- --help | grep -E " next|telemetry"
! npm exec open-gamestudio -- run --help | grep -- "--exec"
```
No generated `project_orchestrator.md` is required or produced.
+2 -1
View File
@@ -58,7 +58,8 @@ ${config.team.active_agents.map((agent) => `- ${agent}: .gamestudio/agents/${age
# Rules
Use bounded context. Load the current role prompt, project config, engine overlay, and task-relevant templates only.
Do not use direct Codex execution, telemetry, planner/next, parallel orchestration, or ownership enforcement in this first build.
Codex execution is first-class: prefer \`open-gamestudio run <agent> --exec\` when you want the toolkit to invoke Codex directly; use \`--dry-run\` or \`--print-prompt\` only for inspection.
Do not use telemetry, planner/next, parallel orchestration, or ownership enforcement in this build.
`;
}
+14 -2
View File
@@ -4,7 +4,7 @@ import path from "node:path";
import { formatTemplateShow, listTemplates, templateRegistry, type TemplateId } from "./templates.js";
import { freezeProject, initProject, resumeProject, statusProject } from "./projects.js";
import { runValidation } from "./validation.js";
import { prepareRun } from "./runner.js";
import { executeCodexRun, prepareRun } from "./runner.js";
const program = new Command();
@@ -84,12 +84,13 @@ templates
program
.command("run")
.description("Prepare one bounded prompt packet for a project agent")
.description("Prepare one bounded prompt packet for a project agent, with optional direct Codex execution")
.argument("<agent>")
.requiredOption("--project <path>", "project path")
.requiredOption("--task <text>", "task text")
.option("--print-prompt", "print deterministic prompt body")
.option("--dry-run", "print selected context and output paths")
.option("--exec", "execute the prompt immediately with codex exec")
.option("--include-artifact <relative-path>", "include one project artifact", (value, previous: string[] = []) => [...previous, value], [])
.option("--allow-broad-context", "explicitly allow broader context discovery")
.action((agent, opts) => {
@@ -98,10 +99,21 @@ program
task: opts.task,
printPrompt: opts.printPrompt,
dryRun: opts.dryRun,
exec: opts.exec,
includeArtifact: opts.includeArtifact,
allowBroadContext: opts.allowBroadContext
});
console.log(result.output);
if (!opts.exec) return;
const execution = executeCodexRun(result);
if (execution.stdout) process.stdout.write(execution.stdout);
if (execution.stderr) process.stderr.write(execution.stderr);
if (execution.error) {
console.error(execution.error.message);
process.exitCode = 1;
return;
}
if (execution.status !== 0) process.exitCode = execution.status ?? 1;
});
program.parseAsync().catch((error: unknown) => {
+40 -2
View File
@@ -1,3 +1,4 @@
import { spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
import path from "node:path";
import { agentNameSchema, readProjectConfig, type AgentName } from "./config.js";
@@ -11,6 +12,8 @@ export type RunOptions = {
task: string;
printPrompt?: boolean;
dryRun?: boolean;
exec?: boolean;
codexBin?: string;
includeArtifact?: string[];
allowBroadContext?: boolean;
};
@@ -19,15 +22,46 @@ export type PreparedRun = {
prompt: string;
promptPath: string;
metadataPath: string;
projectRoot: string;
contextFiles: string[];
codexCommand: { command: string; args: string[]; display: string };
output: string;
};
export type CodexExecutionResult = {
status: number | null;
signal: NodeJS.Signals | null;
stdout: string;
stderr: string;
error?: Error;
};
function requireTask(task: string): string {
if (!task || !task.trim()) throw new Error("--task is required and must be non-empty");
return task.trim();
}
export function codexExecInvocation(projectRoot: string, promptPath: string, codexBin = "codex"): { command: string; args: string[]; display: string } {
const promptRelative = path.relative(projectRoot, promptPath);
const taskPrompt = `Read ${promptRelative} and perform the requested task.`;
const args = ["exec", "--cd", projectRoot, taskPrompt];
return { command: codexBin, args, display: [codexBin, ...args.map((arg) => JSON.stringify(arg))].join(" ") };
}
export function executeCodexRun(run: PreparedRun): CodexExecutionResult {
const result = spawnSync(run.codexCommand.command, run.codexCommand.args, {
cwd: run.projectRoot,
encoding: "utf8"
});
return {
status: result.status,
signal: result.signal,
stdout: result.stdout ?? "",
stderr: result.stderr ?? "",
error: result.error
};
}
function safeArtifact(projectRoot: string, artifact: string): string {
if (path.isAbsolute(artifact)) throw new Error("--include-artifact must be relative");
const full = path.resolve(projectRoot, artifact);
@@ -41,6 +75,7 @@ function safeArtifact(projectRoot: string, artifact: string): string {
let runSequence = 0;
export function prepareRun(agentInput: string, options: RunOptions, cwd = process.cwd()): PreparedRun {
if (options.exec && (options.printPrompt || options.dryRun)) throw new Error("--exec cannot be combined with --print-prompt or --dry-run");
const agent = agentNameSchema.parse(agentInput) as AgentName;
const task = requireTask(options.task);
const projectRoot = resolveProjectRoot(options.project, cwd);
@@ -112,10 +147,13 @@ export function prepareRun(agentInput: string, options: RunOptions, cwd = proces
2
)}\n`
);
const codexCommand = codexExecInvocation(projectRoot, promptPath, options.codexBin ?? process.env.OPEN_GAMESTUDIO_CODEX_BIN ?? "codex");
const output = options.printPrompt
? prompt
: options.dryRun
? `Prompt cache: ${promptPath}\nMetadata: ${metadataPath}\nContext files:\n${contextFiles.map((f) => `- ${f}`).join("\n")}\nValidation: npm run validate -- --project ${path.relative(cwd, projectRoot) || "."}`
: `Prompt cache written: ${promptPath}\nNext manual command: codex exec --cd ${projectRoot} "Read ${path.relative(projectRoot, promptPath)} and perform the requested task."`;
return { prompt, promptPath, metadataPath, contextFiles, output };
: options.exec
? `Prompt cache written: ${promptPath}\nExecuting Codex: ${codexCommand.display}`
: `Prompt cache written: ${promptPath}\nNext Codex command: ${codexCommand.display}`;
return { prompt, promptPath, metadataPath, projectRoot, contextFiles, codexCommand, output };
}
+2 -1
View File
@@ -98,7 +98,8 @@ export async function validateRepo(root = process.cwd()): Promise<ValidationChec
}
}
const help = readFileSync(path.join(root, "src", "cli.ts"), "utf8");
for (const forbidden of ["next", "--exec", "telemetry", "parallel orchestration", "ownership enforcement"]) {
checks.push(help.includes('.option("--exec"') ? pass("codex.exec", "direct codex exec option exposed") : fail("codex.exec", "run command must expose --exec for direct Codex integration"));
for (const forbidden of ["next", "telemetry", "parallel orchestration", "ownership enforcement"]) {
checks.push(!help.includes(`command("${forbidden}`) && !help.includes(`option("${forbidden}`) ? pass(`future.absent.${forbidden}`, `${forbidden} not exposed`) : fail(`future.absent.${forbidden}`, `${forbidden} must not be exposed`));
}
return checks;
+26 -2
View File
@@ -1,10 +1,10 @@
import { existsSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, test } from "vitest";
import { initProject } from "../src/projects.js";
import { prepareRun } from "../src/runner.js";
import { codexExecInvocation, executeCodexRun, prepareRun } from "../src/runner.js";
describe("bounded runner", () => {
test("requires non-empty task", () => {
@@ -73,4 +73,28 @@ describe("bounded runner", () => {
expect(a).toContain("# Template: analytics_setup");
expect(a).toContain("documentation/technical/analytics/analytics-plan.md");
});
test("builds a direct codex exec invocation for prepared prompts", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-"));
const { projectRoot } = initProject({ name: "Codex Exec Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const result = prepareRun("qa_agent", { project: projectRoot, task: "Review validation readiness", exec: true, codexBin: "codex-test" }, cwd);
expect(result.output).toContain("Executing Codex:");
expect(result.codexCommand.command).toBe("codex-test");
expect(result.codexCommand.args.slice(0, 3)).toEqual(["exec", "--cd", projectRoot]);
expect(result.codexCommand.args[3]).toMatch(/Read \.gamestudio\/runs\/.+prompt\.md and perform the requested task\./);
expect(codexExecInvocation(projectRoot, result.promptPath, "codex-test").display).toContain("codex-test");
expect(() => prepareRun("qa_agent", { project: projectRoot, task: "x", exec: true, dryRun: true }, cwd)).toThrow(/--exec cannot be combined/);
});
test("can execute a codex-compatible binary with argument isolation", () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-"));
const { projectRoot } = initProject({ name: "Stub Codex Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
const stub = path.join(cwd, "codex-stub.mjs");
writeFileSync(stub, "#!/usr/bin/env node\nconsole.log(JSON.stringify(process.argv.slice(2)));\n");
chmodSync(stub, 0o755);
const result = prepareRun("qa_agent", { project: projectRoot, task: "Review validation readiness", exec: true, codexBin: stub }, cwd);
const execution = executeCodexRun(result);
expect(execution.status).toBe(0);
expect(JSON.parse(execution.stdout)).toEqual(result.codexCommand.args);
});
});