mirror of
https://github.com/merlinhu1/codex-game-studio.git
synced 2026-08-25 07:54:34 +02:00
feat: add Open GameStudio Codex port
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
projects/
|
||||
*.tgz
|
||||
@@ -0,0 +1,15 @@
|
||||
# Global Agent Instructions
|
||||
|
||||
Use `npm run validate` before any parity claim.
|
||||
|
||||
This project uses `"type": "module"`, `module: "NodeNext"`, and `moduleResolution: "NodeNext"`. Every relative TypeScript import must use the emitted `.js` specifier: write `import { x } from "./config.js"`, never `import { x } from "./config"`.
|
||||
|
||||
For local development before install/link, use npm scripts. Package scripts build first and exercise the built CLI through `node dist/cli.js`; use `npm run init -- ...`, `npm run manage -- ...`, `npm run templates -- list`, and `npm run validate -- ...`. Use `npm exec open-gamestudio -- ...` only after build/link/install or inside package-bin smoke fixtures. Bare `open-gamestudio ...` is only guaranteed after package install/link.
|
||||
|
||||
Keep generated game projects under `projects/<slug>/`.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Contributing
|
||||
|
||||
Use Node 20 or newer.
|
||||
|
||||
Before opening changes:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm test
|
||||
npm run validate
|
||||
```
|
||||
|
||||
Do not add Python compatibility files, duplicate script-wrapper logic, telemetry, direct Codex execution, planner behavior, parallel orchestration, or ownership enforcement without a new design.
|
||||
|
||||
Do not edit `research/*` as part of implementation changes unless the task explicitly asks for research updates.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 MerlinH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Open GameStudio
|
||||
|
||||
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.
|
||||
|
||||
## Why This Exists
|
||||
|
||||
Open GameStudio started as a port motivated by a simple need: make the game-studio workflow open, portable, local-first, scriptable, and usable outside a single assistant environment.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 20 or newer.
|
||||
- npm.
|
||||
|
||||
## Install
|
||||
|
||||
For package use after installation or linking:
|
||||
|
||||
```sh
|
||||
npm exec open-gamestudio -- --help
|
||||
npm exec open-gamestudio -- templates list
|
||||
```
|
||||
|
||||
For local development from this repository, use the npm scripts. They build first and then exercise the built CLI through `node dist/cli.js`:
|
||||
|
||||
```sh
|
||||
npm run init -- --name "My Game" --engine godot --mode prototype --non-interactive
|
||||
npm run manage -- --project projects/my-game
|
||||
npm run templates -- list
|
||||
npm run validate -- --project projects/my-game
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Create a project:
|
||||
|
||||
```sh
|
||||
npm run init -- --name "My Game" --engine godot --mode prototype --non-interactive --concept "A compact puzzle game about routing trains"
|
||||
```
|
||||
|
||||
Inspect project status:
|
||||
|
||||
```sh
|
||||
npm run manage -- --project projects/my-game
|
||||
```
|
||||
|
||||
List templates:
|
||||
|
||||
```sh
|
||||
npm run templates -- list
|
||||
```
|
||||
|
||||
Show a template:
|
||||
|
||||
```sh
|
||||
npm run templates -- show gdd
|
||||
```
|
||||
|
||||
Validate the repository or a generated project:
|
||||
|
||||
```sh
|
||||
npm run validate
|
||||
npm run validate -- --project projects/my-game
|
||||
```
|
||||
|
||||
Prepare a bounded prompt packet for an agent:
|
||||
|
||||
```sh
|
||||
npm run build --silent
|
||||
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.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
- `init` / `new`: create a project under `projects/<slug>/`.
|
||||
- `status`: print project phase, status, engine, and next validation command.
|
||||
- `resume`: print a read-only continuation summary.
|
||||
- `freeze`: mark a project as frozen.
|
||||
- `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.
|
||||
|
||||
## Project Layout
|
||||
|
||||
Repository assets:
|
||||
|
||||
- `src/`: TypeScript CLI implementation.
|
||||
- `agents/base/`: base role prompts packaged with the CLI.
|
||||
- `templates/`: reusable document and setup templates.
|
||||
- `engine_configs/`: engine overlays for Godot, Unity, and Unreal.
|
||||
- `docs/`: setup, migration, validation, and example notes.
|
||||
- `tests/`: Vitest coverage for project workflow, templates, agents, runner prompts, validation, and engine behavior.
|
||||
|
||||
Generated project artifacts:
|
||||
|
||||
- `projects/<slug>/`: the project root created by `init`.
|
||||
- `project.gamestudio.json`: project metadata and workflow state.
|
||||
- `AGENTS.md`: generated project instructions owned by `src/agents.ts`.
|
||||
- `documentation/`: generated game-design and workflow documents.
|
||||
- `source/project-<slug>/`: engine project location contract.
|
||||
- `.gamestudio/runs/`: prepared prompt packets and run metadata.
|
||||
|
||||
## Development
|
||||
|
||||
Use the repository scripts:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run typecheck
|
||||
npm run test
|
||||
npm run validate
|
||||
```
|
||||
|
||||
This project uses ESM TypeScript with `module` and `moduleResolution` set to `NodeNext`. Relative TypeScript imports include the emitted `.js` specifier.
|
||||
|
||||
## License
|
||||
|
||||
Open GameStudio is released under the MIT License. See `LICENSE`.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Define analytics, metrics, events, and experiment-readiness for the project.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, core loop, target audience, and analytics template.
|
||||
|
||||
# Outputs
|
||||
|
||||
Analytics plan, event taxonomy, metric definitions, and validation notes.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/technical/analytics/analytics-plan.md`.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Map instrumentation ideas to the selected engine.
|
||||
|
||||
# Rules
|
||||
|
||||
Do not add telemetry for this toolkit; discuss only game analytics artifacts.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Improve responsiveness, feedback, controls, camera, and tuning.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, mechanics notes, engine overlay, and playtest observations.
|
||||
|
||||
# Outputs
|
||||
|
||||
Game-feel tuning notes, implementation tasks, and validation criteria.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/design/feel/` and selected engine source paths.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Map feedback and tuning work to engine-specific systems.
|
||||
|
||||
# Rules
|
||||
|
||||
Keep changes measurable and playtest-oriented.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Analyze audience, positioning, competitors, and monetization fit.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, competitor names, audience, genre, platform, and market template.
|
||||
|
||||
# Outputs
|
||||
|
||||
Market overview, competitor comparison, positioning risks, and recommended research questions.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `resources/market-research/market-analysis.md`.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Relate market risks to engine/platform constraints.
|
||||
|
||||
# Rules
|
||||
|
||||
Do not generate eager per-competitor reports unless the task asks for them.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Coordinate the game-studio workflow, sequence work, and keep scope aligned with project goals.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, current task, selected engine notes, and relevant handoff material.
|
||||
|
||||
# Outputs
|
||||
|
||||
Coordination notes, next role recommendation, and explicit artifact paths.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/handoffs/` for handoffs and reference project artifacts instead of embedding them.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Adapt sequencing to the selected engine overlay.
|
||||
|
||||
# Rules
|
||||
|
||||
Use bounded context. Do not run Codex, telemetry, planner, parallel orchestration, or ownership enforcement.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Design and implement gameplay mechanics plans within the selected engine contract.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, engine setup notes, feature spec, and current task.
|
||||
|
||||
# Outputs
|
||||
|
||||
Mechanics implementation notes, source-path guidance, and validation checks.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `source/project-<slug>/` and `documentation/technical/`.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Use the selected engine overlay for folder and project-file expectations.
|
||||
|
||||
# Rules
|
||||
|
||||
Respect the `source/project-<slug>/` contract.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Elaborate features, levels, content, and moment-to-moment design details.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, senior design direction, feature template, and current task.
|
||||
|
||||
# Outputs
|
||||
|
||||
Feature notes, content lists, and playtest-ready acceptance criteria.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/design/features/`.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Keep implementation detail compatible with the selected engine.
|
||||
|
||||
# Rules
|
||||
|
||||
Do not load unrelated templates unless the task requires them.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Own production planning, milestones, status summaries, and project delivery rhythm.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, milestones, timeline, current task, and validation state.
|
||||
|
||||
# Outputs
|
||||
|
||||
Production plan updates, risks, and clear next validation gates.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/production/` and update config only when explicitly requested.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Account for engine-specific setup and build risks.
|
||||
|
||||
# Rules
|
||||
|
||||
Keep operational status separate from generated guidance hashes.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Review validation readiness, test plans, acceptance criteria, and regressions.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, task details, validation command, and selected artifacts.
|
||||
|
||||
# Outputs
|
||||
|
||||
QA plan, failure risks, reproduction steps, and validation checklist.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/qa/`.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Check engine-specific project-file and source-root requirements.
|
||||
|
||||
# Rules
|
||||
|
||||
Do not run agents or broad orchestration; report manual next commands.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Own art direction, visual targets, asset priorities, and style consistency.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, audience, design goals, engine overlay, and art task.
|
||||
|
||||
# Outputs
|
||||
|
||||
Art direction notes, asset lists, and production-ready briefs.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/art/` and engine asset folders under `source/project-<slug>/`.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Consider import paths and asset conventions for the selected engine.
|
||||
|
||||
# Rules
|
||||
|
||||
Do not create unrelated assets without explicit task scope.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Own senior design direction, core loop quality, systems fit, and feature specs.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, GDD, feature request, engine notes, and design template.
|
||||
|
||||
# Outputs
|
||||
|
||||
Design decisions, GDD updates, feature specs, and acceptance criteria.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/design/gdd.md` and `documentation/design/features/`.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Adapt designs to selected engine affordances.
|
||||
|
||||
# Rules
|
||||
|
||||
Prefer scoped design artifacts over broad rewrites.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Bridge art and engineering for shaders, pipelines, import settings, and performance.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, art direction, engine overlay, and technical constraints.
|
||||
|
||||
# Outputs
|
||||
|
||||
Pipeline notes, technical art tasks, and validation checks.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/art/technical/` and engine source asset paths.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Use engine-specific material, shader, and import conventions.
|
||||
|
||||
# Rules
|
||||
|
||||
Keep pipeline guidance reproducible.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Role
|
||||
|
||||
Design UI flows, HUDs, menus, accessibility notes, and interaction ergonomics.
|
||||
|
||||
# Inputs
|
||||
|
||||
Project config, audience, platform, engine overlay, and UI task.
|
||||
|
||||
# Outputs
|
||||
|
||||
UI specs, screen flows, HUD requirements, and validation criteria.
|
||||
|
||||
# Output Paths
|
||||
|
||||
Use `documentation/design/ui-ux/`.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>`.
|
||||
|
||||
# Engine Notes
|
||||
|
||||
Map UI recommendations to selected engine UI systems.
|
||||
|
||||
# Rules
|
||||
|
||||
Protect gameplay readability and avoid broad unrelated context.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Development Rules
|
||||
|
||||
This repository is TypeScript/Node only.
|
||||
|
||||
Use `.js` specifiers for all relative TypeScript imports because the package uses NodeNext.
|
||||
|
||||
Run validation before claiming parity:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm test
|
||||
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.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Examples
|
||||
|
||||
Create and validate a project:
|
||||
|
||||
```bash
|
||||
npm exec open-gamestudio -- init --name "My Game" --engine godot --mode prototype --non-interactive --competitor "Mini Metro" --competitor "Dorfromantik"
|
||||
npm exec open-gamestudio -- status --project projects/my-game
|
||||
npm exec open-gamestudio -- validate --project projects/my-game
|
||||
```
|
||||
|
||||
Prepare a bounded prompt packet:
|
||||
|
||||
```bash
|
||||
npm exec open-gamestudio -- run market_analyst --project projects/my-game --task "Create the initial market overview."
|
||||
```
|
||||
|
||||
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."
|
||||
```
|
||||
|
||||
Discover templates:
|
||||
|
||||
```bash
|
||||
npm exec open-gamestudio -- templates list
|
||||
npm exec open-gamestudio -- templates show market_analysis
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Known Upstream Differences
|
||||
|
||||
This TypeScript/Node port preserves upstream user-facing outcomes while intentionally avoiding legacy implementation details that caused fragile behavior.
|
||||
|
||||
Legacy engine-system checks only partially passed while still reporting success. This port uses hard-failing validation, where any failed check exits nonzero.
|
||||
|
||||
Legacy folder-structure checks and project-file path expectations differed from the desired `source/project-<slug>/` contract. This port uses `source/project-<slug>/` for Godot, Unity, and Unreal.
|
||||
|
||||
Legacy Unreal naming used multiple labels. This port normalizes `Unreal`, `Unreal Engine`, `unreal`, and `ue5` to canonical `unreal`, while keeping `Unreal Engine` as the display name.
|
||||
|
||||
Legacy validation depended on Python and shell assumptions. This port is TypeScript/Node only.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Migration From Claude-Oriented Game Studio
|
||||
|
||||
Use the canonical TypeScript CLI:
|
||||
|
||||
```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."
|
||||
```
|
||||
|
||||
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."
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Setup
|
||||
|
||||
Install and verify with Node 20 or newer:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm test
|
||||
npm run validate
|
||||
```
|
||||
|
||||
Local development scripts build first and then run `node dist/cli.js`.
|
||||
|
||||
```bash
|
||||
npm run init -- --name "My Game" --engine godot --mode prototype --non-interactive --competitor "Mini Metro" --engine-version "4.4.1"
|
||||
npm run templates -- list
|
||||
npm run validate -- --project projects/my-game
|
||||
```
|
||||
|
||||
After build/link/install, the package bin is available:
|
||||
|
||||
```bash
|
||||
npm exec open-gamestudio -- --help
|
||||
npm exec open-gamestudio -- templates show gdd
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
# System Verification
|
||||
|
||||
Required verification:
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm test
|
||||
node dist/cli.js --help
|
||||
node dist/cli.js validate
|
||||
npm exec open-gamestudio -- --help
|
||||
npm exec open-gamestudio -- validate
|
||||
npm exec open-gamestudio -- run --help
|
||||
npm run validate
|
||||
```
|
||||
|
||||
Engine smoke:
|
||||
|
||||
```bash
|
||||
npm run init -- --name "Codex Godot Smoke" --engine godot --mode prototype --non-interactive
|
||||
npm run init -- --name "Codex Unity Smoke" --engine unity --mode design --non-interactive
|
||||
npm run init -- --name "Codex Unreal Smoke" --engine "Unreal Engine" --mode development --non-interactive
|
||||
npm run validate -- --project projects/codex-godot-smoke
|
||||
npm run validate -- --project projects/codex-unity-smoke
|
||||
npm run validate -- --project projects/codex-unreal-smoke
|
||||
rm -rf projects/codex-godot-smoke projects/codex-unity-smoke projects/codex-unreal-smoke
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
# Workflow Validation
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
! 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.
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "godot",
|
||||
"display_name": "Godot",
|
||||
"aliases": ["godot", "Godot", "Godot Engine"],
|
||||
"default_version": "4.4.1",
|
||||
"source_root_pattern": "source/project-{slug}",
|
||||
"folders": ["assets", "scenes", "scripts"],
|
||||
"project_files": ["project.godot"],
|
||||
"best_practices": ["Use scenes for composition.", "Keep gameplay scripts focused."],
|
||||
"agent_specializations": {
|
||||
"summary": "Godot project using scenes, nodes, resources, and GDScript-friendly architecture.",
|
||||
"validation": "Confirm project.godot exists under source/project-<slug>/."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "unity",
|
||||
"display_name": "Unity",
|
||||
"aliases": ["unity", "Unity", "Unity Engine"],
|
||||
"default_version": "6000.0",
|
||||
"source_root_pattern": "source/project-{slug}",
|
||||
"folders": ["Assets", "Packages", "ProjectSettings"],
|
||||
"project_files": ["Packages/manifest.json", "ProjectSettings/ProjectSettings.asset"],
|
||||
"best_practices": ["Keep gameplay code under Assets.", "Track ProjectSettings markers."],
|
||||
"agent_specializations": {
|
||||
"summary": "Unity project using Assets, Packages, and ProjectSettings layout.",
|
||||
"validation": "Confirm Packages/manifest.json and ProjectSettings/ProjectSettings.asset exist."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "unreal",
|
||||
"display_name": "Unreal Engine",
|
||||
"aliases": ["unreal", "Unreal", "Unreal Engine", "ue", "ue5"],
|
||||
"default_version": "5.4",
|
||||
"source_root_pattern": "source/project-{slug}",
|
||||
"folders": ["Content", "Config", "Source"],
|
||||
"project_files": ["<ProjectClass>.uproject"],
|
||||
"best_practices": ["Keep gameplay modules explicit.", "Use Content for assets and Config for project defaults."],
|
||||
"agent_specializations": {
|
||||
"summary": "Unreal Engine project using Content, Config, Source, and a PascalCase .uproject file.",
|
||||
"validation": "Confirm <ProjectClass>.uproject exists under source/project-<slug>/."
|
||||
}
|
||||
}
|
||||
Generated
+2228
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "open-gamestudio",
|
||||
"version": "0.1.0",
|
||||
"description": "Provider-neutral CLI workflow layer for agent-assisted game projects.",
|
||||
"author": "MerlinH",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"engine_configs/",
|
||||
"agents/base/",
|
||||
"templates/"
|
||||
],
|
||||
"bin": {
|
||||
"open-gamestudio": "./dist/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "vitest run",
|
||||
"validate": "npm run build --silent && node dist/cli.js validate",
|
||||
"init": "npm run build --silent && node dist/cli.js init",
|
||||
"manage": "npm run build --silent && node dist/cli.js status",
|
||||
"templates": "npm run build --silent && node dist/cli.js templates"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.1.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.25",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^1.6.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,680 @@
|
||||
# Codex-Native Game Studio Port Design
|
||||
|
||||
## Purpose
|
||||
|
||||
Port `pamirtuna/gamestudio-subagents` into this repository as a Codex-native TypeScript/Node toolkit without carrying over the legacy implementation's brittle execution model or false-green validation behavior.
|
||||
|
||||
The port must preserve upstream user-facing capabilities. Scope control should remove only **new Codex-era optional features**, not features that already exist upstream. If upstream supports a workflow, engine, agent, template, script-style entry point, or project-management action, the TypeScript port should preserve that capability with a cleaner implementation and hard-failing validation.
|
||||
|
||||
This document is split into three scopes:
|
||||
|
||||
1. **Clean parity contract**: upstream capabilities that must be preserved without legacy bugs.
|
||||
2. **Codex-native improvements**: additions that make the port more useful but should remain tightly bounded.
|
||||
3. **Future optional layer**: features not present upstream and not needed for the first implementation.
|
||||
|
||||
## Ground Rules
|
||||
|
||||
- Build the port as TypeScript/Node only.
|
||||
- Use `open-gamestudio` as the canonical CLI.
|
||||
- Preserve upstream user-facing capabilities unless explicitly documented as an intentional known difference.
|
||||
- Do not copy legacy source during the research/design phase.
|
||||
- Do not preserve Python internals, Python package metadata, or shell alias assumptions.
|
||||
- Do not reproduce upstream false-green validation behavior.
|
||||
- Do not claim parity until the new implementation passes clean, hard-failing validation.
|
||||
- Scope reduction is allowed only for features that are new to this port, such as telemetry, direct Codex execution, planner logic, and parallel orchestration.
|
||||
|
||||
## Intentional Known Differences
|
||||
|
||||
The port may intentionally regress upstream implementation or UX details that add maintenance cost without preserving meaningful user value.
|
||||
|
||||
Allowed differences:
|
||||
|
||||
- **No duplicate script-wrapper implementation.** Upstream-style script wrappers are not part of the core parity promise. The port should preserve npm/package discoverability and the canonical `open-gamestudio` CLI, but it does not need separate wrapper files such as `scripts/init_project.mjs` or `scripts/project_manager.mjs` unless they are demonstrably useful. If wrappers are added, they must be thin pass-throughs to the canonical CLI and must not own logic.
|
||||
- **No interactive menu.** Do not port upstream's interactive `menu` flow. It creates a second UI surface with extra state and terminal complexity. Non-interactive CLI commands are the supported interface.
|
||||
- **No `project_orchestrator.md` file parity.** Preserve the orchestration and handoff behavior through project `AGENTS.md`, the materialized `master_orchestrator` agent, and handoff templates/docs. Do not generate a separate upstream-style `project_orchestrator.md` file.
|
||||
- **No exact `template_info.md` parity.** Replace the static upstream template-info document with a machine-readable template registry and/or `open-gamestudio templates` commands. Generated docs may exist later, but exact file parity is not required.
|
||||
- **No eager competitor report generation during init.** Initialization should record competitor names in config and create a market overview/seed document. Full competitor analysis reports should be created by the market analyst workflow when requested, not as init clutter.
|
||||
- **No upstream license/authorship/citation doc parity.** This is a port/rewrite with project-owned licensing. Do not copy upstream license, authorship, or citation docs as parity artifacts. Use only this repository's chosen license outside the parity contract.
|
||||
- **No `startover` command.** Do not implement upstream `startover`; the name is ambiguous and encourages destructive semantics. If revision/reset workflows are needed later, design them under explicit non-destructive names.
|
||||
|
||||
Every intentional difference must be listed here or in migration docs before parity is claimed.
|
||||
|
||||
## Verified Upstream Capability Baseline
|
||||
|
||||
The upstream repository includes these user-facing surfaces:
|
||||
|
||||
- **Engines**: Godot, Unity, and Unreal Engine configs and initializer branches.
|
||||
- **Agents**: 12 studio role prompts plus project-specific agent customization.
|
||||
- **Project initialization**: interactive initializer that creates project folders, engine files, market docs, config, milestones, and customized agents.
|
||||
- **Project management**: status, new, resume, and freeze flows. Upstream also has `menu` and `startover`, but those are intentional known differences and should not be ported.
|
||||
- **Templates**: GDD, feature spec, handoff, analytics setup, engine setup, market analysis, and project config. Upstream `template_info.md` is replaced by registry/command discoverability.
|
||||
- **Validation scripts**: project workflow and engine-system checks.
|
||||
- **Package scripts**: `init`, `manage`, and `test` command aliases.
|
||||
- **Docs**: setup, examples, contribution, development rules, system verification, and workflow validation. Upstream license/authorship/citation files are not parity artifacts for this rewrite.
|
||||
|
||||
The upstream validation baseline is not clean. In particular, the engine-system test reports only part of the suite passing while still exiting successfully. Known legacy issues include:
|
||||
|
||||
- folder-structure helper argument mismatches;
|
||||
- project-file path expectations that differ from generated source roots;
|
||||
- inconsistent Unreal naming such as `Unreal`, `Unreal Engine`, and related aliases;
|
||||
- shell-command assumptions around `python`.
|
||||
|
||||
The port must fix these mistakes by defining clean contracts and hard-failing tests. It must not avoid the features entirely.
|
||||
|
||||
---
|
||||
|
||||
# Part 1: Clean Parity Contract
|
||||
|
||||
## Parity Goal
|
||||
|
||||
The first complete port should preserve upstream workflow outcomes while replacing the fragile Python implementation with a clean TypeScript/Node architecture.
|
||||
|
||||
The core loop is:
|
||||
|
||||
```text
|
||||
idea -> init project -> validate project -> use project-specific agents -> manage project state -> continue work
|
||||
```
|
||||
|
||||
The Codex-native runner described later can improve this loop, but the baseline port must not regress upstream project creation, engine support, agent coverage, templates, or project management except for the intentional known differences listed above.
|
||||
|
||||
## Engine Support
|
||||
|
||||
The port must support all upstream engines:
|
||||
|
||||
- Godot
|
||||
- Unity
|
||||
- Unreal Engine
|
||||
|
||||
Use canonical IDs internally:
|
||||
|
||||
```text
|
||||
godot
|
||||
unity
|
||||
unreal
|
||||
```
|
||||
|
||||
Engine configs should include aliases so user input normalizes cleanly:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "unreal",
|
||||
"display_name": "Unreal Engine",
|
||||
"aliases": ["Unreal", "Unreal Engine", "ue", "ue5"]
|
||||
}
|
||||
```
|
||||
|
||||
Required engine contract:
|
||||
|
||||
- all engine source roots are under `projects/<slug>/source/project-<slug>/`;
|
||||
- engine display names are stable and user-facing;
|
||||
- engine aliases normalize before folder or project-file generation;
|
||||
- validation checks the generated folder and project-file contract for each engine;
|
||||
- each engine has prompt-overlay data for project-specific agents;
|
||||
- unsupported or unknown engine names fail clearly.
|
||||
- Unreal `.uproject` filenames are generated from a shared `projectClassName(nameOrSlug)` helper: split on non-alphanumeric boundaries, PascalCase words, strip punctuation, prefix `Game` when the result would start with a digit, and fail clearly when no alphanumeric characters remain. Examples: `"Test Game" -> "TestGame"` and `"codex-unreal-smoke" -> "CodexUnrealSmoke"`.
|
||||
|
||||
Required generated project files:
|
||||
|
||||
```text
|
||||
Godot:
|
||||
source/project-<slug>/project.godot
|
||||
|
||||
Unity:
|
||||
source/project-<slug>/Packages/manifest.json
|
||||
source/project-<slug>/ProjectSettings/ProjectSettings.asset or another documented Unity project-settings marker
|
||||
|
||||
Unreal:
|
||||
source/project-<slug>/<ProjectName>.uproject
|
||||
```
|
||||
|
||||
Unreal naming must be fixed in the new contract: `Unreal`, `Unreal Engine`, `unreal`, and `ue5` should all normalize to canonical `unreal`, while display output remains `Unreal Engine`.
|
||||
|
||||
## Agent Support
|
||||
|
||||
The port must preserve all 12 upstream studio roles:
|
||||
|
||||
- `master_orchestrator`
|
||||
- `producer_agent`
|
||||
- `market_analyst`
|
||||
- `data_scientist`
|
||||
- `sr_game_designer`
|
||||
- `mid_game_designer`
|
||||
- `mechanics_developer`
|
||||
- `game_feel_developer`
|
||||
- `sr_game_artist`
|
||||
- `technical_artist`
|
||||
- `ui_ux_agent`
|
||||
- `qa_agent`
|
||||
|
||||
These roles are not optional. The first parity target should include all 12 base prompts, adapted to Codex-style structured inputs/outputs.
|
||||
|
||||
Project initialization must also preserve project-specific agent materialization. In the TypeScript port this should be owned by `src/agents.ts` rather than copied from the Python `agent_customizer.py` implementation.
|
||||
|
||||
Required behavior:
|
||||
|
||||
- validate the 12 required base prompts exist;
|
||||
- select active agents by project mode;
|
||||
- inject project summary, engine overlay, and role-specific output guidance;
|
||||
- materialize project-specific prompts under `projects/<slug>/.gamestudio/agents/`;
|
||||
- generate a project-level `AGENTS.md` for Codex-local guidance;
|
||||
- preserve upstream project orchestration/handoff behavior through compact `AGENTS.md`, the materialized `master_orchestrator` agent, and handoff templates/docs. Do not generate a separate `project_orchestrator.md` file.
|
||||
|
||||
Mode-specific active-agent behavior should preserve upstream intent:
|
||||
|
||||
```text
|
||||
always:
|
||||
master_orchestrator
|
||||
producer_agent
|
||||
market_analyst
|
||||
data_scientist
|
||||
|
||||
design:
|
||||
sr_game_designer
|
||||
mid_game_designer
|
||||
sr_game_artist
|
||||
|
||||
prototype:
|
||||
sr_game_designer
|
||||
mechanics_developer
|
||||
qa_agent
|
||||
|
||||
development:
|
||||
sr_game_designer
|
||||
mid_game_designer
|
||||
mechanics_developer
|
||||
game_feel_developer
|
||||
qa_agent
|
||||
sr_game_artist
|
||||
technical_artist
|
||||
ui_ux_agent
|
||||
```
|
||||
|
||||
## Project Initialization
|
||||
|
||||
`open-gamestudio init` must preserve upstream project-creation outcomes while using a cleaner Node implementation.
|
||||
|
||||
Required initialization outputs:
|
||||
|
||||
```text
|
||||
projects/<slug>/
|
||||
AGENTS.md
|
||||
project-config.json
|
||||
source/project-<slug>/...
|
||||
documentation/
|
||||
resources/market-research/
|
||||
.gamestudio/agents/
|
||||
```
|
||||
|
||||
Project initialization should create:
|
||||
|
||||
- engine-specific folder structure for Godot, Unity, or Unreal;
|
||||
- engine-specific project files;
|
||||
- project config;
|
||||
- market overview/seed document;
|
||||
- configured competitor names in project config, without generating full competitor analysis reports during init;
|
||||
- GDD or starter design documentation;
|
||||
- milestone/timeline data equivalent to upstream behavior;
|
||||
- project-specific agent prompts;
|
||||
- project-level Codex guidance.
|
||||
|
||||
The port may simplify excessive folder creation only where it is clearly not user-facing or not validated upstream, but it must not remove engine support, market-analysis seeds, project config, milestones, or project-specific agents. Full competitor reports are intentionally deferred to the market analyst workflow.
|
||||
|
||||
## Project Config
|
||||
|
||||
Use Zod to validate a versioned project config and avoid implicit prompt contracts.
|
||||
|
||||
Required project fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"project": {
|
||||
"name": "My Game",
|
||||
"slug": "my-game",
|
||||
"concept": "One sentence concept",
|
||||
"genre": "Action",
|
||||
"platform": "PC",
|
||||
"audience": "Players who like short sessions",
|
||||
"competitors": ["competitor-a", "competitor-b"],
|
||||
"monetization": "premium",
|
||||
"timeline": "12 weeks",
|
||||
"engine": "godot",
|
||||
"engine_version": "4.4.1",
|
||||
"mode": "prototype",
|
||||
"phase": "Initialization",
|
||||
"status": "active"
|
||||
},
|
||||
"team": {
|
||||
"active_agents": ["master_orchestrator", "producer_agent", "market_analyst", "data_scientist", "sr_game_designer", "mechanics_developer", "qa_agent"]
|
||||
},
|
||||
"production": {
|
||||
"milestones": [
|
||||
{
|
||||
"id": "m1",
|
||||
"title": "Playable prototype",
|
||||
"target": "Week 4",
|
||||
"exit_criteria": ["Core loop is playable"],
|
||||
"status": "planned"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The TypeScript schema may improve shape and naming, but it must preserve upstream information: project identity, audience, competitors, monetization, engine, engine version, mode, phase/status, team/active agents, and schema-validated milestones. Config serialization used for generated guidance hashes must be canonical: recursively sorted keys, two-space indentation, LF newlines, one trailing newline, and an operational-field omission mode for status/run-state fields.
|
||||
|
||||
## Templates and Docs
|
||||
|
||||
The port must preserve upstream template categories:
|
||||
|
||||
- `gdd` -> `gdd_template.md`
|
||||
- `feature_spec` -> `feature_spec_template.md`
|
||||
- `handoff` -> `handoff_template.md`
|
||||
- `analytics_setup` -> `analytics_setup_template.md`
|
||||
- `engine_setup` -> `engine_setup_template.md`
|
||||
- `market_analysis` -> `market_analysis_template.md`
|
||||
- `project_config` -> `project_config_template.json`
|
||||
|
||||
The TypeScript port may rename files only if the migration is documented and validation knows the new paths. Do not preserve exact `template_info.md` file parity; expose template discoverability through a typed registry and/or `open-gamestudio templates` commands.
|
||||
|
||||
Template selection must be deterministic and bounded: `market_analyst` selects `market_analysis`, `data_scientist` selects `analytics_setup`, designer/spec tasks select `gdd`/`feature_spec`, engine/project setup tasks select `engine_setup`/`project_config`, and `handoff` is selected only for handoff/coordination tasks. QA does not load all templates by default.
|
||||
|
||||
The port should also carry forward equivalent docs for:
|
||||
|
||||
- setup/quickstart;
|
||||
- examples;
|
||||
- development rules;
|
||||
- system verification;
|
||||
- workflow validation;
|
||||
- contribution notes.
|
||||
|
||||
Docs do not need to be copied verbatim, but the user-facing guidance should not disappear. Upstream license/authorship/citation docs are explicitly out of scope for this rewrite; use this repository's own licensing policy instead.
|
||||
|
||||
## Project Management
|
||||
|
||||
The port must preserve useful upstream project-management capabilities while intentionally dropping `menu` and `startover`.
|
||||
|
||||
Canonical CLI commands should include equivalents for:
|
||||
|
||||
```bash
|
||||
open-gamestudio status [--project projects/my-game]
|
||||
open-gamestudio new
|
||||
open-gamestudio resume --project projects/my-game
|
||||
open-gamestudio freeze --project projects/my-game
|
||||
```
|
||||
|
||||
`open-gamestudio init` may be the canonical implementation behind `new`.
|
||||
|
||||
Interactive `menu` behavior is intentionally not implemented. Users should rely on documented non-interactive commands.
|
||||
|
||||
Status/resume/freeze should operate on project config state and should not become a separate orchestration system.
|
||||
|
||||
Project-management command semantics:
|
||||
|
||||
- `status`: read-only summary of project config, phase/status, active agents, and latest validation state if available.
|
||||
- `new`: alias or guided wrapper for `init`; it must not create a second project-creation path.
|
||||
- `resume`: read-only continuation summary with the next suggested manual command; it must not run agents.
|
||||
- `freeze`: change only project status to frozen/inactive without deleting source, docs, or run history. Status is operational state and is omitted from the project `AGENTS.md` guidance hash, so a status-only freeze must not make generated guidance stale.
|
||||
- `menu`: intentionally omitted.
|
||||
- `startover`: intentionally omitted. Future revision/reset workflows require a separate design with explicit non-destructive command names.
|
||||
|
||||
## Script-Style Entry Points
|
||||
|
||||
Upstream exposes script/package commands for init, manage, and test. The TypeScript port should preserve package-level discoverability without creating duplicate wrapper logic.
|
||||
|
||||
Required package metadata excerpt:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"init": "npm run build --silent && node dist/cli.js init",
|
||||
"manage": "npm run build --silent && node dist/cli.js status",
|
||||
"test": "vitest run",
|
||||
"validate": "npm run build --silent && node dist/cli.js validate",
|
||||
"templates": "npm run build --silent && node dist/cli.js templates"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"engine_configs/",
|
||||
"agents/base/",
|
||||
"templates/"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Separate thin Node wrappers such as `scripts/init_project.mjs`, `scripts/project_manager.mjs`, and `scripts/validate.mjs` are optional known differences. Prefer package scripts that call the built CLI via `node dist/cli.js`, plus explicit smoke tests for the canonical `open-gamestudio` binary through `npm exec open-gamestudio -- ...` after build/link/install. Do not rely on a bare self-bin name inside the package's own npm scripts before install/link. If wrappers exist, they must call the same command handlers as the canonical CLI and must not fork logic.
|
||||
|
||||
The build config must keep the package bin stable: `tsconfig.json` may typecheck both `src/**/*.ts` and `tests/**/*.ts`, but `tsconfig.build.json` must emit `src/cli.ts` to `dist/cli.js` rather than `dist/src/cli.js`. Relative TypeScript imports must use emitted `.js` specifiers under NodeNext, for example `import { loadConfig } from "./config.js"`.
|
||||
|
||||
Runtime package assets (`engine_configs/`, `agents/base/`, and `templates/`) must resolve from the installed package root via `import.meta.url`-based helpers, not from `process.cwd()`. The package-root helper should walk upward from the current module URL until it finds this package's `package.json`, rather than assuming a fixed relative path from `dist`. Project paths resolve from explicit `--project` or documented current-project cwd behavior.
|
||||
|
||||
Package shipping must be tested, not assumed. `npm pack --json` must include the built CLI and runtime asset directories, and a temporary non-repo cwd install smoke must prove the installed package bin can load templates and engine configs. CLI black-box tests that execute `dist/cli.js` or the package bin must build first so they cannot accidentally pass against stale output or source-only execution.
|
||||
|
||||
## Clean Validation Design
|
||||
|
||||
Validation is a first-class product surface, not a copied legacy behavior.
|
||||
|
||||
`open-gamestudio validate` must:
|
||||
|
||||
- return exit code `0` only when all selected checks pass;
|
||||
- return non-zero when any selected check fails;
|
||||
- print clear failure messages with paths and check names;
|
||||
- avoid false-green behavior where failures are printed but the command succeeds;
|
||||
- validate all upstream parity surfaces before parity is claimed.
|
||||
|
||||
Validation should use a typed internal result shape:
|
||||
|
||||
```ts
|
||||
type CheckStatus = "pass" | "fail" | "skip";
|
||||
|
||||
type ValidationCheck = {
|
||||
id: string;
|
||||
status: CheckStatus;
|
||||
message: string;
|
||||
path?: string;
|
||||
};
|
||||
```
|
||||
|
||||
CLI exit behavior:
|
||||
|
||||
```text
|
||||
any fail -> exit 1
|
||||
no fail -> exit 0
|
||||
skip -> allowed only for explicitly documented non-parity checks
|
||||
```
|
||||
|
||||
Required validation checks:
|
||||
|
||||
- package scripts exist;
|
||||
- TypeScript build output produces `dist/cli.js`, and NodeNext relative imports use `.js` specifiers;
|
||||
- package assets resolve from the installed package root, including subdirectory invocation;
|
||||
- package metadata declares a supported Node runtime, includes runtime assets in the publish set, and `npm pack` plus temp install proves installed-bin asset loading from a non-repo cwd;
|
||||
- all 12 base agents exist;
|
||||
- required templates exist;
|
||||
- Godot, Unity, and Unreal engine configs are valid;
|
||||
- engine aliases are unique and normalize correctly;
|
||||
- generated project config is schema-valid;
|
||||
- active-agent mode selection matches the contract;
|
||||
- engine source root exists under `source/project-<slug>/`;
|
||||
- expected engine project file exists;
|
||||
- project-specific agents are materialized;
|
||||
- project `AGENTS.md` exists, includes provenance markers, and its `source-config-sha256` matches the operational-field-omitting guidance hash;
|
||||
- market overview exists and configured competitor names are preserved in project config;
|
||||
- starter GDD and milestone/timeline artifacts exist, including schema-valid config milestones and timeline document sections;
|
||||
- `status` and `resume` report status without mutating the project;
|
||||
- mutating project-management commands such as `freeze` and `new` are verified only against disposable test fixtures, not by normal `validate --project` on a user project;
|
||||
- CLI/help surfaces do not expose future-only `next`, `--exec`, telemetry, parallel orchestration, or hard ownership enforcement;
|
||||
- validation itself fails hard when a check fails.
|
||||
|
||||
## Parity Acceptance Criteria
|
||||
|
||||
The clean parity contract is satisfied when all are true:
|
||||
|
||||
- TypeScript package builds.
|
||||
- Typecheck passes.
|
||||
- Tests pass.
|
||||
- `open-gamestudio init` can create Godot, Unity, and Unreal projects.
|
||||
- Each generated engine project validates.
|
||||
- All 12 base agents exist and can be materialized for a project.
|
||||
- Materialized prompts include the selected engine's prompt-overlay content, not just generic engine text.
|
||||
- Project-specific `AGENTS.md`, materialized `master_orchestrator`, and handoff guidance are generated without a separate `project_orchestrator.md` file.
|
||||
- Market and analytics templates are present and reachable by agents.
|
||||
- Project management supports status, new/init, resume, and freeze. `menu` and `startover` are intentional omissions.
|
||||
- Package scripts preserve init/manage/test discoverability; separate wrapper files are optional known differences.
|
||||
- Package metadata declares the Node runtime floor, `npm pack` includes `dist/`, `engine_configs/`, `agents/base/`, and `templates/`, and a temp-installed package bin can load those assets from outside the repo.
|
||||
- Validation exits non-zero on failures.
|
||||
- No parity claim appears until the above checks pass.
|
||||
|
||||
---
|
||||
|
||||
# Part 2: Codex-Native Improvements
|
||||
|
||||
These are improvements over upstream that are useful for a Codex-native port, but they should remain bounded and should not crowd out parity work.
|
||||
|
||||
## Canonical TypeScript CLI
|
||||
|
||||
`open-gamestudio` is the canonical public interface. Npm scripts should call into it. Separate script wrappers are optional and should be avoided unless they provide clear compatibility value.
|
||||
|
||||
Recommended core commands:
|
||||
|
||||
```bash
|
||||
open-gamestudio init
|
||||
open-gamestudio status
|
||||
open-gamestudio resume --project projects/my-game
|
||||
open-gamestudio freeze --project projects/my-game
|
||||
open-gamestudio validate
|
||||
open-gamestudio run <agent> --project projects/my-game --task "..."
|
||||
```
|
||||
|
||||
## Codex Runner
|
||||
|
||||
Upstream relies on users manually telling an AI CLI which project and agent files to read. The Codex-native port should add a bounded runner that assembles a structured prompt packet for one agent and one task.
|
||||
|
||||
Default `open-gamestudio run <agent> --project <path> --task <text>` behavior:
|
||||
|
||||
- assemble one structured prompt packet;
|
||||
- write prompt cache and minimal metadata;
|
||||
- print the exact prompt path and next manual/Codex command;
|
||||
- not execute Codex or modify project artifacts beyond the prompt cache.
|
||||
|
||||
`open-gamestudio run` should:
|
||||
|
||||
- load one selected agent;
|
||||
- load the project config summary;
|
||||
- load the selected engine overlay;
|
||||
- load only task-relevant templates;
|
||||
- include explicit output paths;
|
||||
- include a validation command;
|
||||
- write a prompt cache;
|
||||
- print the prompt path and next command for the user.
|
||||
|
||||
Runner flags:
|
||||
|
||||
```bash
|
||||
--print-prompt
|
||||
--dry-run
|
||||
--include-artifact <relative-path>
|
||||
--allow-broad-context
|
||||
```
|
||||
|
||||
Flag semantics:
|
||||
|
||||
- `--print-prompt`: print the deterministic prompt body only.
|
||||
- `--dry-run`: print selected context files, output paths, validation command, prompt cache path, and metadata path without executing Codex.
|
||||
- `--include-artifact <relative-path>`: explicitly include one prior artifact under the project root; reject absolute paths and traversal outside the project.
|
||||
- `--allow-broad-context`: explicitly opt in to broader project context discovery. Without this flag, the runner must not scan or include broad project artifacts.
|
||||
- `--exec`: future-only. Do not implement direct Codex execution until command quoting, working-directory behavior, timeouts, failure handling, and write-scope rules are designed.
|
||||
|
||||
The runner must not load all agents, all templates, or unrelated project artifacts by default.
|
||||
|
||||
Initial runner acceptance guardrails:
|
||||
|
||||
- metadata records `prompt_chars` for every prepared run;
|
||||
- dry-run output lists every included context file;
|
||||
- tests prove a single-agent run does not include unrelated agents;
|
||||
- tests prove a single-agent run does not include all templates;
|
||||
- tests prove named prior artifacts are included only when explicitly requested;
|
||||
- broad project reads require an explicit opt-in flag and are not used by default.
|
||||
|
||||
## Prompt Cache and Minimal Metadata
|
||||
|
||||
Every dry run or printed prompt should write:
|
||||
|
||||
```text
|
||||
projects/<slug>/.gamestudio/runs/<run-id>-<agent>/prompt.md
|
||||
projects/<slug>/.gamestudio/runs/<run-id>-<agent>/metadata.json
|
||||
```
|
||||
|
||||
Minimal metadata is enough:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "...",
|
||||
"project": "projects/my-game",
|
||||
"agent": "market_analyst",
|
||||
"task": "Create the first market overview",
|
||||
"prompt_chars": 12345,
|
||||
"prompt_cache_path": "projects/my-game/.gamestudio/runs/<run-id>-<agent>/prompt.md"
|
||||
}
|
||||
```
|
||||
|
||||
This is not telemetry. Do not add changed-file tracking, runtime metrics, token estimates, productivity comparisons, or JSONL telemetry in the initial implementation.
|
||||
|
||||
## Project-Level AGENTS.md
|
||||
|
||||
Project-level `AGENTS.md` is a Codex-native replacement/addition for upstream's project-specific agent context.
|
||||
|
||||
`src/agents.ts` should own project `AGENTS.md` generation.
|
||||
|
||||
Project `AGENTS.md` must be a compact index and rules file, not a full prompt bundle. It should include project identity, engine/mode, validation commands, pointers to materialized agent prompts, and critical repo-local rules. It must not embed operational status, all agent prompts, all templates, full market docs, or run history. Those belong in config or explicit runner-selected context files.
|
||||
|
||||
Generated files must include provenance markers so validation can prove they came from the generator:
|
||||
|
||||
```md
|
||||
<!-- generated-by: open-gamestudio src/agents.ts schema=1.0 -->
|
||||
<!-- source-config-sha256: <hash> -->
|
||||
```
|
||||
|
||||
The hash is computed from the canonical project-config serialization with operational fields such as `project.status` omitted. Therefore `freeze` may update status without regenerating project `AGENTS.md`; non-operational config changes must stale the hash and fail validation until regeneration.
|
||||
|
||||
## Bounded Context Loading
|
||||
|
||||
The main performance win should come from scoped prompt packets, not from adding a large orchestration system.
|
||||
|
||||
Default context for an agent run:
|
||||
|
||||
- one base/materialized agent;
|
||||
- one project config summary;
|
||||
- one engine overlay;
|
||||
- task-relevant templates;
|
||||
- named prior artifacts only when explicitly requested.
|
||||
|
||||
Broader project reads must be opt-in.
|
||||
|
||||
Bounded-context validation should check the generated prompt packet, not only source code. A regression that accidentally includes all agents, all templates, or broad project artifacts is a performance bug even if functional tests still pass.
|
||||
|
||||
---
|
||||
|
||||
# Part 3: Future Optional Layer
|
||||
|
||||
These features are not present upstream and are not required for the initial implementation. They should stay out of the first build unless explicitly requested later. First-build validation/docs must include explicit absence checks: no `open-gamestudio next`, no `run --exec`, no telemetry command/files, no parallel orchestration surface, and no hard ownership-enforcement behavior.
|
||||
|
||||
## Planner / `next`
|
||||
|
||||
A real `open-gamestudio next` can be useful, but it is easy to overbuild and easy to make stale recommendations.
|
||||
|
||||
Until project state, validation, run metadata, and handoff summaries are mature, the CLI should print simple static next-step suggestions rather than pretending to have a planner.
|
||||
|
||||
Future planner inputs may include:
|
||||
|
||||
- project phase;
|
||||
- missing required artifacts;
|
||||
- last validation status;
|
||||
- run metadata;
|
||||
- handoff summaries;
|
||||
- output ownership.
|
||||
|
||||
## Telemetry
|
||||
|
||||
A future telemetry layer may record:
|
||||
|
||||
- prompt size trends;
|
||||
- estimated token count;
|
||||
- elapsed runtime;
|
||||
- validation results;
|
||||
- changed files;
|
||||
- handoff paths;
|
||||
- suggested next task;
|
||||
- productivity comparisons.
|
||||
|
||||
This is not needed for the initial implementation. Minimal run metadata is enough.
|
||||
|
||||
## Direct Codex Execution
|
||||
|
||||
Future `--exec` support may spawn `codex exec`, but the initial runner should only prepare prompt packets and print commands.
|
||||
|
||||
Before direct execution is added, the design should define:
|
||||
|
||||
- command quoting rules;
|
||||
- working-directory behavior;
|
||||
- failure handling;
|
||||
- timeout behavior;
|
||||
- whether Codex can edit files outside declared output paths.
|
||||
|
||||
## Parallel Orchestration
|
||||
|
||||
Parallel Hermes/subagent execution is a future optimization only.
|
||||
|
||||
It requires mature ownership metadata and validation. Until then, workflows should be serial by default.
|
||||
|
||||
Future parallel execution must require disjoint ownership sets such as:
|
||||
|
||||
```yaml
|
||||
agent: market_analyst
|
||||
may_write:
|
||||
- resources/market-research/**
|
||||
|
||||
agent: data_scientist
|
||||
may_write:
|
||||
- documentation/technical/analytics/**
|
||||
```
|
||||
|
||||
If ownership overlaps, parallel execution must be rejected.
|
||||
|
||||
## Output Ownership Enforcement
|
||||
|
||||
Initial prompts can include suggested output paths, but hard ownership enforcement can wait.
|
||||
|
||||
Future enforcement may validate that generated changes stay inside `may_write` globs and avoid `must_not_write` globs.
|
||||
|
||||
## Performance Optimization Metrics
|
||||
|
||||
Prompt-size budgets, token estimates, artifact summarization, and run-history compression are future optimization work.
|
||||
|
||||
They should be added only after the parity workflow exists and real prompt sizes are measurable.
|
||||
|
||||
---
|
||||
|
||||
# Final Scope Summary
|
||||
|
||||
## Preserve From Upstream Now
|
||||
|
||||
```text
|
||||
Godot / Unity / Unreal
|
||||
12 agents
|
||||
project-specific agent materialization
|
||||
project init
|
||||
market overview/seeds and templates
|
||||
project config and milestones
|
||||
status / new / resume / freeze
|
||||
init / manage / test package discoverability
|
||||
hard-failing validation
|
||||
```
|
||||
|
||||
## Add as Bounded Codex Improvements
|
||||
|
||||
```text
|
||||
canonical open-gamestudio CLI
|
||||
project AGENTS.md
|
||||
bounded run command
|
||||
prompt cache
|
||||
minimal metadata
|
||||
scoped context loading
|
||||
prompt context guardrails
|
||||
```
|
||||
|
||||
## Keep Future-Only
|
||||
|
||||
```text
|
||||
planner / next
|
||||
telemetry
|
||||
changed-file tracking
|
||||
direct Codex execution
|
||||
parallel orchestration
|
||||
ownership enforcement
|
||||
performance optimization metrics
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { agentNames, guidanceConfigHash, type AgentName, type ProjectConfig } from "./config.js";
|
||||
import type { EngineConfigRegistry } from "./engines.js";
|
||||
import { packageAssetPath } from "./paths.js";
|
||||
|
||||
export type MaterializeAgentsInput = {
|
||||
projectRoot: string;
|
||||
config: ProjectConfig;
|
||||
engines: EngineConfigRegistry;
|
||||
};
|
||||
|
||||
export function validateBaseAgents(): string[] {
|
||||
return agentNames.flatMap((agent) => {
|
||||
const file = packageAssetPath(`agents/base/${agent}.md`);
|
||||
if (!existsSync(file)) return [`Missing base agent ${agent}`];
|
||||
const body = readFileSync(file, "utf8");
|
||||
return ["# Role", "# Inputs", "# Outputs", "# Validation", "# Engine Notes", "# Rules"].filter((section) => !sectionHasContent(body, section)).map((section) => `${agent} missing non-empty ${section}`);
|
||||
});
|
||||
}
|
||||
|
||||
function sectionHasContent(body: string, section: string): boolean {
|
||||
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = new RegExp(`^${escaped}\\s*$`, "m").exec(body);
|
||||
if (!match) return false;
|
||||
const start = match.index + match[0].length;
|
||||
const rest = body.slice(start);
|
||||
const nextHeading = rest.search(/^#/m);
|
||||
const content = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
||||
return content.trim().length > 0;
|
||||
}
|
||||
|
||||
export function readAgentPrompt(agent: AgentName, projectRoot?: string): string {
|
||||
const projectPrompt = projectRoot ? path.join(projectRoot, ".gamestudio", "agents", `${agent}.md`) : "";
|
||||
if (projectPrompt && existsSync(projectPrompt)) return readFileSync(projectPrompt, "utf8");
|
||||
return readFileSync(packageAssetPath(`agents/base/${agent}.md`), "utf8");
|
||||
}
|
||||
|
||||
export function generateProjectAgentsMd(config: ProjectConfig): string {
|
||||
const hash = guidanceConfigHash(config);
|
||||
return `<!-- generated-by: open-gamestudio src/agents.ts schema=1.0 -->
|
||||
<!-- source-config-sha256: ${hash} -->
|
||||
# ${config.project.name} Agents
|
||||
|
||||
Project: ${config.project.name}
|
||||
Slug: ${config.project.slug}
|
||||
Engine: ${config.project.engine}
|
||||
Mode: ${config.project.mode}
|
||||
|
||||
# Validation
|
||||
|
||||
Run \`npm run validate -- --project projects/${config.project.slug}\`.
|
||||
|
||||
# Agent Prompts
|
||||
|
||||
${config.team.active_agents.map((agent) => `- ${agent}: .gamestudio/agents/${agent}.md`).join("\n")}
|
||||
|
||||
# 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.
|
||||
`;
|
||||
}
|
||||
|
||||
export function materializeAgents(input: MaterializeAgentsInput): string[] {
|
||||
const config = input.config;
|
||||
const engine = input.engines[config.project.engine];
|
||||
const target = path.join(input.projectRoot, ".gamestudio", "agents");
|
||||
mkdirSync(target, { recursive: true });
|
||||
const written: string[] = [];
|
||||
for (const agent of config.team.active_agents) {
|
||||
const base = readFileSync(packageAssetPath(`agents/base/${agent}.md`), "utf8");
|
||||
const body = `${base}
|
||||
|
||||
# Project Context
|
||||
|
||||
- Name: ${config.project.name}
|
||||
- Concept: ${config.project.concept}
|
||||
- Audience: ${config.project.audience}
|
||||
- Engine: ${engine.display_name} ${config.project.engine_version}
|
||||
- Mode: ${config.project.mode}
|
||||
|
||||
# Engine Overlay
|
||||
|
||||
${Object.values(engine.agent_specializations).join("\n")}
|
||||
`;
|
||||
const file = path.join(target, `${agent}.md`);
|
||||
writeFileSync(file, body);
|
||||
written.push(file);
|
||||
}
|
||||
const agentsMd = path.join(input.projectRoot, "AGENTS.md");
|
||||
writeFileSync(agentsMd, generateProjectAgentsMd(config));
|
||||
written.push(agentsMd);
|
||||
return written;
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from "commander";
|
||||
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";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
function collectCompetitor(value: string, previous: string[] = []): string[] {
|
||||
return [...previous, value.trim()].filter(Boolean);
|
||||
}
|
||||
|
||||
program.name("open-gamestudio").description("Codex-native TypeScript game-studio toolkit").version("0.1.0");
|
||||
|
||||
function addInitCommand(name: "init" | "new"): void {
|
||||
program
|
||||
.command(name)
|
||||
.description(name === "new" ? "Create a new project through the init path" : "Initialize a game project")
|
||||
.requiredOption("--name <name>", "project name")
|
||||
.requiredOption("--engine <engine>", "godot, unity, or unreal")
|
||||
.requiredOption("--mode <mode>", "design, prototype, or development")
|
||||
.option("--concept <text>", "project concept")
|
||||
.option("--genre <text>", "genre")
|
||||
.option("--platform <text>", "platform")
|
||||
.option("--audience <text>", "audience")
|
||||
.option("--competitor <name>", "competitor name; repeat for multiple competitors", collectCompetitor, [])
|
||||
.option("--monetization <text>", "monetization model")
|
||||
.option("--timeline <text>", "timeline")
|
||||
.option("--engine-version <version>", "engine version override")
|
||||
.requiredOption("--non-interactive", "use deterministic defaults")
|
||||
.action((opts) => {
|
||||
const result = initProject({ ...opts, competitors: opts.competitor });
|
||||
console.log(`Created ${result.config.project.name} at ${path.relative(process.cwd(), result.projectRoot)}`);
|
||||
});
|
||||
}
|
||||
|
||||
addInitCommand("init");
|
||||
addInitCommand("new");
|
||||
|
||||
program
|
||||
.command("status")
|
||||
.description("Print project status")
|
||||
.option("--project <path>", "project path")
|
||||
.action((opts) => console.log(statusProject(opts.project)));
|
||||
|
||||
program
|
||||
.command("resume")
|
||||
.description("Print a read-only continuation summary")
|
||||
.requiredOption("--project <path>", "project path")
|
||||
.action((opts) => console.log(resumeProject(opts.project)));
|
||||
|
||||
program
|
||||
.command("freeze")
|
||||
.description("Set project status to frozen")
|
||||
.requiredOption("--project <path>", "project path")
|
||||
.action((opts) => console.log(freezeProject(opts.project)));
|
||||
|
||||
program
|
||||
.command("validate")
|
||||
.description("Run hard-failing repo or project validation")
|
||||
.option("--project <path>", "project path")
|
||||
.action(async (opts) => {
|
||||
const result = await runValidation({ project: opts.project });
|
||||
for (const check of result.checks) {
|
||||
console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}${check.path ? ` (${check.path})` : ""}`);
|
||||
}
|
||||
if (result.failed) process.exitCode = 1;
|
||||
});
|
||||
|
||||
const templates = program.command("templates").description("Discover templates");
|
||||
templates.command("list").description("List template IDs").action(() => {
|
||||
for (const info of listTemplates()) console.log(`${info.id}\t${info.category}\t${info.path}`);
|
||||
});
|
||||
templates
|
||||
.command("show")
|
||||
.description("Show a template")
|
||||
.argument("<template-id>")
|
||||
.action((id: TemplateId) => {
|
||||
if (!templateRegistry[id]) throw new Error(`Unknown template "${id}"`);
|
||||
console.log(formatTemplateShow(id));
|
||||
});
|
||||
|
||||
program
|
||||
.command("run")
|
||||
.description("Prepare one bounded prompt packet for a project agent")
|
||||
.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("--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) => {
|
||||
const result = prepareRun(agent, {
|
||||
project: opts.project,
|
||||
task: opts.task,
|
||||
printPrompt: opts.printPrompt,
|
||||
dryRun: opts.dryRun,
|
||||
includeArtifact: opts.includeArtifact,
|
||||
allowBroadContext: opts.allowBroadContext
|
||||
});
|
||||
console.log(result.output);
|
||||
});
|
||||
|
||||
program.parseAsync().catch((error: unknown) => {
|
||||
console.error((error as Error).message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { z } from "zod";
|
||||
|
||||
export const agentNames = [
|
||||
"master_orchestrator",
|
||||
"producer_agent",
|
||||
"market_analyst",
|
||||
"data_scientist",
|
||||
"sr_game_designer",
|
||||
"mid_game_designer",
|
||||
"mechanics_developer",
|
||||
"game_feel_developer",
|
||||
"sr_game_artist",
|
||||
"technical_artist",
|
||||
"ui_ux_agent",
|
||||
"qa_agent"
|
||||
] as const;
|
||||
|
||||
export const modeSchema = z.enum(["design", "prototype", "development"]);
|
||||
export const agentNameSchema = z.enum(agentNames);
|
||||
export type AgentName = z.infer<typeof agentNameSchema>;
|
||||
export type ProjectMode = z.infer<typeof modeSchema>;
|
||||
|
||||
export const milestoneSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
target: z.string().min(1),
|
||||
exit_criteria: z.array(z.string().min(1)).min(1),
|
||||
status: z.enum(["planned", "active", "complete", "blocked"]).default("planned")
|
||||
});
|
||||
|
||||
export const projectConfigSchema = z.object({
|
||||
schema_version: z.literal("1.0"),
|
||||
project: z.object({
|
||||
name: z.string().min(1),
|
||||
slug: z.string().min(1),
|
||||
concept: z.string().min(1),
|
||||
genre: z.string().min(1),
|
||||
platform: z.string().min(1),
|
||||
audience: z.string().min(1),
|
||||
competitors: z.array(z.string().min(1)),
|
||||
monetization: z.string().min(1),
|
||||
timeline: z.string().min(1),
|
||||
engine: z.enum(["godot", "unity", "unreal"]),
|
||||
engine_version: z.string().min(1),
|
||||
mode: modeSchema,
|
||||
phase: z.string().min(1),
|
||||
status: z.enum(["active", "frozen", "inactive"])
|
||||
}),
|
||||
team: z.object({
|
||||
active_agents: z.array(agentNameSchema).min(1)
|
||||
}),
|
||||
production: z.object({
|
||||
milestones: z.array(milestoneSchema).min(1)
|
||||
})
|
||||
});
|
||||
|
||||
export type ProjectConfig = z.infer<typeof projectConfigSchema>;
|
||||
|
||||
export function slugify(value: string): string {
|
||||
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
if (!slug) throw new Error(`Cannot create slug from "${value}"`);
|
||||
return slug;
|
||||
}
|
||||
|
||||
export function activeAgentsForMode(mode: ProjectMode): AgentName[] {
|
||||
const always: AgentName[] = ["master_orchestrator", "producer_agent", "market_analyst", "data_scientist"];
|
||||
const byMode: Record<ProjectMode, AgentName[]> = {
|
||||
design: ["sr_game_designer", "mid_game_designer", "sr_game_artist"],
|
||||
prototype: ["sr_game_designer", "mechanics_developer", "qa_agent"],
|
||||
development: [
|
||||
"sr_game_designer",
|
||||
"mid_game_designer",
|
||||
"mechanics_developer",
|
||||
"game_feel_developer",
|
||||
"qa_agent",
|
||||
"sr_game_artist",
|
||||
"technical_artist",
|
||||
"ui_ux_agent"
|
||||
]
|
||||
};
|
||||
return [...always, ...byMode[mode]];
|
||||
}
|
||||
|
||||
function ordered(value: unknown, omitOperationalFields: boolean): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => ordered(item, omitOperationalFields));
|
||||
if (value && typeof value === "object") {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
||||
if (omitOperationalFields && key === "status") continue;
|
||||
if (omitOperationalFields && /timestamp|run_state|validation/i.test(key)) continue;
|
||||
result[key] = ordered((value as Record<string, unknown>)[key], omitOperationalFields);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function canonicalProjectConfigJson(
|
||||
config: ProjectConfig,
|
||||
options: { omitOperationalFields?: boolean } = {}
|
||||
): string {
|
||||
return `${JSON.stringify(ordered(config, options.omitOperationalFields ?? false), null, 2)}\n`;
|
||||
}
|
||||
|
||||
export function guidanceConfigHash(config: ProjectConfig): string {
|
||||
return createHash("sha256")
|
||||
.update(canonicalProjectConfigJson(config, { omitOperationalFields: true }))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
export function readProjectConfig(filePath: string): ProjectConfig {
|
||||
return projectConfigSchema.parse(JSON.parse(readFileSync(filePath, "utf8")));
|
||||
}
|
||||
|
||||
export function writeProjectConfig(filePath: string, config: ProjectConfig): void {
|
||||
writeFileSync(filePath, canonicalProjectConfigJson(projectConfigSchema.parse(config)));
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
export type EngineId = "godot" | "unity" | "unreal";
|
||||
|
||||
const engineConfigSchema = z.object({
|
||||
id: z.enum(["godot", "unity", "unreal"]),
|
||||
display_name: z.string(),
|
||||
aliases: z.array(z.string()).min(1),
|
||||
default_version: z.string(),
|
||||
source_root_pattern: z.literal("source/project-{slug}"),
|
||||
folders: z.array(z.string()),
|
||||
project_files: z.array(z.string()),
|
||||
best_practices: z.array(z.string()),
|
||||
agent_specializations: z.record(z.string())
|
||||
});
|
||||
|
||||
export type EngineConfig = z.infer<typeof engineConfigSchema>;
|
||||
export type EngineConfigRegistry = Record<EngineId, EngineConfig>;
|
||||
export type EngineCreateInput = {
|
||||
projectRoot: string;
|
||||
projectSlug: string;
|
||||
projectName: string;
|
||||
engine: EngineId;
|
||||
registry: EngineConfigRegistry;
|
||||
engineVersion?: string;
|
||||
};
|
||||
|
||||
export function loadEngineConfigs(configDir: string): EngineConfigRegistry {
|
||||
const entries = ["godot", "unity", "unreal"] as const;
|
||||
const configs = Object.fromEntries(
|
||||
entries.map((id) => [id, engineConfigSchema.parse(JSON.parse(readFileSync(path.join(configDir, `${id}.json`), "utf8")))])
|
||||
) as EngineConfigRegistry;
|
||||
return configs;
|
||||
}
|
||||
|
||||
export function normalizeEngine(value: string, registry: EngineConfigRegistry): EngineId {
|
||||
const wanted = value.trim().toLowerCase();
|
||||
for (const [id, config] of Object.entries(registry) as [EngineId, EngineConfig][]) {
|
||||
if (id === wanted || config.display_name.toLowerCase() === wanted || config.aliases.some((a) => a.toLowerCase() === wanted)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
throw new Error(`Unknown engine "${value}". Expected one of: godot, unity, unreal`);
|
||||
}
|
||||
|
||||
export function sourceRoot(projectRoot: string, projectSlug: string): string {
|
||||
return path.join(projectRoot, "source", `project-${projectSlug}`);
|
||||
}
|
||||
|
||||
export function projectClassName(displayNameOrSlug: string): string {
|
||||
const words = displayNameOrSlug.match(/[A-Za-z0-9]+/g);
|
||||
if (!words) throw new Error(`Cannot create project class name from "${displayNameOrSlug}"`);
|
||||
let result = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
||||
if (/^\d/.test(result)) result = `Game${result}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function unrealProjectFileName(displayNameOrSlug: string): string {
|
||||
return `${projectClassName(displayNameOrSlug)}.uproject`;
|
||||
}
|
||||
|
||||
export function createEngineFolders(input: EngineCreateInput): string[] {
|
||||
const config = input.registry[input.engine];
|
||||
if (!config) throw new Error(`Unknown engine "${input.engine}"`);
|
||||
const root = sourceRoot(input.projectRoot, input.projectSlug);
|
||||
const created = [root, ...config.folders.map((folder) => path.join(root, folder))];
|
||||
for (const folder of created) mkdirSync(folder, { recursive: true });
|
||||
return created;
|
||||
}
|
||||
|
||||
export function createEngineProjectFiles(input: EngineCreateInput): string[] {
|
||||
const root = sourceRoot(input.projectRoot, input.projectSlug);
|
||||
const files: string[] = [];
|
||||
if (input.engine === "godot") {
|
||||
const file = path.join(root, "project.godot");
|
||||
writeFileSync(file, `; Engine configuration file.\nconfig/name="${input.projectName}"\n`);
|
||||
files.push(file);
|
||||
} else if (input.engine === "unity") {
|
||||
const manifest = path.join(root, "Packages", "manifest.json");
|
||||
const settings = path.join(root, "ProjectSettings", "ProjectSettings.asset");
|
||||
mkdirSync(path.dirname(manifest), { recursive: true });
|
||||
mkdirSync(path.dirname(settings), { recursive: true });
|
||||
writeFileSync(manifest, `${JSON.stringify({ dependencies: {} }, null, 2)}\n`);
|
||||
writeFileSync(settings, `%YAML 1.1\nProjectSettings:\n productName: ${input.projectName}\n`);
|
||||
files.push(manifest, settings);
|
||||
} else if (input.engine === "unreal") {
|
||||
const file = path.join(root, unrealProjectFileName(input.projectName));
|
||||
writeFileSync(file, `${JSON.stringify({ FileVersion: 3, EngineAssociation: input.engineVersion ?? input.registry.unreal.default_version }, null, 2)}\n`);
|
||||
files.push(file);
|
||||
} else {
|
||||
throw new Error(`Unknown engine "${input.engine}"`);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
export { engineConfigSchema };
|
||||
@@ -0,0 +1,30 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export function packageRoot(metaUrl: string = import.meta.url): string {
|
||||
let current = path.dirname(fileURLToPath(metaUrl));
|
||||
while (true) {
|
||||
const manifest = path.join(current, "package.json");
|
||||
if (existsSync(manifest)) {
|
||||
const parsed = JSON.parse(readFileSync(manifest, "utf8")) as { name?: string };
|
||||
if (parsed.name === "open-gamestudio") return current;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
throw new Error("Could not find open-gamestudio package root");
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export function packageAssetPath(relativePath: string): string {
|
||||
if (path.isAbsolute(relativePath) || relativePath.includes("..")) {
|
||||
throw new Error(`Package asset path must be relative and contained: ${relativePath}`);
|
||||
}
|
||||
return path.join(packageRoot(import.meta.url), relativePath);
|
||||
}
|
||||
|
||||
export function resolveProjectRoot(input?: string, cwd: string = process.cwd()): string {
|
||||
return path.resolve(cwd, input ?? ".");
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { activeAgentsForMode, readProjectConfig, slugify, writeProjectConfig, type ProjectConfig, type ProjectMode } from "./config.js";
|
||||
import { createEngineFolders, createEngineProjectFiles, loadEngineConfigs, normalizeEngine, projectClassName, sourceRoot, unrealProjectFileName } from "./engines.js";
|
||||
import { materializeAgents } from "./agents.js";
|
||||
import { packageAssetPath, resolveProjectRoot } from "./paths.js";
|
||||
|
||||
export type InitProjectOptions = {
|
||||
name: string;
|
||||
engine: string;
|
||||
mode?: ProjectMode;
|
||||
concept?: string;
|
||||
genre?: string;
|
||||
platform?: string;
|
||||
audience?: string;
|
||||
competitors?: string[];
|
||||
monetization?: string;
|
||||
timeline?: string;
|
||||
engineVersion?: string;
|
||||
nonInteractive?: boolean;
|
||||
};
|
||||
|
||||
export function defaultProjectConfig(options: InitProjectOptions): ProjectConfig {
|
||||
const engines = loadEngineConfigs(packageAssetPath("engine_configs"));
|
||||
const engine = normalizeEngine(options.engine, engines);
|
||||
if (!options.nonInteractive) throw new Error("init requires --non-interactive");
|
||||
if (!options.mode) throw new Error("init requires --mode");
|
||||
const mode = options.mode;
|
||||
const slug = slugify(options.name);
|
||||
return {
|
||||
schema_version: "1.0",
|
||||
project: {
|
||||
name: options.name,
|
||||
slug,
|
||||
concept: options.concept ?? `${options.name} concept`,
|
||||
genre: options.genre ?? "Unspecified",
|
||||
platform: options.platform ?? "PC",
|
||||
audience: options.audience ?? "General players",
|
||||
competitors: options.competitors ?? [],
|
||||
monetization: options.monetization ?? "undecided",
|
||||
timeline: options.timeline ?? "TBD",
|
||||
engine,
|
||||
engine_version: options.engineVersion ?? engines[engine].default_version,
|
||||
mode,
|
||||
phase: "Initialization",
|
||||
status: "active"
|
||||
},
|
||||
team: { active_agents: activeAgentsForMode(mode) },
|
||||
production: {
|
||||
milestones: [
|
||||
{
|
||||
id: "m1",
|
||||
title: "Playable prototype",
|
||||
target: "Week 4",
|
||||
exit_criteria: ["Core loop is playable"],
|
||||
status: "planned"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function writeStarterDocs(projectRoot: string, config: ProjectConfig): void {
|
||||
mkdirSync(path.join(projectRoot, "documentation", "design"), { recursive: true });
|
||||
mkdirSync(path.join(projectRoot, "documentation", "production"), { recursive: true });
|
||||
mkdirSync(path.join(projectRoot, "resources", "market-research"), { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(projectRoot, "documentation", "design", "gdd.md"),
|
||||
`# ${config.project.name} GDD\n\n# Purpose\n\n${config.project.concept}\n\n# Core Loop\n\nDefine and validate the playable loop.\n\n# Validation\n\nRun \`npm run validate -- --project projects/${config.project.slug}\`.\n`
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(projectRoot, "documentation", "production", "timeline.md"),
|
||||
`# Timeline\n\n${config.project.timeline}\n\n# Milestones\n\n${config.production.milestones.map((m) => `- ${m.id}: ${m.title} (${m.target})`).join("\n")}\n\n# Risks\n\n- Scope may exceed the first validation gate.\n\n# Next Validation Gate\n\nRun project validation after first playable setup.\n`
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(projectRoot, "resources", "market-research", "market-overview.md"),
|
||||
`# Market Overview\n\nAudience: ${config.project.audience}\n\nCompetitors: ${config.project.competitors.join(", ")}\n\nThis is a seed, not a full competitor report.\n`
|
||||
);
|
||||
}
|
||||
|
||||
function assertNoSameParentCollision(parent: string, config: ProjectConfig): void {
|
||||
if (!existsSync(parent)) return;
|
||||
const nextClass = projectClassName(config.project.name);
|
||||
for (const entry of readdirSync(parent, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const configPath = path.join(parent, entry.name, "project-config.json");
|
||||
if (!existsSync(configPath)) continue;
|
||||
const existing = readProjectConfig(configPath);
|
||||
if (existing.project.name === config.project.name) continue;
|
||||
if (existing.project.slug === config.project.slug) {
|
||||
throw new Error(`Project name "${config.project.name}" collides with existing slug "${existing.project.slug}" in ${parent}`);
|
||||
}
|
||||
if (existing.project.engine === "unreal" || config.project.engine === "unreal") {
|
||||
const existingClass = projectClassName(existing.project.name);
|
||||
if (existingClass === nextClass) {
|
||||
throw new Error(`Project name "${config.project.name}" collides with existing Unreal class name "${existingClass}" in ${parent}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initProject(options: InitProjectOptions, cwd = process.cwd()): { projectRoot: string; config: ProjectConfig } {
|
||||
const config = defaultProjectConfig(options);
|
||||
const projectRoot = path.resolve(cwd, path.join("projects", config.project.slug));
|
||||
if (existsSync(projectRoot)) throw new Error(`Project path already exists or collides: ${projectRoot}`);
|
||||
assertNoSameParentCollision(path.dirname(projectRoot), config);
|
||||
const engines = loadEngineConfigs(packageAssetPath("engine_configs"));
|
||||
mkdirSync(projectRoot, { recursive: true });
|
||||
createEngineFolders({ projectRoot, projectSlug: config.project.slug, projectName: config.project.name, engine: config.project.engine, registry: engines });
|
||||
createEngineProjectFiles({ projectRoot, projectSlug: config.project.slug, projectName: config.project.name, engine: config.project.engine, registry: engines, engineVersion: config.project.engine_version });
|
||||
writeProjectConfig(path.join(projectRoot, "project-config.json"), config);
|
||||
writeStarterDocs(projectRoot, config);
|
||||
materializeAgents({ projectRoot, config, engines });
|
||||
return { projectRoot, config };
|
||||
}
|
||||
|
||||
export function statusProject(project?: string, cwd = process.cwd()): string {
|
||||
const root = resolveProjectRoot(project, cwd);
|
||||
const config = readProjectConfig(path.join(root, "project-config.json"));
|
||||
return [
|
||||
`${config.project.name}`,
|
||||
`phase: ${config.project.phase}`,
|
||||
`status: ${config.project.status}`,
|
||||
`mode: ${config.project.mode}`,
|
||||
`engine: ${config.project.engine}`,
|
||||
`active agents: ${config.team.active_agents.join(", ")}`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function resumeProject(project?: string, cwd = process.cwd()): string {
|
||||
const root = resolveProjectRoot(project, cwd);
|
||||
const config = readProjectConfig(path.join(root, "project-config.json"));
|
||||
return `Resume ${config.project.name}\nphase: ${config.project.phase}\nstatus: ${config.project.status}\nSuggested next command: npm exec open-gamestudio -- run producer_agent --project ${path.relative(cwd, root) || "."} --task "Summarize current project state"`;
|
||||
}
|
||||
|
||||
export function freezeProject(project?: string, cwd = process.cwd()): string {
|
||||
const root = resolveProjectRoot(project, cwd);
|
||||
const file = path.join(root, "project-config.json");
|
||||
const config = readProjectConfig(file);
|
||||
config.project.status = "frozen";
|
||||
writeProjectConfig(file, config);
|
||||
return `Frozen ${config.project.name}`;
|
||||
}
|
||||
|
||||
export function expectedEngineProjectFile(projectRoot: string, config: ProjectConfig): string {
|
||||
const root = sourceRoot(projectRoot, config.project.slug);
|
||||
if (config.project.engine === "godot") return path.join(root, "project.godot");
|
||||
if (config.project.engine === "unity") return path.join(root, "Packages", "manifest.json");
|
||||
return path.join(root, unrealProjectFileName(config.project.name));
|
||||
}
|
||||
|
||||
export function readFileIfExists(file: string): string | undefined {
|
||||
return existsSync(file) ? readFileSync(file, "utf8") : undefined;
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { agentNameSchema, readProjectConfig, type AgentName } from "./config.js";
|
||||
import { readAgentPrompt } from "./agents.js";
|
||||
import { loadEngineConfigs } from "./engines.js";
|
||||
import { packageAssetPath, resolveProjectRoot } from "./paths.js";
|
||||
import { readTemplate, selectTemplates } from "./templates.js";
|
||||
|
||||
export type RunOptions = {
|
||||
project: string;
|
||||
task: string;
|
||||
printPrompt?: boolean;
|
||||
dryRun?: boolean;
|
||||
includeArtifact?: string[];
|
||||
allowBroadContext?: boolean;
|
||||
};
|
||||
|
||||
export type PreparedRun = {
|
||||
prompt: string;
|
||||
promptPath: string;
|
||||
metadataPath: string;
|
||||
contextFiles: string[];
|
||||
output: string;
|
||||
};
|
||||
|
||||
function requireTask(task: string): string {
|
||||
if (!task || !task.trim()) throw new Error("--task is required and must be non-empty");
|
||||
return task.trim();
|
||||
}
|
||||
|
||||
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);
|
||||
if (!full.startsWith(`${projectRoot}${path.sep}`)) throw new Error("--include-artifact cannot escape the project root");
|
||||
const realRoot = realpathSync(projectRoot);
|
||||
const realFull = realpathSync(full);
|
||||
if (realFull !== realRoot && !realFull.startsWith(`${realRoot}${path.sep}`)) throw new Error("--include-artifact cannot escape the project root");
|
||||
return full;
|
||||
}
|
||||
|
||||
let runSequence = 0;
|
||||
|
||||
export function prepareRun(agentInput: string, options: RunOptions, cwd = process.cwd()): PreparedRun {
|
||||
const agent = agentNameSchema.parse(agentInput) as AgentName;
|
||||
const task = requireTask(options.task);
|
||||
const projectRoot = resolveProjectRoot(options.project, cwd);
|
||||
const config = readProjectConfig(path.join(projectRoot, "project-config.json"));
|
||||
const engines = loadEngineConfigs(packageAssetPath("engine_configs"));
|
||||
const engine = engines[config.project.engine];
|
||||
const templates = selectTemplates(agent, task);
|
||||
const contextFiles = [
|
||||
path.relative(projectRoot, path.join(projectRoot, ".gamestudio", "agents", `${agent}.md`)),
|
||||
"project-config.json",
|
||||
`engine_configs/${config.project.engine}.json`,
|
||||
...templates.map((id) => `templates/${id}`)
|
||||
];
|
||||
const artifactBodies = (options.includeArtifact ?? []).map((artifact) => {
|
||||
const full = safeArtifact(projectRoot, artifact);
|
||||
contextFiles.push(artifact);
|
||||
return `# Included Artifact: ${artifact}\n\n${readFileSync(full, "utf8")}`;
|
||||
});
|
||||
const templateBodies = templates.map((id) => `# Template: ${id}\n\n${readTemplate(id)}`).join("\n\n");
|
||||
const outputPaths = [
|
||||
agent === "market_analyst" ? "resources/market-research/market-analysis.md" : undefined,
|
||||
agent === "data_scientist" ? "documentation/technical/analytics/analytics-plan.md" : undefined,
|
||||
agent === "qa_agent" ? "documentation/qa/validation-review.md" : undefined
|
||||
].filter(Boolean);
|
||||
const prompt = [
|
||||
`# Open GameStudio Prompt`,
|
||||
`Agent: ${agent}`,
|
||||
`Task: ${task}`,
|
||||
`Project: ${config.project.name} (${config.project.slug})`,
|
||||
`Engine: ${engine.display_name} ${config.project.engine_version}`,
|
||||
`Validation: npm run validate -- --project ${path.relative(cwd, projectRoot) || "."}`,
|
||||
"",
|
||||
"# Agent Prompt",
|
||||
readAgentPrompt(agent, projectRoot),
|
||||
"",
|
||||
"# Project Summary",
|
||||
`Concept: ${config.project.concept}`,
|
||||
`Audience: ${config.project.audience}`,
|
||||
`Competitors: ${config.project.competitors.join(", ")}`,
|
||||
"",
|
||||
"# Engine Overlay",
|
||||
Object.values(engine.agent_specializations).join("\n"),
|
||||
"",
|
||||
templateBodies,
|
||||
...artifactBodies,
|
||||
"",
|
||||
"# Output Paths",
|
||||
outputPaths.length ? outputPaths.map((p) => `- ${p}`).join("\n") : "- Use the role prompt output path conventions.",
|
||||
options.allowBroadContext ? "\n# Broad Context\nExplicit broad context opt-in was provided." : ""
|
||||
].join("\n");
|
||||
const runId = `${new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 17)}-${process.pid}-${++runSequence}`;
|
||||
const runDir = path.join(projectRoot, ".gamestudio", "runs", `${runId}-${agent}`);
|
||||
mkdirSync(runDir, { recursive: true });
|
||||
const promptPath = path.join(runDir, "prompt.md");
|
||||
const metadataPath = path.join(runDir, "metadata.json");
|
||||
writeFileSync(promptPath, prompt);
|
||||
writeFileSync(
|
||||
metadataPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
project: path.relative(cwd, projectRoot) || ".",
|
||||
agent,
|
||||
task,
|
||||
prompt_chars: prompt.length,
|
||||
prompt_cache_path: path.relative(cwd, promptPath)
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
);
|
||||
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 };
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { packageAssetPath } from "./paths.js";
|
||||
import type { AgentName } from "./config.js";
|
||||
|
||||
export type TemplateId =
|
||||
| "gdd"
|
||||
| "feature_spec"
|
||||
| "handoff"
|
||||
| "analytics_setup"
|
||||
| "engine_setup"
|
||||
| "market_analysis"
|
||||
| "project_config";
|
||||
|
||||
export type TemplateInfo = {
|
||||
id: TemplateId;
|
||||
category: string;
|
||||
path: string;
|
||||
roles: AgentName[];
|
||||
tags: string[];
|
||||
requiredSections: string[];
|
||||
};
|
||||
|
||||
export const templateRegistry: Record<TemplateId, TemplateInfo> = {
|
||||
gdd: {
|
||||
id: "gdd",
|
||||
category: "design",
|
||||
path: "templates/gdd_template.md",
|
||||
roles: ["sr_game_designer", "mid_game_designer"],
|
||||
tags: ["design", "gdd"],
|
||||
requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"]
|
||||
},
|
||||
feature_spec: {
|
||||
id: "feature_spec",
|
||||
category: "design",
|
||||
path: "templates/feature_spec_template.md",
|
||||
roles: ["sr_game_designer", "mid_game_designer", "mechanics_developer"],
|
||||
tags: ["feature", "spec", "design"],
|
||||
requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"]
|
||||
},
|
||||
handoff: {
|
||||
id: "handoff",
|
||||
category: "coordination",
|
||||
path: "templates/handoff_template.md",
|
||||
roles: ["master_orchestrator", "producer_agent"],
|
||||
tags: ["handoff", "coordination"],
|
||||
requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"]
|
||||
},
|
||||
analytics_setup: {
|
||||
id: "analytics_setup",
|
||||
category: "analytics",
|
||||
path: "templates/analytics_setup_template.md",
|
||||
roles: ["data_scientist"],
|
||||
tags: ["analytics", "metrics"],
|
||||
requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"]
|
||||
},
|
||||
engine_setup: {
|
||||
id: "engine_setup",
|
||||
category: "engine",
|
||||
path: "templates/engine_setup_template.md",
|
||||
roles: ["mechanics_developer", "technical_artist"],
|
||||
tags: ["engine", "setup"],
|
||||
requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"]
|
||||
},
|
||||
market_analysis: {
|
||||
id: "market_analysis",
|
||||
category: "market",
|
||||
path: "templates/market_analysis_template.md",
|
||||
roles: ["market_analyst"],
|
||||
tags: ["market", "competitors"],
|
||||
requiredSections: ["# Purpose", "# Inputs", "# Outputs", "# Validation"]
|
||||
},
|
||||
project_config: {
|
||||
id: "project_config",
|
||||
category: "config",
|
||||
path: "templates/project_config_template.json",
|
||||
roles: ["producer_agent", "master_orchestrator"],
|
||||
tags: ["config", "setup"],
|
||||
requiredSections: []
|
||||
}
|
||||
};
|
||||
|
||||
export function listTemplates(): TemplateInfo[] {
|
||||
return Object.values(templateRegistry);
|
||||
}
|
||||
|
||||
export function readTemplate(id: TemplateId): string {
|
||||
const info = templateRegistry[id];
|
||||
if (!info) throw new Error(`Unknown template "${id}"`);
|
||||
return readFileSync(packageAssetPath(info.path), "utf8");
|
||||
}
|
||||
|
||||
export function formatTemplateShow(id: TemplateId): string {
|
||||
const info = templateRegistry[id];
|
||||
if (!info) throw new Error(`Unknown template "${id}"`);
|
||||
return [
|
||||
`ID: ${info.id}`,
|
||||
`Category: ${info.category}`,
|
||||
`Path: ${info.path}`,
|
||||
`Roles: ${info.roles.join(", ")}`,
|
||||
`Tags: ${info.tags.join(", ")}`,
|
||||
"",
|
||||
readTemplate(id)
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function templatePath(id: TemplateId): string {
|
||||
return packageAssetPath(templateRegistry[id].path);
|
||||
}
|
||||
|
||||
function sectionHasContent(body: string, section: string): boolean {
|
||||
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = new RegExp(`^${escaped}\\s*$`, "m").exec(body);
|
||||
if (!match) return false;
|
||||
const start = match.index + match[0].length;
|
||||
const rest = body.slice(start);
|
||||
const nextHeading = rest.search(/^#/m);
|
||||
const content = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
||||
return content.trim().length > 0;
|
||||
}
|
||||
|
||||
export function validateTemplateFiles(): string[] {
|
||||
const failures: string[] = [];
|
||||
for (const info of listTemplates()) {
|
||||
const fullPath = path.resolve(packageAssetPath(info.path));
|
||||
if (!existsSync(fullPath)) {
|
||||
failures.push(`Missing template ${info.id}: ${info.path}`);
|
||||
continue;
|
||||
}
|
||||
const body = readFileSync(fullPath, "utf8");
|
||||
for (const section of info.requiredSections) {
|
||||
if (!sectionHasContent(body, section)) failures.push(`Template ${info.id} missing non-empty ${section}`);
|
||||
}
|
||||
if (info.id === "project_config") JSON.parse(body);
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
export function selectTemplates(agent: AgentName, task: string): TemplateId[] {
|
||||
const lower = task.toLowerCase();
|
||||
const selected = new Set<TemplateId>();
|
||||
if (/(handoff|coordination|coordinate)/.test(lower)) selected.add("handoff");
|
||||
if (agent === "market_analyst") selected.add("market_analysis");
|
||||
if (agent === "data_scientist") selected.add("analytics_setup");
|
||||
if ((agent === "sr_game_designer" || agent === "mid_game_designer") && /(design|spec|gdd|feature)/.test(lower)) {
|
||||
selected.add("gdd");
|
||||
selected.add("feature_spec");
|
||||
}
|
||||
if (/(engine|setup|project config|initialize|init)/.test(lower)) {
|
||||
selected.add("engine_setup");
|
||||
selected.add("project_config");
|
||||
}
|
||||
if (agent === "qa_agent" && /(spec review|review spec)/.test(lower)) selected.add("feature_spec");
|
||||
return [...selected];
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, unlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { agentNames, activeAgentsForMode, guidanceConfigHash, readProjectConfig } from "./config.js";
|
||||
import { validateBaseAgents } from "./agents.js";
|
||||
import { loadEngineConfigs, normalizeEngine, sourceRoot } from "./engines.js";
|
||||
import { packageAssetPath } from "./paths.js";
|
||||
import { expectedEngineProjectFile, resumeProject, statusProject } from "./projects.js";
|
||||
import { templateRegistry, validateTemplateFiles } from "./templates.js";
|
||||
|
||||
export type CheckStatus = "pass" | "fail" | "skip";
|
||||
export type ValidationCheck = { id: string; status: CheckStatus; message: string; path?: string };
|
||||
|
||||
function pass(id: string, message: string, file?: string): ValidationCheck {
|
||||
return { id, status: "pass", message, path: file };
|
||||
}
|
||||
|
||||
function fail(id: string, message: string, file?: string): ValidationCheck {
|
||||
return { id, status: "fail", message, path: file };
|
||||
}
|
||||
|
||||
function sectionHasContent(body: string, section: string): boolean {
|
||||
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = new RegExp(`^${escaped}\\s*$`, "m").exec(body);
|
||||
if (!match) return false;
|
||||
const start = match.index + match[0].length;
|
||||
const rest = body.slice(start);
|
||||
const nextHeading = rest.search(/^#/m);
|
||||
const content = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
||||
return content.trim().length > 0;
|
||||
}
|
||||
|
||||
const requiredAgentSections = ["# Role", "# Inputs", "# Outputs", "# Validation", "# Engine Notes", "# Rules"];
|
||||
|
||||
export async function validateRepo(root = process.cwd()): Promise<ValidationCheck[]> {
|
||||
const checks: ValidationCheck[] = [];
|
||||
const pkgPath = path.join(root, "package.json");
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as {
|
||||
scripts?: Record<string, string>;
|
||||
bin?: Record<string, string>;
|
||||
files?: string[];
|
||||
engines?: { node?: string };
|
||||
};
|
||||
const scripts = pkg.scripts ?? {};
|
||||
for (const script of ["init", "manage", "test", "validate", "templates"]) {
|
||||
checks.push(scripts[script] ? pass(`package.script.${script}`, `script ${script} exists`) : fail(`package.script.${script}`, `missing script ${script}`, pkgPath));
|
||||
}
|
||||
checks.push(scripts.build === "tsc -p tsconfig.build.json" ? pass("package.build", "build uses tsconfig.build.json") : fail("package.build", "build must use tsconfig.build.json", pkgPath));
|
||||
checks.push(pkg.bin?.["open-gamestudio"] === "./dist/cli.js" ? pass("package.bin", "bin points to dist/cli.js") : fail("package.bin", "bin must point to ./dist/cli.js", pkgPath));
|
||||
checks.push(pkg.engines?.node?.includes(">=20") ? pass("package.node", "node floor declared") : fail("package.node", "node >=20 must be declared", pkgPath));
|
||||
for (const file of ["dist/", "engine_configs/", "agents/base/", "templates/"]) {
|
||||
checks.push(pkg.files?.includes(file) ? pass(`package.files.${file}`, `${file} shipped`) : fail(`package.files.${file}`, `${file} missing from package files`, pkgPath));
|
||||
}
|
||||
for (const file of ["src/cli.ts", "src/paths.ts", "src/config.ts", "src/engines.ts", "src/templates.ts", "src/agents.ts", "src/projects.ts", "src/runner.ts", "src/validation.ts"]) {
|
||||
checks.push(existsSync(path.join(root, file)) ? pass(`source.${file}`, `${file} exists`) : fail(`source.${file}`, `${file} missing`, file));
|
||||
}
|
||||
const tsFiles = ["cli", "config", "engines", "templates", "agents", "projects", "runner", "validation", "paths"].map((f) => path.join(root, "src", `${f}.ts`));
|
||||
for (const file of tsFiles.filter(existsSync)) {
|
||||
const body = readFileSync(file, "utf8");
|
||||
const bad = body.match(/from "\.\/(?!.*\.js")/);
|
||||
if (bad) checks.push(fail("typescript.imports", `relative import missing .js in ${file}`, file));
|
||||
}
|
||||
const engines = loadEngineConfigs(packageAssetPath("engine_configs"));
|
||||
for (const value of ["Godot", "Unity", "Unreal", "Unreal Engine", "ue5"]) {
|
||||
try {
|
||||
normalizeEngine(value, engines);
|
||||
checks.push(pass(`engine.alias.${value}`, `${value} normalizes`));
|
||||
} catch (error) {
|
||||
checks.push(fail(`engine.alias.${value}`, (error as Error).message));
|
||||
}
|
||||
}
|
||||
checks.push(...validateBaseAgents().map((message) => fail("agents.base", message)));
|
||||
if (validateBaseAgents().length === 0) checks.push(pass("agents.base", "all 12 base agents exist"));
|
||||
const templateFailures = validateTemplateFiles();
|
||||
checks.push(...templateFailures.map((message) => fail("templates", message)));
|
||||
if (templateFailures.length === 0 && Object.keys(templateRegistry).length === 7) checks.push(pass("templates", "all templates exist"));
|
||||
checks.push(existsSync(path.join(root, "dist", "cli.js")) ? pass("build.output", "dist/cli.js exists") : fail("build.output", "dist/cli.js missing; run npm run build", path.join(root, "dist", "cli.js")));
|
||||
if (existsSync(path.join(root, "dist", "cli.js"))) {
|
||||
try {
|
||||
const packRaw = execFileSync("npm", ["pack", "--json"], { cwd: root, encoding: "utf8" });
|
||||
const packInfo = JSON.parse(packRaw)[0] as { filename: string; files: { path: string }[] };
|
||||
const packed = new Set(packInfo.files.map((file) => file.path));
|
||||
for (const need of ["dist/cli.js", "engine_configs/godot.json", "engine_configs/unity.json", "engine_configs/unreal.json", "templates/gdd_template.md", "agents/base/master_orchestrator.md"]) {
|
||||
checks.push(packed.has(need) ? pass(`pack.${need}`, `${need} packed`) : fail(`pack.${need}`, `${need} missing from npm pack`));
|
||||
}
|
||||
const temp = mkdtempSync(path.join(tmpdir(), "open-gamestudio-pack-"));
|
||||
try {
|
||||
execFileSync("npm", ["install", "--silent", "--prefix", temp, path.join(root, packInfo.filename)], { cwd: root, encoding: "utf8" });
|
||||
execFileSync("npm", ["exec", "--prefix", temp, "open-gamestudio", "--", "templates", "list"], { cwd: temp, encoding: "utf8" });
|
||||
checks.push(pass("pack.install_smoke", "installed package bin loads templates from temp cwd"));
|
||||
} finally {
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
unlinkSync(path.join(root, packInfo.filename));
|
||||
}
|
||||
} catch (error) {
|
||||
checks.push(fail("pack.install_smoke", `package smoke failed: ${(error as Error).message}`));
|
||||
}
|
||||
}
|
||||
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(`command("${forbidden}`) && !help.includes(`option("${forbidden}`) ? pass(`future.absent.${forbidden}`, `${forbidden} not exposed`) : fail(`future.absent.${forbidden}`, `${forbidden} must not be exposed`));
|
||||
}
|
||||
return checks;
|
||||
}
|
||||
|
||||
export function validateProject(projectRoot: string): ValidationCheck[] {
|
||||
const checks: ValidationCheck[] = [];
|
||||
const configPath = path.join(projectRoot, "project-config.json");
|
||||
let config;
|
||||
try {
|
||||
config = readProjectConfig(configPath);
|
||||
checks.push(pass("project.config", "config schema-valid", configPath));
|
||||
} catch (error) {
|
||||
return [fail("project.config", `invalid project config: ${(error as Error).message}`, configPath)];
|
||||
}
|
||||
const expectedAgents = activeAgentsForMode(config.project.mode);
|
||||
checks.push(JSON.stringify(expectedAgents) === JSON.stringify(config.team.active_agents) ? pass("project.active_agents", "active agents match mode") : fail("project.active_agents", "active agents do not match mode", configPath));
|
||||
const engines = loadEngineConfigs(packageAssetPath("engine_configs"));
|
||||
const root = sourceRoot(projectRoot, config.project.slug);
|
||||
checks.push(existsSync(root) ? pass("project.source_root", "engine source root exists", root) : fail("project.source_root", "engine source root missing", root));
|
||||
const engineFile = expectedEngineProjectFile(projectRoot, config);
|
||||
checks.push(existsSync(engineFile) ? pass("project.engine_file", "engine project file exists", engineFile) : fail("project.engine_file", "engine project file missing", engineFile));
|
||||
if (config.project.engine === "unity") {
|
||||
const settings = path.join(root, "ProjectSettings", "ProjectSettings.asset");
|
||||
checks.push(existsSync(settings) ? pass("project.engine_settings", "Unity ProjectSettings marker exists", settings) : fail("project.engine_settings", "Unity ProjectSettings marker missing", settings));
|
||||
}
|
||||
for (const agent of config.team.active_agents) {
|
||||
const file = path.join(projectRoot, ".gamestudio", "agents", `${agent}.md`);
|
||||
if (!existsSync(file)) {
|
||||
checks.push(fail(`project.agent.${agent}`, `${agent} prompt missing`, file));
|
||||
continue;
|
||||
}
|
||||
const body = readFileSync(file, "utf8");
|
||||
const engine = engines[config.project.engine];
|
||||
const hasProjectContext =
|
||||
sectionHasContent(body, "# Project Context") &&
|
||||
body.includes(`- Name: ${config.project.name}`) &&
|
||||
body.includes(`- Engine: ${engine.display_name} ${config.project.engine_version}`) &&
|
||||
sectionHasContent(body, "# Engine Overlay");
|
||||
const missingSections = requiredAgentSections.filter((section) => !sectionHasContent(body, section));
|
||||
checks.push(
|
||||
hasProjectContext && missingSections.length === 0
|
||||
? pass(`project.agent.${agent}`, `${agent} materialized`, file)
|
||||
: fail(`project.agent.${agent}`, `${agent} prompt missing project context or non-empty sections: ${missingSections.join(", ") || "project context"}`, file)
|
||||
);
|
||||
}
|
||||
const agentsMd = path.join(projectRoot, "AGENTS.md");
|
||||
if (existsSync(agentsMd)) {
|
||||
const body = readFileSync(agentsMd, "utf8");
|
||||
const hash = guidanceConfigHash(config);
|
||||
checks.push(body.includes("generated-by: open-gamestudio src/agents.ts") ? pass("project.agents_md.provenance", "AGENTS.md provenance ok", agentsMd) : fail("project.agents_md.provenance", "AGENTS.md provenance missing", agentsMd));
|
||||
checks.push(body.includes(`source-config-sha256: ${hash}`) ? pass("project.agents_md.hash", "AGENTS.md hash current", agentsMd) : fail("project.agents_md.hash", "AGENTS.md stale; regenerate project agents", agentsMd));
|
||||
} else {
|
||||
checks.push(fail("project.agents_md", "project AGENTS.md missing", agentsMd));
|
||||
}
|
||||
for (const file of ["resources/market-research/market-overview.md", "documentation/design/gdd.md", "documentation/production/timeline.md"]) {
|
||||
checks.push(existsSync(path.join(projectRoot, file)) ? pass(`project.artifact.${file}`, `${file} exists`) : fail(`project.artifact.${file}`, `${file} missing`, path.join(projectRoot, file)));
|
||||
}
|
||||
const timeline = path.join(projectRoot, "documentation", "production", "timeline.md");
|
||||
if (existsSync(timeline)) {
|
||||
const body = readFileSync(timeline, "utf8");
|
||||
for (const section of ["# Timeline", "# Milestones", "# Risks", "# Next Validation Gate"]) {
|
||||
checks.push(sectionHasContent(body, section) ? pass(`project.timeline.${section}`, `${section} exists`, timeline) : fail(`project.timeline.${section}`, `${section} missing non-empty content`, timeline));
|
||||
}
|
||||
}
|
||||
const before = JSON.stringify(readFileSync(configPath, "utf8"));
|
||||
statusProject(projectRoot, path.dirname(projectRoot));
|
||||
resumeProject(projectRoot, path.dirname(projectRoot));
|
||||
const after = JSON.stringify(readFileSync(configPath, "utf8"));
|
||||
checks.push(before === after ? pass("project.read_only", "status/resume are read-only") : fail("project.read_only", "status/resume mutated project", configPath));
|
||||
return checks;
|
||||
}
|
||||
|
||||
export async function runValidation(options: { project?: string; root?: string } = {}): Promise<{ checks: ValidationCheck[]; failed: boolean }> {
|
||||
const root = options.root ?? process.cwd();
|
||||
const checks = options.project ? validateProject(path.resolve(root, options.project)) : await validateRepo(root);
|
||||
return { checks, failed: checks.some((check) => check.status === "fail") };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Purpose
|
||||
|
||||
Plan analytics instrumentation for the game.
|
||||
|
||||
# Inputs
|
||||
|
||||
- Core loop, player actions, success metrics, privacy constraints, and engine.
|
||||
|
||||
# Outputs
|
||||
|
||||
- `documentation/technical/analytics/analytics-plan.md`
|
||||
- Event taxonomy, funnels, dashboards, and validation checks.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>` and verify analytics docs are consistent with project goals.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Purpose
|
||||
|
||||
Document engine setup work for the selected game engine.
|
||||
|
||||
# Inputs
|
||||
|
||||
- Engine config, source root, project files, target platform, and team mode.
|
||||
|
||||
# Outputs
|
||||
|
||||
- `documentation/technical/engine-setup.md`
|
||||
- Engine version, required folders, project-file contract, and setup checklist.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>` and verify engine project files exist.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Purpose
|
||||
|
||||
Define a focused feature specification.
|
||||
|
||||
# Inputs
|
||||
|
||||
- Feature goal, affected systems, constraints, and acceptance criteria.
|
||||
|
||||
# Outputs
|
||||
|
||||
- `documentation/design/features/<feature-slug>.md`
|
||||
- User story, mechanics, implementation notes, test cases, and open questions.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>` and include feature-specific checks.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Purpose
|
||||
|
||||
Create or update the game design document.
|
||||
|
||||
# Inputs
|
||||
|
||||
- Project concept, engine, audience, mode, and current milestones.
|
||||
|
||||
# Outputs
|
||||
|
||||
- `documentation/design/gdd.md`
|
||||
- Clear core loop, player goals, systems, content scope, and risks.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>` after updating design docs.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Purpose
|
||||
|
||||
Transfer context between agents without broad project scanning.
|
||||
|
||||
# Inputs
|
||||
|
||||
- Completed work, changed artifacts, decisions, blockers, and next requested role.
|
||||
|
||||
# Outputs
|
||||
|
||||
- `documentation/handoffs/<handoff-slug>.md`
|
||||
- Summary, artifact links, validation state, and next command.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>` before handing off.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Purpose
|
||||
|
||||
Analyze market positioning using configured competitors as seeds.
|
||||
|
||||
# Inputs
|
||||
|
||||
- Project audience, genre, platform, monetization, and competitor names.
|
||||
|
||||
# Outputs
|
||||
|
||||
- `resources/market-research/market-analysis.md`
|
||||
- Audience hypothesis, competitor comparison, positioning, and risks.
|
||||
|
||||
# Validation
|
||||
|
||||
Run `npm run validate -- --project <project>` and preserve competitor names in project config.
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"project": {
|
||||
"name": "Test Game",
|
||||
"slug": "test-game",
|
||||
"concept": "A focused test concept",
|
||||
"genre": "Puzzle",
|
||||
"platform": "PC",
|
||||
"audience": "Players who like compact strategy games",
|
||||
"competitors": ["mini-metro", "dorfromantik"],
|
||||
"monetization": "premium",
|
||||
"timeline": "8 weeks",
|
||||
"engine": "godot",
|
||||
"engine_version": "4.4.1",
|
||||
"mode": "prototype",
|
||||
"phase": "Initialization",
|
||||
"status": "active"
|
||||
},
|
||||
"team": {
|
||||
"active_agents": [
|
||||
"master_orchestrator",
|
||||
"producer_agent",
|
||||
"market_analyst",
|
||||
"data_scientist",
|
||||
"sr_game_designer",
|
||||
"mechanics_developer",
|
||||
"qa_agent"
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"milestones": [
|
||||
{
|
||||
"id": "m1",
|
||||
"title": "Playable prototype",
|
||||
"target": "Week 4",
|
||||
"exit_criteria": ["Core loop is playable"],
|
||||
"status": "planned"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { activeAgentsForMode, canonicalProjectConfigJson, guidanceConfigHash, projectConfigSchema, slugify } from "../src/config.js";
|
||||
import { validateBaseAgents } from "../src/agents.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_agent");
|
||||
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([
|
||||
"analytics_setup",
|
||||
"engine_setup",
|
||||
"feature_spec",
|
||||
"gdd",
|
||||
"handoff",
|
||||
"market_analysis",
|
||||
"project_config"
|
||||
]);
|
||||
});
|
||||
|
||||
test("template selection is bounded", () => {
|
||||
expect(selectTemplates("market_analyst", "Create market overview")).toEqual(["market_analysis"]);
|
||||
expect(selectTemplates("data_scientist", "Create analytics plan")).toEqual(["analytics_setup"]);
|
||||
expect(selectTemplates("qa_agent", "Review validation readiness")).toEqual([]);
|
||||
expect(selectTemplates("producer_agent", "handoff coordination")).toEqual(["handoff"]);
|
||||
});
|
||||
|
||||
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("Roles: sr_game_designer, mid_game_designer");
|
||||
expect(output).toContain("# Purpose");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { mkdtempSync, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { createEngineFolders, createEngineProjectFiles, loadEngineConfigs, normalizeEngine, projectClassName, sourceRoot, unrealProjectFileName } from "../src/engines.js";
|
||||
import { packageAssetPath } from "../src/paths.js";
|
||||
|
||||
describe("engine registry", () => {
|
||||
test("loads all engine configs and aliases", () => {
|
||||
const registry = loadEngineConfigs(packageAssetPath("engine_configs"));
|
||||
expect(Object.keys(registry).sort()).toEqual(["godot", "unity", "unreal"]);
|
||||
expect(normalizeEngine("Godot", registry)).toBe("godot");
|
||||
expect(normalizeEngine("Unity Engine", registry)).toBe("unity");
|
||||
expect(normalizeEngine("Unreal Engine", registry)).toBe("unreal");
|
||||
expect(normalizeEngine("ue5", registry)).toBe("unreal");
|
||||
expect(() => normalizeEngine("scratch", registry)).toThrow(/Unknown engine/);
|
||||
});
|
||||
|
||||
test("creates engine roots under source/project-slug", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "ogs-engine-"));
|
||||
const registry = loadEngineConfigs(packageAssetPath("engine_configs"));
|
||||
for (const engine of ["godot", "unity", "unreal"] as const) {
|
||||
createEngineFolders({ projectRoot: root, projectSlug: "test-game", projectName: "Test Game", engine, registry });
|
||||
createEngineProjectFiles({ projectRoot: root, projectSlug: "test-game", projectName: "Test Game", engine, registry });
|
||||
}
|
||||
const src = sourceRoot(root, "test-game");
|
||||
expect(existsSync(path.join(src, "project.godot"))).toBe(true);
|
||||
expect(existsSync(path.join(src, "Packages", "manifest.json"))).toBe(true);
|
||||
expect(existsSync(path.join(src, "ProjectSettings", "ProjectSettings.asset"))).toBe(true);
|
||||
expect(existsSync(path.join(src, "TestGame.uproject"))).toBe(true);
|
||||
});
|
||||
|
||||
test("generates Unreal class names", () => {
|
||||
expect(projectClassName("Test Game")).toBe("TestGame");
|
||||
expect(projectClassName("codex-unreal-smoke")).toBe("CodexUnrealSmoke");
|
||||
expect(projectClassName("2d arena")).toBe("Game2dArena");
|
||||
expect(projectClassName("rocket! zone")).toBe("RocketZone");
|
||||
expect(() => projectClassName("!!!")).toThrow(/Cannot create/);
|
||||
expect(unrealProjectFileName("Test Game")).toBe("TestGame.uproject");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { existsSync, readFileSync } 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, expect, test } from "vitest";
|
||||
import { guidanceConfigHash, readProjectConfig } from "../src/config.js";
|
||||
import { freezeProject, initProject, resumeProject, statusProject } from "../src/projects.js";
|
||||
|
||||
describe("project workflow", () => {
|
||||
test("init creates project docs, config, agents, and engine files", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-project-"));
|
||||
const { projectRoot, config } = initProject({ name: "Test Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
expect(existsSync(path.join(projectRoot, "project-config.json"))).toBe(true);
|
||||
expect(existsSync(path.join(projectRoot, "source", "project-test-game", "project.godot"))).toBe(true);
|
||||
expect(existsSync(path.join(projectRoot, "resources", "market-research", "market-overview.md"))).toBe(true);
|
||||
expect(existsSync(path.join(projectRoot, "documentation", "design", "gdd.md"))).toBe(true);
|
||||
expect(existsSync(path.join(projectRoot, ".gamestudio", "agents", "master_orchestrator.md"))).toBe(true);
|
||||
expect(readFileSync(path.join(projectRoot, "AGENTS.md"), "utf8")).toContain(guidanceConfigHash(config));
|
||||
expect(config.project.concept).toBe("Test Game concept");
|
||||
expect(config.project.genre).toBe("Unspecified");
|
||||
expect(config.project.platform).toBe("PC");
|
||||
expect(config.project.audience).toBe("General players");
|
||||
expect(config.project.competitors).toEqual([]);
|
||||
expect(config.project.monetization).toBe("undecided");
|
||||
expect(config.project.timeline).toBe("TBD");
|
||||
expect(existsSync(path.join(projectRoot, "resources", "market-research", "mini-metro.md"))).toBe(false);
|
||||
});
|
||||
|
||||
test("init requires explicit non-interactive mode and supports optional overrides", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-required-"));
|
||||
expect(() => initProject({ name: "Missing Mode", engine: "godot", nonInteractive: true }, cwd)).toThrow(/--mode/);
|
||||
expect(() => initProject({ name: "Missing Noninteractive", engine: "godot", mode: "prototype" }, cwd)).toThrow(/--non-interactive/);
|
||||
const { config } = initProject({
|
||||
name: "Override Game",
|
||||
engine: "godot",
|
||||
mode: "design",
|
||||
nonInteractive: true,
|
||||
competitors: ["terra nil", "mini metro"],
|
||||
engineVersion: "4.5.custom"
|
||||
}, cwd);
|
||||
expect(config.project.competitors).toEqual(["terra nil", "mini metro"]);
|
||||
expect(config.project.engine_version).toBe("4.5.custom");
|
||||
});
|
||||
|
||||
test("CLI init requires mode and non-interactive and accepts repeated competitor flags", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-cli-"));
|
||||
const cli = path.join(process.cwd(), "src", "cli.ts");
|
||||
const tsx = path.join(process.cwd(), "node_modules", ".bin", "tsx");
|
||||
expect(() => execFileSync(tsx, [cli, "init", "--name", "CLI Missing Mode", "--engine", "godot", "--non-interactive"], { cwd, encoding: "utf8", stdio: "pipe" })).toThrow();
|
||||
execFileSync(tsx, [
|
||||
cli,
|
||||
"init",
|
||||
"--name",
|
||||
"CLI Game",
|
||||
"--engine",
|
||||
"godot",
|
||||
"--mode",
|
||||
"prototype",
|
||||
"--non-interactive",
|
||||
"--competitor",
|
||||
"terra nil",
|
||||
"--competitor",
|
||||
"mini metro",
|
||||
"--engine-version",
|
||||
"4.5.custom"
|
||||
], { cwd, encoding: "utf8" });
|
||||
const config = readProjectConfig(path.join(cwd, "projects", "cli-game", "project-config.json"));
|
||||
expect(config.project.competitors).toEqual(["terra nil", "mini metro"]);
|
||||
expect(config.project.engine_version).toBe("4.5.custom");
|
||||
});
|
||||
|
||||
test("CLI init does not expose arbitrary project root override", () => {
|
||||
const cli = path.join(process.cwd(), "src", "cli.ts");
|
||||
const tsx = path.join(process.cwd(), "node_modules", ".bin", "tsx");
|
||||
const help = execFileSync(tsx, [cli, "init", "--help"], { encoding: "utf8" });
|
||||
expect(help).not.toContain("--root");
|
||||
});
|
||||
|
||||
test("init ignores arbitrary root override and stays under projects slug", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-root-"));
|
||||
const outsideRoot = path.join(cwd, "outside-root");
|
||||
const { projectRoot } = initProject({ name: "Root Escape", engine: "godot", mode: "prototype", nonInteractive: true, root: outsideRoot } as Parameters<typeof initProject>[0], cwd);
|
||||
expect(projectRoot).toBe(path.join(cwd, "projects", "root-escape"));
|
||||
expect(existsSync(outsideRoot)).toBe(false);
|
||||
});
|
||||
|
||||
test("all engines initialize expected files", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-engines-"));
|
||||
expect(existsSync(path.join(initProject({ name: "Godot Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd).projectRoot, "source", "project-godot-game", "project.godot"))).toBe(true);
|
||||
expect(existsSync(path.join(initProject({ name: "Unity Game", engine: "unity", mode: "design", nonInteractive: true }, cwd).projectRoot, "source", "project-unity-game", "Packages", "manifest.json"))).toBe(true);
|
||||
expect(existsSync(path.join(initProject({ name: "Unreal Game", engine: "Unreal Engine", mode: "development", nonInteractive: true }, cwd).projectRoot, "source", "project-unreal-game", "UnrealGame.uproject"))).toBe(true);
|
||||
});
|
||||
|
||||
test("same-parent init rejects Unreal class-name collisions", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-collision-"));
|
||||
initProject({ name: "Foo1", engine: "unreal", mode: "prototype", nonInteractive: true }, cwd);
|
||||
expect(() => initProject({ name: "Foo 1", engine: "unreal", mode: "prototype", nonInteractive: true }, cwd)).toThrow(/collides/i);
|
||||
});
|
||||
|
||||
test("status resume are read-only and freeze only changes operational status", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-status-"));
|
||||
const { projectRoot } = initProject({ name: "Freeze Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
const before = readFileSync(path.join(projectRoot, "project-config.json"), "utf8");
|
||||
expect(statusProject(projectRoot, cwd)).toContain("status: active");
|
||||
expect(resumeProject(projectRoot, cwd)).toContain("Suggested next command");
|
||||
expect(readFileSync(path.join(projectRoot, "project-config.json"), "utf8")).toBe(before);
|
||||
const hash = guidanceConfigHash(readProjectConfig(path.join(projectRoot, "project-config.json")));
|
||||
freezeProject(projectRoot, cwd);
|
||||
expect(readProjectConfig(path.join(projectRoot, "project-config.json")).project.status).toBe("frozen");
|
||||
expect(guidanceConfigHash(readProjectConfig(path.join(projectRoot, "project-config.json")))).toBe(hash);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { 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";
|
||||
|
||||
describe("bounded runner", () => {
|
||||
test("requires non-empty task", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-"));
|
||||
const { projectRoot } = initProject({ name: "Run Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
expect(() => prepareRun("qa_agent", { project: projectRoot, task: "" }, cwd)).toThrow(/--task/);
|
||||
});
|
||||
|
||||
test("writes prompt cache and metadata with bounded context", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-"));
|
||||
const { projectRoot } = initProject({ name: "Market Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
const result = prepareRun("market_analyst", { project: projectRoot, task: "Create first market overview", printPrompt: true }, cwd);
|
||||
expect(existsSync(result.promptPath)).toBe(true);
|
||||
expect(existsSync(result.metadataPath)).toBe(true);
|
||||
expect(result.prompt).toContain("# Template: market_analysis");
|
||||
expect(result.prompt).toContain("resources/market-research/market-analysis.md");
|
||||
expect(result.prompt).not.toContain("# Template: analytics_setup");
|
||||
expect(result.prompt).not.toContain("Data scientist prompt");
|
||||
const metadata = JSON.parse(readFileSync(result.metadataPath, "utf8"));
|
||||
expect(metadata.prompt_chars).toBe(result.prompt.length);
|
||||
expect(metadata.prompt_cache_path).toContain("prompt.md");
|
||||
});
|
||||
|
||||
test("dry-run lists context and explicit artifacts only", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-"));
|
||||
const { projectRoot } = initProject({ name: "Qa Game", engine: "Unreal Engine", mode: "development", nonInteractive: true }, cwd);
|
||||
writeFileSync(path.join(projectRoot, "documentation", "design", "note.md"), "# Note\n");
|
||||
const result = prepareRun(
|
||||
"qa_agent",
|
||||
{ project: projectRoot, task: "Review validation readiness", dryRun: true, includeArtifact: ["documentation/design/note.md"] },
|
||||
cwd
|
||||
);
|
||||
expect(result.output).toContain("Context files:");
|
||||
expect(result.output).toContain("documentation/design/note.md");
|
||||
expect(result.prompt).toContain("Unreal Engine");
|
||||
expect(result.prompt).toContain("npm run validate");
|
||||
expect(() => prepareRun("qa_agent", { project: projectRoot, task: "x", includeArtifact: ["../outside.md"] }, cwd)).toThrow(/escape/);
|
||||
});
|
||||
|
||||
test("included artifacts cannot escape through project-local symlinks", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-"));
|
||||
const { projectRoot } = initProject({ name: "Symlink Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
const outside = path.join(cwd, "outside.md");
|
||||
writeFileSync(outside, "# Secret\n");
|
||||
symlinkSync(outside, path.join(projectRoot, "documentation", "design", "outside-link.md"));
|
||||
expect(() =>
|
||||
prepareRun("qa_agent", { project: projectRoot, task: "x", includeArtifact: ["documentation/design/outside-link.md"] }, cwd)
|
||||
).toThrow(/escape/);
|
||||
});
|
||||
|
||||
test("prompt cache paths are unique for repeated runs", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-"));
|
||||
const { projectRoot } = initProject({ name: "Unique Run Game", engine: "unity", mode: "prototype", nonInteractive: true }, cwd);
|
||||
const a = prepareRun("data_scientist", { project: projectRoot, task: "Create analytics plan" }, cwd);
|
||||
const b = prepareRun("data_scientist", { project: projectRoot, task: "Create analytics plan" }, cwd);
|
||||
expect(a.promptPath).not.toBe(b.promptPath);
|
||||
expect(a.metadataPath).not.toBe(b.metadataPath);
|
||||
});
|
||||
|
||||
test("same inputs produce same deterministic prompt body aside from metadata path", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-run-"));
|
||||
const { projectRoot } = initProject({ name: "Stable Game", engine: "unity", mode: "prototype", nonInteractive: true }, cwd);
|
||||
const a = prepareRun("data_scientist", { project: projectRoot, task: "Create analytics plan" }, cwd).prompt;
|
||||
const b = prepareRun("data_scientist", { project: projectRoot, task: "Create analytics plan" }, cwd).prompt;
|
||||
expect(a).toBe(b);
|
||||
expect(a).toContain("# Template: analytics_setup");
|
||||
expect(a).toContain("documentation/technical/analytics/analytics-plan.md");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { rmSync, 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 { guidanceConfigHash, readProjectConfig, writeProjectConfig } from "../src/config.js";
|
||||
import { freezeProject, initProject } from "../src/projects.js";
|
||||
import { runValidation, validateProject } from "../src/validation.js";
|
||||
|
||||
describe("validation", () => {
|
||||
test("fresh initialized projects validate and failures are explicit", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
|
||||
for (const [name, engine, mode] of [
|
||||
["Godot Val", "godot", "prototype"],
|
||||
["Unity Val", "unity", "design"],
|
||||
["Unreal Val", "ue5", "development"]
|
||||
] as const) {
|
||||
const { projectRoot } = initProject({ 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);
|
||||
rmSync(path.join(projectRoot, "AGENTS.md"));
|
||||
rmSync(path.join(projectRoot, "source", "project-broken-game", "project.godot"));
|
||||
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
|
||||
expect(failures.map((f) => f.id)).toContain("project.agents_md");
|
||||
expect(failures.map((f) => f.id)).toContain("project.engine_file");
|
||||
});
|
||||
|
||||
test("malformed materialized agent prompts fail validation", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
|
||||
const { projectRoot, config } = initProject({ name: "Prompt Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
writeFileSync(
|
||||
path.join(projectRoot, ".gamestudio", "agents", "master_orchestrator.md"),
|
||||
`# Role\n\n# Inputs\n\n# Outputs\n\n# Validation\n\n# Engine Notes\n\n# Rules\n\n# Project Context\n\n- Name: ${config.project.name}\n- Engine: Godot ${config.project.engine_version}\n\n# Engine Overlay\n\nUse Godot.\n`
|
||||
);
|
||||
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
|
||||
expect(failures.map((f) => f.id)).toContain("project.agent.master_orchestrator");
|
||||
});
|
||||
|
||||
test("materialized agent prompts fail validation when engine overlay is empty", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
|
||||
const { projectRoot, config } = initProject({ name: "Overlay Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
writeFileSync(
|
||||
path.join(projectRoot, ".gamestudio", "agents", "master_orchestrator.md"),
|
||||
`# Role\n\nCoordinate the team.\n\n# Inputs\n\n- Project brief.\n\n# Outputs\n\n- Production direction.\n\n# Validation\n\n- Check generated artifacts.\n\n# Engine Notes\n\n- Use engine-specific guidance.\n\n# Rules\n\n- Keep work scoped.\n\n# Project Context\n\n- Name: ${config.project.name}\n- Engine: Godot ${config.project.engine_version}\n\n# Engine Overlay\n\n`
|
||||
);
|
||||
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
|
||||
expect(failures.map((f) => f.id)).toContain("project.agent.master_orchestrator");
|
||||
});
|
||||
|
||||
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);
|
||||
writeFileSync(
|
||||
path.join(projectRoot, "documentation", "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"
|
||||
);
|
||||
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
|
||||
expect(failures.map((f) => f.id)).toContain("project.timeline.# Milestones");
|
||||
});
|
||||
|
||||
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);
|
||||
rmSync(path.join(projectRoot, "source", "project-broken-unity", "ProjectSettings", "ProjectSettings.asset"));
|
||||
const failures = validateProject(projectRoot).filter((c) => c.status === "fail");
|
||||
expect(failures.map((f) => f.id)).toContain("project.engine_settings");
|
||||
});
|
||||
|
||||
test("invalid config and stale AGENTS hash fail", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
|
||||
const { projectRoot } = initProject({ name: "Stale Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
const configPath = path.join(projectRoot, "project-config.json");
|
||||
const config = readProjectConfig(configPath);
|
||||
const hash = guidanceConfigHash(config);
|
||||
config.project.genre = "Changed";
|
||||
writeProjectConfig(configPath, config);
|
||||
expect(validateProject(projectRoot).some((c) => c.id === "project.agents_md.hash" && c.status === "fail")).toBe(true);
|
||||
config.project.status = "frozen";
|
||||
expect(guidanceConfigHash(config)).not.toBe(hash);
|
||||
writeFileSync(configPath, "{ invalid json");
|
||||
expect(validateProject(projectRoot)[0].status).toBe("fail");
|
||||
});
|
||||
|
||||
test("freeze status-only changes do not stale AGENTS hash", () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-val-"));
|
||||
const { projectRoot } = initProject({ name: "Freeze Valid", engine: "godot", mode: "prototype", nonInteractive: true }, cwd);
|
||||
freezeProject(projectRoot, cwd);
|
||||
expect(validateProject(projectRoot).filter((c) => c.status === "fail")).toEqual([]);
|
||||
});
|
||||
|
||||
test("repo validation fails hard when built CLI is missing", async () => {
|
||||
const result = await runValidation();
|
||||
expect(result.checks.some((c) => c.id === "package.bin")).toBe(true);
|
||||
expect(result.failed).toBe(result.checks.some((c) => c.status === "fail"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"noEmit": false,
|
||||
"declaration": false,
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"tests/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user