diff --git a/AGENTS.md b/AGENTS.md index 0444958..55732a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ 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 is the default path via `opengamestudio run `. `--dry-run` and `--print-prompt` are inspection-only paths. Telemetry, planner/next, ownership enforcement, and parallel orchestration are future-only. +Direct Codex execution is the default path via `opengamestudio run `. `--dry-run` and `--print-prompt` are inspection-only paths. Explicit, file-backed task orchestration is now inside the product boundary; telemetry, planner/next, ownership enforcement, hosted orchestration, background loops, and unbounded parallelism remain future-only. ## Repository Rules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca7cbe5..722a18d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,6 @@ 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 add Python compatibility files, duplicate script-wrapper logic, telemetry, direct Codex execution, planner behavior, hosted/background orchestration, unbounded parallelism, or ownership enforcement without a new design. Explicit local task orchestration is allowed only under the product boundary and must include tests and docs. Do not edit `research/*` as part of implementation changes unless the task explicitly asks for research updates. diff --git a/README.md b/README.md index 7316af6..68642cf 100644 --- a/README.md +++ b/README.md @@ -150,9 +150,10 @@ Validation exits nonzero on failure. It checks package contracts, template avail | `templates list` | List packaged template IDs. | | `templates show ` | Print a packaged template. | | `run ` | Prepare one bounded Codex prompt packet and invoke `codex exec` by default. | -| `task create` / `task run` | Manage file-backed `.codex/tasks.json` tasks. | +| `task create` / `task run` / `task orchestrate` | Manage file-backed `.codex/tasks.json` tasks and run explicit local bounded orchestration. | | `market`, `analytics`, `design-spec`, `feel-review`, `art-direction`, `ui-review`, `milestone`, `handoff` | Render focused workflow prompts. | | `review`, `ship-check` | Render baseline review and release-check prompts. | +| `workflow create-tasks ` | Create explicit file-backed tasks from supported workflow recipes such as `vertical-slice`, `bugfix`, `ui-ux-review`, and `release-checklist`. | ## Studio roles @@ -187,8 +188,9 @@ Project artifacts: - `.codex/studio.json`: project metadata, role roster, workflow IDs, and workflow state. - `.codex/prompts/`: generated role prompts. - `.codex/workflows/`: generated workflow prompts. -- `.codex/runs/`: prepared prompt packets and run metadata from non-dry role runs. -- `.codex/tasks.json`: file-backed task state when you use `task create` or `task run`. +- `.codex/runs/`: prepared prompt packets, per-task orchestration output, and run metadata from non-dry role or orchestration runs. +- `.codex/locks/`: transient file-backed locks for bounded parallel task orchestration. +- `.codex/tasks.json`: file-backed task state when you use `task create`, `task run`, workflow task recipes, or `task orchestrate`. - `documentation/`: starter game-design and production documents. - `source/project-/`: engine project location contract. @@ -205,6 +207,9 @@ Implemented: - direct `codex exec` role execution; - dry-run and prompt-print inspection; - file-backed tasks; +- explicit local task orchestration with bounded `--max-concurrency` and file-backed locks; +- workflow task recipes for selected high-value workflows; +- curated CCGS adaptation registry for role/skill/workflow translation decisions; - bounded review, verification, and fix-pass options; - hard-failing repository and project validation. @@ -212,12 +217,15 @@ Future-only, not exposed as working features: - planner/`next`; - telemetry; -- parallel orchestration; - changed-file tracking; +- hosted/background orchestration; +- unbounded parallelism; - hard output-ownership enforcement; - legacy `.gamestudio` compatibility; - generated `CODEX.md` or `project_orchestrator.md` surfaces. +Explicit local task orchestration is now inside the product boundary, but user-facing runtime claims require implementation, tests, and docs. + See [`docs/known-upstream-differences.md`](docs/known-upstream-differences.md) and [`docs/migration-from-claude.md`](docs/migration-from-claude.md) for the detailed migration contract. ## Development diff --git a/docs/ai/repo-rules.md b/docs/ai/repo-rules.md index 05fdca7..0dcaeb7 100644 --- a/docs/ai/repo-rules.md +++ b/docs/ai/repo-rules.md @@ -22,7 +22,7 @@ This file mirrors the repository-specific agent rules from `AGENTS.md` in the co - `src/agents.ts` is the single owner for generated project `AGENTS.md`. - Direct Codex execution is the default path via `opengamestudio run `. - `--dry-run` and `--print-prompt` are inspection-only paths. -- Telemetry, planner/next, ownership enforcement, and parallel orchestration are future-only. +- Explicit, file-backed task orchestration is now inside the product boundary; telemetry, planner/next, ownership enforcement, hosted orchestration, background loops, and unbounded parallelism remain future-only. - Read `docs/architecture/product-boundary.md` before creating or revising designs, implementation plans, OpenSpec changes, generated project surfaces, role/workflow expansions, approval/write-policy behavior, or runtime execution behavior. ## Truthmark Notes diff --git a/docs/architecture/flows/role-run-lifecycle.md b/docs/architecture/flows/role-run-lifecycle.md index 32db5e1..9d94027 100644 --- a/docs/architecture/flows/role-run-lifecycle.md +++ b/docs/architecture/flows/role-run-lifecycle.md @@ -173,7 +173,7 @@ flowchart TD ## Rationale -Separating inspection, implementation, verification, review, and fix branches makes the Codex lifecycle auditable without inventing hidden planner, telemetry, ownership enforcement, or parallel orchestration behavior. +Separating inspection, implementation, verification, review, and fix branches makes the Codex lifecycle auditable without inventing hidden planner, telemetry, ownership enforcement, hosted/background orchestration, or unbounded parallel behavior. ## Truth Sources diff --git a/docs/architecture/flows/workflow-prompt-rendering.md b/docs/architecture/flows/workflow-prompt-rendering.md index 8e80722..210c7b3 100644 --- a/docs/architecture/flows/workflow-prompt-rendering.md +++ b/docs/architecture/flows/workflow-prompt-rendering.md @@ -109,7 +109,7 @@ flowchart TD - They do not call Codex. - They do not write `.codex/runs/` cache files. - They do not mutate `.codex/studio.json` or `.codex/tasks.json`. -- They do not expose hidden planner, telemetry, ownership enforcement, or parallel orchestration behavior. +- They do not expose hidden planner, telemetry, ownership enforcement, hosted/background orchestration, or unbounded parallel behavior. Explicit task orchestration belongs in task/run lifecycle commands, not render-only shortcut output. ## Failure Modes And Debugging Cues diff --git a/docs/architecture/product-boundary.md b/docs/architecture/product-boundary.md index 7e087e2..1c96241 100644 --- a/docs/architecture/product-boundary.md +++ b/docs/architecture/product-boundary.md @@ -2,7 +2,7 @@ status: active doc_type: architecture truth_kind: architecture -last_reviewed: 2026-06-13 +last_reviewed: 2026-06-25 source_of_truth: - ../../README.md - ../../AGENTS.md @@ -24,7 +24,7 @@ Open Game Studio helps developers use Codex as a practical game-development stud The product should make it easier to start, plan, build, review, and ship games by providing a package-friendly TypeScript CLI that creates project scaffolds, renders bounded Codex prompts, manages game-studio roles and workflows, records auditable project state, and validates the generated surfaces. -The product exists to expand what a developer can do with Codex for game creation. It must not turn game development into mandatory studio ceremony, hide the developer's intent behind an opaque orchestrator, or replace human creative and technical judgment. +The product exists to expand what a developer can do with Codex for game creation. It may orchestrate role-scoped Codex work when the plan, state, permissions, and handoffs stay explicit and reviewable. It must not turn game development into mandatory studio ceremony, hide the developer's intent behind an opaque orchestrator, or replace human creative and technical judgment. ## Product Shape @@ -34,13 +34,14 @@ Open Game Studio is: - Codex-native in its primary execution path; - oriented around generated project files such as `AGENTS.md`, `.codex/**`, templates, tasks, and validation output; - useful for both solo/prototype work and more structured studio-style work; -- explicit about when it is rendering prompts, running Codex, mutating project state, or only inspecting planned behavior. +- allowed to provide bounded task orchestration for role-scoped Codex runs when it remains local, explicit, file-backed, policy-gated, and validated; +- explicit about when it is rendering prompts, planning tasks, orchestrating task runs, running Codex, mutating project state, or only inspecting planned behavior. Open Game Studio is not: - a game engine or replacement for engine tooling; - a hosted service, daemon, IDE, or background workflow controller; -- a general-purpose agent orchestrator or arbitrary workflow DAG engine; +- a general-purpose agent platform or arbitrary workflow DAG engine outside game-studio tasks; - a hidden memory, checkpoint, telemetry, or analytics platform; - a CI, merge-approval, or release-enforcement product; - a requirements-management system or heavyweight studio-process mandate; @@ -51,25 +52,26 @@ External tools, reference workflows, and comparison projects may inspire improve ## Core Behavior Expectations 1. **Codex-native by default.** Direct Codex execution through `opengamestudio run ` remains the primary runtime path. Inspection paths such as `--dry-run` and `--print-prompt` must stay non-mutating. -2. **Local repository files stay reviewable.** Project state, prompts, tasks, approvals, templates, and validation evidence should be visible in the working tree or deterministic CLI output, not hidden in off-repo services. -3. **Developer control comes first.** Studio depth is optional and mode-controlled. Fast prototype workflows must remain lightweight; strict approval flows must be explicit rather than silently imposed on every project. -4. **Project stage and studio strictness are separate.** Lifecycle stage (`design`, `prototype`, `development`) must not be collapsed into process strictness (`fast-prototype`, `guided-studio`, `strict-studio`). -5. **Generated instructions use `AGENTS.md` and `.codex/**`.** Do not add `CODEX.md`, legacy generated-project compatibility shims, or alternate instruction contracts unless a future boundary update explicitly changes this rule. -6. **Depth comes from selected context, not prompt bloat.** Registries may contain rich roles, workflows, engine references, and rules, but generated prompts should include only relevant selected material. -7. **Mutation is policy-gated and visible.** Any design that lets Codex or the CLI mutate files must specify write policy, approval/override behavior, sandbox selection, dry-run diagnostics, and where provenance is recorded. -8. **Future-only surfaces must remain absent until built.** Planner/next, telemetry, parallel orchestration, hard output-ownership enforcement, and similar surfaces must not appear as user-facing behavior before they have implementation, tests, and docs. -9. **Validation is part of the product.** New generated surfaces, package assets, CLI commands, and behavior-bearing docs need repo-native validation and tests before readiness or parity claims. -10. **Truthmark is repository workflow tooling here, not the product.** Truthmark-backed docs may guard Open Game Studio's repository truth, but Open Game Studio should not present Truthmark workflow mechanics as game-studio product features. +2. **Orchestration is now in scope when explicit.** The CLI may coordinate multiple role-scoped Codex task runs, including dependencies and bounded sequences, only when task state, selected context, approvals, locks, run metadata, and failure outcomes are visible in `.codex/**` or deterministic CLI output. +3. **Local repository files stay reviewable.** Project state, prompts, tasks, approvals, templates, and validation evidence should be visible in the working tree or deterministic CLI output, not hidden in off-repo services. +4. **Developer control comes first.** Studio depth is optional and mode-controlled. Fast prototype workflows must remain lightweight; orchestration and strict approval flows must be explicit rather than silently imposed on every project. +5. **Project stage and studio strictness are separate.** Lifecycle stage (`design`, `prototype`, `development`) must not be collapsed into process strictness (`fast-prototype`, `guided-studio`, `strict-studio`). +6. **Generated instructions use `AGENTS.md` and `.codex/**`.** Do not add `CODEX.md`, legacy generated-project compatibility shims, or alternate instruction contracts unless a future boundary update explicitly changes this rule. +7. **Depth comes from selected context, not prompt bloat.** Registries may contain rich roles, workflows, engine references, and rules, but generated prompts and task packets should include only relevant selected material. +8. **Mutation is policy-gated and visible.** Any design that lets Codex or the CLI mutate files must specify write policy, approval/override behavior, sandbox selection, dry-run diagnostics, and where provenance is recorded. +9. **Future-only surfaces must remain absent until built.** Planner/next, telemetry, hard output-ownership enforcement, hosted orchestration, background autonomous loops, and unbounded parallelism must not appear as user-facing behavior before they have implementation, tests, and docs. +10. **Validation is part of the product.** New generated surfaces, package assets, CLI commands, orchestration behavior, and behavior-bearing docs need repo-native validation and tests before readiness or parity claims. +11. **Truthmark is repository workflow tooling here, not the product.** Truthmark-backed docs may guard Open Game Studio's repository truth, but Open Game Studio should not present Truthmark workflow mechanics as game-studio product features. ## In Scope Designs and plans may expand or refine: -- CLI commands for project initialization, status, templates, validation, tasks, approvals, role runs, and bounded workflow shortcuts; +- CLI commands for project initialization, status, templates, validation, tasks, approvals, role runs, explicit task orchestration, and bounded workflow shortcuts; - generated game project files under the project root, especially `AGENTS.md`, `.codex/**`, templates, tasks, approvals, context manifests, and selected runtime docs; - role and workflow registries that improve game-development coverage while keeping materialized prompts bounded; - engine references for Godot, Unity, and Unreal when they are packaged, validated, reviewed, and selected by relevance; -- approval and write-policy primitives that explain or gate mutating behavior; +- approval, locking, dependency, and write-policy primitives that explain or gate mutating and orchestrated behavior; - project validation, package smoke tests, and human-readable documentation that keep generated surfaces honest. ## Out Of Scope Unless This Boundary Changes @@ -81,7 +83,7 @@ Designs and plans must not introduce these as required product behavior: - mandatory heavyweight studio process for small prototypes; - non-Codex execution hosts as first-class runtime replacements; - general task management, PR approval, CI enforcement, or release governance outside the game-studio CLI boundary; -- broad prompt generation that loads all agents, all templates, all engine references, or all docs for a single role task; +- broad prompt generation or orchestration packets that load all agents, all templates, all engine references, or all docs for a single role task; - compatibility layers for stale generated project formats unless an explicit migration design is approved. Optional integrations are acceptable only when the local Codex-native workflow remains understandable, reviewable, and usable without them. @@ -97,7 +99,7 @@ Every design, plan, or OpenSpec change should answer these questions before impl - What local files or deterministic outputs let a human review the change? - What writes are allowed, what writes are forbidden, and what fails closed? - What context is selected, and what prevents loading everything by default? -- Does it accidentally introduce a hosted service, daemon, orchestrator, hidden memory layer, CI gate, or heavyweight lifecycle platform? +- Does it accidentally introduce a hosted service, daemon, general-purpose orchestrator, hidden memory layer, CI gate, unbounded parallelism, or heavyweight lifecycle platform? - What tests, validation commands, and behavior-bearing docs prove the boundary remains intact? If a design or plan cannot answer these questions, it is not ready to implement. diff --git a/docs/development-rules.md b/docs/development-rules.md index 698994b..19d69d2 100644 --- a/docs/development-rules.md +++ b/docs/development-rules.md @@ -15,4 +15,4 @@ npm run validate Keep generated projects under `projects//`. -The current build invokes Codex by default through `run `. Use `--dry-run` or `--print-prompt` for inspection-only paths. It still intentionally excludes planner commands, telemetry, parallel orchestration, changed-file tracking, and ownership enforcement. +The current build invokes Codex by default through `run `. Use `--dry-run` or `--print-prompt` for inspection-only paths. Explicit local task orchestration is now inside the product boundary, but runtime claims require implementation and validation. The build still intentionally excludes planner commands, telemetry, changed-file tracking, hosted/background orchestration, unbounded parallelism, and ownership enforcement. diff --git a/docs/known-upstream-differences.md b/docs/known-upstream-differences.md index bde3e78..d543bfc 100644 --- a/docs/known-upstream-differences.md +++ b/docs/known-upstream-differences.md @@ -16,9 +16,9 @@ Generated projects materialize project-specific `.codex/prompts/.md` files Market and analytics are first-class renderable workflows owned by dedicated roles. Workflow prompts and normal role runs inline selected package template bodies instead of pointing Codex at project-relative template paths or loading every template. -Studio orchestration is provided by the `studio-orchestrator` role and the render-only `handoff` workflow shortcut, not by a generated `project_orchestrator.md`. +Studio orchestration is provided through Codex-native roles, file-backed task state, explicit `task orchestrate` execution, selected workflow task recipes, and bounded workflow surfaces, not by a generated `project_orchestrator.md`. -Richer workflows exist for design specs, game-feel review, art direction, UI/UX review, production milestones, review, ship-check, playtest, bugfix, vertical slice, market, analytics, and handoff. Shortcut workflow commands render prompts only; executable workflow lifecycle support remains future-only. +Richer workflows exist for design specs, game-feel review, art direction, UI/UX review, production milestones, review, ship-check, playtest, bugfix, vertical slice, market, analytics, and handoff. Shortcut workflow commands render prompts. Supported workflow recipes such as `vertical-slice`, `bugfix`, `ui-ux-review`, and `release-checklist` can explicitly create `.codex/tasks.json` task graphs before `task orchestrate` runs them. 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. @@ -26,4 +26,4 @@ Codex-native difference: `run ` invokes `codex exec` by default against th Generated role prompts and workflow files include freshness metadata and rendered-body hashes for new projects. Validation reports legacy missing-metadata files as regeneration-needed skip diagnostics instead of treating them as fresh. -Future-only features are not implemented in this build: planner/`next`, telemetry, parallel orchestration, changed-file tracking, prompt-size metrics, hard output-ownership enforcement, legacy `.gamestudio` compatibility, `CODEX.md`, and `project_orchestrator.md`. +Future-only features still not implemented in this build include planner/`next`, telemetry, changed-file tracking, prompt-size metrics, hard output-ownership enforcement, hosted/background orchestration, unbounded parallelism, legacy `.gamestudio` compatibility, `CODEX.md`, and `project_orchestrator.md`. Explicit local task orchestration is implemented as foreground, bounded, file-backed CLI behavior. diff --git a/docs/migration-from-claude.md b/docs/migration-from-claude.md index 521b92e..d324d48 100644 --- a/docs/migration-from-claude.md +++ b/docs/migration-from-claude.md @@ -13,4 +13,4 @@ For inspection-only runs, add `--dry-run` or `--print-prompt` to view the genera Intentional differences: no interactive menu, no `startover`, no exact `template_info.md`, no eager competitor reports during init, no generated `project_orchestrator.md`, no `CODEX.md`, no legacy `.gamestudio` compatibility, and no unsupported upstream underscore role IDs. Supported aliases such as `new` for `init` remain available. -Future-only features are not implemented: `opengamestudio next`, telemetry, parallel orchestration, changed-file tracking, and ownership enforcement. +Future-only features are not implemented: `opengamestudio next`, telemetry, changed-file tracking, hosted/background orchestration, unbounded parallelism, and ownership enforcement. Explicit local task orchestration is implemented through reviewable `.codex/**` task, lock, and run state. diff --git a/docs/plans/2026-06-25-task-orchestration-bounded-parallelism-ccgs-adaptation.md b/docs/plans/2026-06-25-task-orchestration-bounded-parallelism-ccgs-adaptation.md new file mode 100644 index 0000000..d27c8a5 --- /dev/null +++ b/docs/plans/2026-06-25-task-orchestration-bounded-parallelism-ccgs-adaptation.md @@ -0,0 +1,1083 @@ +# Task Orchestration, Bounded Parallelism, and Curated CCGS Adaptation Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** Add explicit local task orchestration with bounded parallel execution, and adapt the useful Claude Code Game Studios (CCGS) role/skill/workflow surface into Open Game Studio without importing Claude-specific machinery or unbounded orchestration. + +**Architecture:** Keep Open Game Studio local-first and Codex-native. Extend the existing `.codex/tasks.json`, `.codex/runs/**`, role packages, workflow registry, template registry, approvals, and validation systems. Orchestration is a foreground CLI command that plans, locks, executes, verifies, reviews, and records bounded task runs; it is not a daemon, hosted scheduler, hidden planner, or generic workflow DAG engine. + +**Tech Stack:** TypeScript ESM on Node 24, Commander CLI, Vitest, existing Codex runtime, existing project validation, Truthmark-backed docs. + +--- + +## 1. Product Boundary Decisions + +### 1.1 In scope + +- `opengamestudio task orchestrate --project ` as the primary orchestration entrypoint. +- Bounded task DAG execution using explicit task dependencies. +- Bounded parallelism with an explicit `--max-concurrency` flag and a hard product cap. +- Local reviewable state in `.codex/tasks.json`, `.codex/locks/**`, and `.codex/runs//**`. +- Existing Codex role execution, approval gates, sandbox/write policy, verification, review, and fix-pass behavior reused per task. +- Curated CCGS adaptation as Codex-native roles, workflow recipes, templates, and optional project-local `custom-*` overlays. + +### 1.2 Out of scope + +- Hosted orchestration, accounts, remote queues, remote artifact storage, billing, or server-side scheduling. +- Background autonomous loops or daemon workers. +- Unbounded parallelism. +- Hidden planner/`next` command behavior. +- Generated `CODEX.md`, `.gamestudio/**`, `project_orchestrator.md`, or Claude hook/skill runtime compatibility. +- General workflow DAGs unrelated to game-studio tasks. + +### 1.3 Closed decisions + +1. **Default concurrency is serial.** `task orchestrate` defaults to `--max-concurrency 1`. +2. **Parallelism is opt-in and capped.** The first implementation allows `--max-concurrency 1..3`; values above `3` fail with a clear error. +3. **Parallel mutating tasks require declared write sets.** A task without declared `writeFiles` uses a conservative project-wide write lock when file edits are allowed. +4. **No separate scheduler process.** Orchestration runs in the current foreground CLI process and exits when the bounded run completes. +5. **No hidden task generation.** CCGS workflow adaptation may create task graphs only through explicit commands that show the planned tasks before writing or executing them. +6. **CCGS adaptation is curated, not mirrored.** Use CCGS as source material, but translate into existing Codex-native IDs, bounded context selection, and Open Game Studio's role/workflow/template contracts. + +### 1.4 Review follow-up constraints + +- Orchestration planning/preflight must be side-effect-free. Do not call any API path that writes `.codex/runs/**`, `.codex/tasks.json`, or `.codex/locks/**` before approvals, dependency validation, and lock planning pass. +- The orchestrator must serialize all `.codex/tasks.json` writes during parallel execution. +- In the first implementation, `writeFiles` are normalized literal project-relative file paths only. Reject globs, directories, `.git`, escaping paths, and control characters. +- Approval matching for mutating task runs is based on `writeFiles`; `files` are read/context inputs only. + +--- + +## 2. Current Repository Baseline + +Relevant existing files: + +- `src/tasks.ts` owns `.codex/tasks.json`, task creation, task status updates, and `task run` integration. +- `src/runner.ts` owns role-run preparation, prompt cache metadata, Codex execution, verification, review, and fix passes. +- `src/codex-runtime.ts` owns Codex CLI command construction and availability checks. +- `src/cli.ts` exposes `run`, `task create`, `task run`, workflow shortcuts, approvals, templates, and validation. +- `src/roles.ts` contains built-in Codex role packages. +- `src/workflows.ts` contains prompt-only workflow registry entries. +- `src/templates.ts` contains package template registry and selection. +- `src/customization.ts` validates extend-only project-local `custom-*` roles, workflows, and templates. +- `src/validation.ts` validates repo/package/project behavior and future-only surfaces. +- `tests/tasks.test.ts`, `tests/runner.test.ts`, `tests/codex-runtime.test.ts`, `tests/functionality-gap-pass.test.ts`, `tests/customization.test.ts`, and `tests/validation.test.ts` are the main test anchors. + +Current constraints to preserve: + +- Every relative TypeScript import must use emitted `.js` specifiers. +- `run ` remains the primary single-role Codex execution path. +- `--dry-run` and `--print-prompt` remain inspection-only. +- Workflow shortcuts remain render-only unless explicitly converted into task-graph creation commands. +- Unknown legacy CCGS underscore role IDs remain invalid public role IDs. + +--- + +## 3. Task Data Model + +### 3.1 Upgrade `.codex/tasks.json` to schema version 2 + +Modify `src/tasks.ts` types and parser so schema v1 stores still read and normalize into schema v2 in memory. + +```ts +export type StudioTaskStatus = "ready" | "running" | "blocked" | "done" | "cancelled" | "skipped"; + +export type StudioTaskDependency = { + taskId: string; + requiredStatus: "done"; +}; + +export type StudioTaskRunPolicy = { + maxFixPasses?: number; + review?: boolean; + constrainedSandbox?: boolean; +}; + +export type StudioTask = { + id: string; + title: string; + role: StudioRoleId; + status: StudioTaskStatus; + files: string[]; + writeFiles: string[]; + dependencies: StudioTaskDependency[]; + workflowId?: string; + groupId?: string; + priority: number; + verification?: VerificationCommand; + runPolicy?: StudioTaskRunPolicy; + notes: string[]; + createdAt: string; + updatedAt: string; + lastRunId?: string; +}; + +export type TaskStore = { + schemaVersion: 2; + tasks: StudioTask[]; +}; +``` + +### 3.2 Migration behavior + +- Existing schema v1 task fields map as: + - `files` remains selected read/context files. + - `writeFiles` becomes `[]`. + - `dependencies` becomes `[]`. + - `priority` becomes `0`. + - `createdAt` and `updatedAt` become a deterministic migration timestamp only when the store is rewritten. +- `readTaskStore()` may return normalized v2 data without rewriting. +- `writeTaskStore()` always writes schema v2. +- Missing `writeFiles` means parallel mutating execution falls back to project-wide lock, not unsafe optimism. + +### 3.3 Task creation CLI additions + +Extend `task create`: + +```bash +opengamestudio task create --project projects/demo \ + --role gameplay-programmer \ + --file documentation/design/gdd.md \ + --write-file source/project-demo/player.gd \ + --depends-on task-001 \ + --workflow vertical-slice \ + --priority 10 \ + --verify-command npm --verify-arg run --verify-arg validate --verify-arg -- --verify-arg --project --verify-arg projects/demo \ + "Implement player jump" +``` + +Rules: + +- `--file` is context/read input. +- `--write-file` is declared mutation scope and lock input. +- `--depends-on` may repeat. +- `--workflow` records source workflow ID but does not imply hidden execution. +- Unknown dependencies fail before writing. +- Cycles are checked when orchestrating, not when creating one task, so users can assemble a graph incrementally. + +--- + +## 4. Orchestration State and Locking + +### 4.1 Run directory + +Each orchestration invocation creates: + +```text +.codex/runs// + orchestration.json + events.jsonl + tasks// + prompt.md + metadata.json + output.txt +``` + +`orchestration.json` records: + +```ts +type OrchestrationRunMetadata = { + schemaVersion: 1; + product: "codex-game-studio"; + runId: string; + startedAt: string; + finishedAt?: string; + projectRoot: string; + maxConcurrency: number; + requestedTaskIds: string[]; + selectedTaskIds: string[]; + dryRun: boolean; + review: boolean; + fix: boolean; + status: "planned" | "running" | "done" | "blocked" | "cancelled"; + summary: Array<{ taskId: string; status: StudioTaskStatus; runPath?: string; reason?: string }>; +}; +``` + +`events.jsonl` appends deterministic event records: + +- `orchestration.started` +- `task.eligible` +- `task.locked` +- `task.started` +- `task.finished` +- `task.blocked` +- `task.skipped` +- `task.unlocked` +- `orchestration.finished` + +### 4.2 Lock files + +Use a reviewable lock directory: + +```text +.codex/locks/ + .json +``` + +Lock file shape: + +```ts +type TaskLock = { + schemaVersion: 1; + lockKey: string; + taskId: string; + orchestrationRunId: string; + role: string; + writeFile: string; + acquiredAt: string; + expiresAt: string; + releasedAt?: string; +}; +``` + +Implementation details: + +- Derive `lockKey` from canonical project-relative write path or conservative key `__project_write__`. +- Acquire with exclusive create (`fs.openSync(path, "wx")`) so concurrent CLI processes cannot silently share a write lock. +- Release by rewriting the lock with `releasedAt` and then removing it. +- If a stale lock exists past `expiresAt`, fail closed first; add a later explicit `task lock cleanup` command only after the basic orchestrator is stable. +- Read-only tasks do not acquire write locks. +- Mutating tasks with no `writeFiles` acquire `__project_write__`, making them serial with all other mutating tasks. + +### 4.3 Conflict rules + +Two tasks may run together only when all are true: + +- Both have all dependencies satisfied. +- Neither is `running`, `done`, `cancelled`, or `skipped`. +- Their required approval/write-policy checks pass. +- Their lock sets do not conflict. +- The current running count is below `maxConcurrency`. +- They do not require the conservative project-wide write lock at the same time as any other mutating task. + +--- + +## 5. Orchestration Engine + +Create `src/orchestrator.ts`. + +Core API: + +```ts +export type OrchestrateOptions = { + project: string; + taskIds?: string[]; + workflowId?: string; + maxConcurrency?: number; + dryRun?: boolean; + review?: boolean; + fix?: boolean; + maxFixPasses?: number; + approvedByUser?: boolean; + constrainedSandbox?: boolean; + approvalScope?: string[]; + codexBin?: string; +}; + +export type OrchestrationResult = { + runId: string; + status: "planned" | "done" | "blocked"; + selectedTaskIds: string[]; + startedTaskIds: string[]; + blockedTaskIds: string[]; + skippedTaskIds: string[]; + output: string; +}; + +export async function orchestrateTasks(options: OrchestrateOptions): Promise; +``` + +Algorithm: + +1. Resolve project root. +2. Read and normalize task store. +3. Select tasks: + - explicit `taskIds` if provided; + - else tasks matching `workflowId` if provided; + - else all `ready` tasks. +4. Validate graph: + - no unknown dependencies; + - no cycles among selected tasks and their required dependencies; + - no selected task depends on a `blocked`, `cancelled`, or `skipped` task unless user explicitly selected only downstream dry-run inspection; + - no concurrency above hard cap. +5. Prepare every selected task using `prepareRun()` before starting any non-dry run. +6. In strict/guided modes, fail closed before starting any task if required approvals are missing. +7. For dry-run, print planned waves, lock sets, selected context, Codex command previews, and approval diagnostics; write no task/runs/locks state. +8. For execution: + - mark orchestrator run as `running`; + - compute ready wave; + - acquire locks for up to `maxConcurrency` tasks; + - start Codex lifecycle for each task; + - stream or buffer task output into per-task run output; + - update task status to `done` or `blocked`; + - release locks; + - recompute ready wave until no runnable tasks remain. +9. Tasks whose dependencies cannot be satisfied because another task blocked become `skipped` with a note naming the blocker. +10. Final orchestrator status is `done` only if every selected task is `done`; otherwise `blocked`. + +--- + +## 6. Codex Runtime Changes for Parallelism + +Current `executeRunLifecycle()` is `async` but calls synchronous Codex spawning. Parallel orchestration needs real asynchronous execution. + +Modify `src/codex-runtime.ts` to add: + +```ts +export async function executeCodexCommand( + command: { command: string; args: string[] }, + input: string, + options: { cwd: string; timeoutMs?: number } +): Promise; +``` + +Modify `src/runner.ts`: + +- Keep sync helpers only for tests or deprecate them internally. +- Update implementation, review, and fix pass execution to use the async function. +- Preserve existing output formatting and final status semantics. +- Add timeout support later only if a concrete test requires it; do not add global scheduler timeouts in the first pass. + +Testing requirement: + +- Use fake Codex binaries that sleep and write deterministic output to prove `--max-concurrency 2` completes faster than serial without relying on real Codex. +- Do not call hosted LLMs in tests. + +--- + +## 7. CLI Design + +### 7.1 New command + +```bash +opengamestudio task orchestrate --project [task-id...] +``` + +Options: + +```text +--workflow select ready tasks from one workflow/group +--max-concurrency default 1, allowed 1..3 +--dry-run show plan, locks, approvals, and commands; no mutation +--review run per-task review pass +--fix run bounded per-task fix passes +--max-fix-passes reuse existing task run behavior +--approval-scope repeatable diagnostic/approval scope +--approved-by-user guided-studio local override +--constrained-sandbox use workspace-write instead of full-access sandbox +``` + +Examples: + +```bash +opengamestudio task orchestrate --project projects/demo --dry-run +opengamestudio task orchestrate --project projects/demo --max-concurrency 2 --review --fix +opengamestudio task orchestrate --project projects/demo --workflow vertical-slice --max-concurrency 2 +opengamestudio task orchestrate --project projects/demo task-001 task-002 task-003 --dry-run +``` + +### 7.2 Help surface guardrails + +- Help may mention `orchestrate` and `--max-concurrency`. +- Help must not expose `next`, `telemetry`, hosted orchestration, daemon mode, or unbounded parallel options. +- Existing future-surface guard tests should be updated from "no parallel at all" to "no unbounded/hosted/background parallelism". + +--- + +## 8. Workflow-to-Task Recipes + +Prompt-only workflows should remain prompt-only. Add explicit recipe commands for workflows that should produce task graphs. + +Create `src/workflow-recipes.ts`. + +```ts +export type WorkflowTaskRecipe = { + workflowId: WorkflowId | string; + title: string; + tasks: Array<{ + title: string; + role: StudioRoleId; + files: string[]; + writeFiles: string[]; + dependencies: string[]; // local recipe keys, not final task IDs + verification?: VerificationCommand; + }>; +}; +``` + +Add CLI: + +```bash +opengamestudio workflow create-tasks --project --dry-run +opengamestudio workflow create-tasks --project +``` + +Rules: + +- `--dry-run` prints proposed tasks and dependency graph, writes nothing. +- Non-dry writes tasks to `.codex/tasks.json` with `workflowId` and `groupId`. +- Recipe-local dependency keys are resolved to real task IDs after creation. +- The command does not run Codex. Users run `task orchestrate` explicitly. + +Initial recipe set: + +1. `vertical-slice` + - producer plans slice + - game-designer writes acceptance/spec detail + - gameplay-programmer implements core loop + - technical-artist or sound-designer handles asset/audio hook if declared + - qa-playtester reviews and verifies +2. `bugfix` + - qa-playtester reproduces/records expected behavior + - gameplay-programmer fixes + - qa-playtester verifies +3. `ui-ux-review` + - ui-ux-designer reviews flow + - ui-programmer implements bounded UI fix if needed + - accessibility-specialist reviews accessibility gaps +4. `release-checklist` + - qa-playtester validates evidence + - performance-analyst checks performance risks + - security-engineer checks release/security risks + - release-manager synthesizes ship/no-ship + +Do not create all CCGS team workflows in the first pass. Add recipes only when their lock/dependency/write-set behavior is obvious and testable. + +--- + +## 9. Curated CCGS Adaptation Design + +### 9.1 Source inventory + +Reference source inspected for this design: + +- `Donchitos/Claude-Code-Game-Studios` +- `.claude/agents`: 49 Claude agents +- `.claude/skills`: 73 Claude skills +- `.claude/hooks`: Claude hook runtime files +- `.claude/rules`: Claude-specific rule files + +Important translation principle: CCGS is a rich reference library, not an implementation contract. Open Game Studio adapts outcomes into local Codex-native primitives. + +### 9.2 Role adaptation policy + +Role decisions use four categories: + +| Decision | Meaning | +|---|---| +| `built-in-existing` | Already represented by an Open Game Studio role package. Improve prompt depth only if tests show a gap. | +| `built-in-add` | Add a new canonical hyphenated Open Game Studio role. | +| `specialty-context` | Do not add a role; adapt as engine/module/plugin reference context selected by task keywords. | +| `custom-pack-example` | Keep as project-local `custom-*` example or docs, not built-in product surface. | + +### 9.3 CCGS role mapping + +| CCGS role | Open Game Studio target | Decision | +|---|---|---| +| `producer` | `producer` | built-in-existing | +| `creative-director` | `creative-director` | built-in-existing | +| `game-designer` | `game-designer` / `senior-game-designer` | built-in-existing | +| `systems-designer` | `systems-designer` | built-in-existing | +| `economy-designer` | `economy-designer` | built-in-existing | +| `level-designer` | `level-designer` | built-in-existing | +| `world-builder` | `world-builder` | built-in-existing | +| `writer` | `writer` | built-in-existing | +| `gameplay-programmer` | `gameplay-programmer` | built-in-existing | +| `ai-programmer` | `ai-programmer` | built-in-existing | +| `network-programmer` | `network-programmer` | built-in-existing | +| `ui-programmer` | `ui-programmer` | built-in-existing | +| `engine-programmer` | `engine-programmer` | built-in-existing | +| `tools-programmer` | `tools-programmer` | built-in-existing | +| `technical-director` | `technical-director` | built-in-existing | +| `devops-engineer` | `devops-engineer` | built-in-existing | +| `security-engineer` | `security-engineer` | built-in-existing | +| `performance-analyst` | `performance-analyst` | built-in-existing | +| `technical-artist` | `technical-artist` | built-in-existing | +| `audio-director` | `audio-director` | built-in-existing | +| `sound-designer` | `sound-designer` | built-in-existing | +| `accessibility-specialist` | `accessibility-specialist` | built-in-existing | +| `localization-lead` | `localization-lead` | built-in-existing | +| `live-ops-designer` | `live-ops-designer` | built-in-existing | +| `community-manager` | `community-manager` | built-in-existing | +| `release-manager` | `release-manager` | built-in-existing | +| `godot-specialist` | active-engine `godot-specialist` | built-in-existing | +| `unity-specialist` | active-engine `unity-specialist` | built-in-existing | +| `unreal-specialist` | active-engine `unreal-specialist` | built-in-existing | +| `analytics-engineer` | `data-scientist` plus analytics templates | built-in-existing, prompt-depth improvement | +| `art-director` | `senior-game-artist` plus art-direction workflow | built-in-existing, maybe rename not needed | +| `narrative-director` | `narrative-designer` plus `world-builder` | built-in-existing, prompt-depth improvement | +| `ux-designer` | `ui-ux-designer` | built-in-existing | +| `qa-lead` | add `qa-lead` only if QA planning/release strategy needs a separate owner | built-in-add candidate | +| `qa-tester` | `qa-playtester`; maybe add `qa-tester` later for test-case execution | defer unless tests show split needed | +| `lead-programmer` | add `lead-programmer` if technical-director is too broad for code review/refactor ownership | built-in-add candidate | +| `prototyper` | keep as `prototype` workflow/recipe, not role | specialty workflow | +| Godot sub-specialists | engine references selected by task keywords | specialty-context | +| Unity sub-specialists | engine references selected by task keywords | specialty-context | +| Unreal sub-specialists | engine references selected by task keywords | specialty-context | + +First role additions, if any, should be only: + +1. `lead-programmer` — code architecture, code review, refactor strategy, programming work assignment. +2. `qa-lead` — QA strategy, bug triage, test plan ownership, release quality gates. + +Do not add every engine sub-specialist as a first-class role. Use active-engine references and templates instead. + +### 9.4 CCGS skill adaptation policy + +Do not generate `.claude/skills` or implement a Claude skill runtime. Convert CCGS skills into one of these Open Game Studio surfaces: + +| CCGS skill kind | Open Game Studio surface | +|---|---| +| Planning or review skill | built-in workflow prompt or workflow task recipe | +| Structured output document | package template | +| Team coordination skill | explicit workflow task recipe with dependencies | +| Maintenance skill for Claude skills/hooks | out of scope or project-local example only | +| Hook/rule-driven behavior | explicit CLI option, validation check, or docs; never hidden hook behavior | + +### 9.5 Initial CCGS skill decisions + +Already covered or mostly covered: + +- `architecture-decision` → existing workflow/template. +- `architecture-review` → existing workflow/template. +- `brainstorm` → existing workflow. +- `bug-triage` / `bug-report` → `bugfix` workflow plus future bug-report template if needed. +- `create-epics` → existing workflow. +- `create-stories` → existing workflow. +- `hotfix` → existing workflow. +- `onboard` / `start` → existing workflow aliases. +- `perf-profile` → existing workflow. +- `playtest-report` → `playtest` workflow/template. +- `prototype` → existing workflow, future task recipe. +- `qa-plan` → existing workflow/template. +- `regression-suite` → existing workflow. +- `release-checklist` / `launch-checklist` → existing release workflow; add alias if needed. +- `security-audit` → existing workflow. +- `sprint-plan` → existing workflow. +- `sprint-status` → existing workflow. +- `story-readiness` → existing workflow. +- `story-done` → existing workflow. +- `ux-review` → `ui-ux-review` workflow. +- `vertical-slice` → existing workflow, future task recipe. + +High-value additions: + +- `gate-check` → new workflow for stage readiness verdict. +- `project-stage-detect` → new read-only workflow for repo state audit and recommended next action. +- `scope-check` → new workflow/template for scope risk and feature cut decisions. +- `estimate` → new producer workflow/template for rough schedule/complexity estimates. +- `tech-debt` → new lead-programmer or technical-director workflow/template. +- `smoke-check` → new QA/release workflow with minimal validation checklist. +- `test-evidence-review` → new QA workflow for evidence completeness. +- `asset-audit` → new technical-artist/senior-game-artist workflow/template. +- `asset-spec` → new art-direction template/workflow. +- `balance-check` → new systems/economy design workflow. +- `map-systems` → new systems-design workflow/template. +- `reverse-document` → new documentation workflow that derives missing docs from implementation. +- `propagate-design-change` → new architecture/design impact workflow using traceability docs. +- `create-control-manifest` → new technical-director workflow/template after ADRs are accepted. +- `ux-design` → new UI/UX design workflow distinct from review. + +Team skills become recipes, not prompt-only aliases: + +- `team-combat` → game-designer → gameplay-programmer/ai-programmer/sound-designer → qa-playtester. +- `team-ui` → ui-ux-designer → ui-programmer → accessibility-specialist → qa-playtester. +- `team-audio` → audio-director → sound-designer/technical-artist → gameplay-programmer integration → qa-playtester. +- `team-qa` → qa-lead/qa-playtester split, if `qa-lead` is added. +- `team-release` → release-manager with QA/perf/security dependencies. +- `team-polish` → producer/creative-director triage plus focused UI/audio/perf/QA tasks. +- `team-live-ops` → live-ops-designer/community-manager/data-scientist/release-manager sequence. +- `team-narrative` → narrative-designer/world-builder/writer/localization-lead sequence. +- `team-level` → level-designer/gameplay-programmer/technical-artist/qa-playtester sequence. + +Defer or keep out of product: + +- `adopt`, `help`, `skill-improve`, `skill-test`, `test-helpers` as Claude-skill maintenance concepts. +- Claude hook/rule-only mechanics unless translated into explicit validation or CLI flags. +- Any CCGS skill that depends on persistent Claude memory or hidden hooks. + +--- + +## 10. Implementation Tasks + +### Task 1: Add task schema v2 tests + +**Objective:** Lock the task-store migration contract before changing implementation. + +**Files:** + +- Modify: `tests/tasks.test.ts` +- Later modify: `src/tasks.ts` + +**Steps:** + +1. Add a test that writes a schema v1 `.codex/tasks.json` and expects `readTaskStore()` to return schema v2 with empty dependencies/writeFiles and valid timestamps. +2. Add a test that `writeTaskStore()` writes `schemaVersion: 2`. +3. Run: + ```bash + npx vitest run tests/tasks.test.ts -t "task store" + ``` +4. Expected before implementation: failure because schema v2 is not implemented. + +### Task 2: Implement task schema v2 normalization + +**Objective:** Support old task stores while writing the new shape. + +**Files:** + +- Modify: `src/tasks.ts` +- Modify: `tests/tasks.test.ts` + +**Steps:** + +1. Update task types. +2. Add normalization helpers. +3. Preserve v1 parsing behavior. +4. Ensure status validation accepts `cancelled` and `skipped`. +5. Run: + ```bash + npx vitest run tests/tasks.test.ts + ``` +6. Expected: pass. + +### Task 3: Extend task creation CLI + +**Objective:** Let users declare dependencies, context files, write files, workflow/group metadata, and priority. + +**Files:** + +- Modify: `src/tasks.ts` +- Modify: `src/cli.ts` +- Modify: `tests/tasks.test.ts` +- Modify: `tests/cli-prompt-surface.test.ts` if CLI help assertions need updates. + +**Steps:** + +1. Add `createTask()` input fields. +2. Add `--file`, `--write-file`, `--depends-on`, `--workflow`, and `--priority` options. +3. Validate project-safe relative paths using the same path rules used by customizations/context selection. +4. Test duplicate dependencies and unknown dependency IDs. +5. Run: + ```bash + npx vitest run tests/tasks.test.ts tests/cli-prompt-surface.test.ts + ``` + +### Task 4: Add asynchronous Codex execution + +**Objective:** Make parallel execution possible without blocking the event loop on `spawnSync`. + +**Files:** + +- Modify: `src/codex-runtime.ts` +- Modify: `src/runner.ts` +- Modify: `tests/codex-runtime.test.ts` +- Modify: `tests/runner.test.ts` + +**Steps:** + +1. Add `executeCodexCommand()` using `node:child_process` `spawn`. +2. Preserve `CodexExecutionResult` shape. +3. Update implementation/review/fix passes to await async execution. +4. Keep current output formatting unchanged. +5. Run: + ```bash + npx vitest run tests/codex-runtime.test.ts tests/runner.test.ts + ``` + +### Task 5: Add lock acquisition tests + +**Objective:** Define lock behavior before implementation. + +**Files:** + +- Create: `tests/orchestrator-locks.test.ts` +- Create later: `src/orchestrator-locks.ts` + +**Steps:** + +1. Test two tasks with disjoint `writeFiles` can both acquire locks. +2. Test overlapping write file lock acquisition fails for the second task. +3. Test missing `writeFiles` uses `__project_write__`. +4. Test released locks are removed or marked released according to final implementation choice. +5. Run: + ```bash + npx vitest run tests/orchestrator-locks.test.ts + ``` +6. Expected before implementation: failure because module does not exist. + +### Task 6: Implement lock store + +**Objective:** Provide atomic file-backed locks for bounded parallel execution. + +**Files:** + +- Create: `src/orchestrator-locks.ts` +- Modify: `tests/orchestrator-locks.test.ts` + +**Steps:** + +1. Implement canonical lock key generation. +2. Implement exclusive lock creation with `fs.openSync(path, "wx")`. +3. Implement release cleanup. +4. Implement stale lock diagnostics but do not auto-clean stale locks yet. +5. Run: + ```bash + npx vitest run tests/orchestrator-locks.test.ts + ``` + +### Task 7: Add orchestration graph tests + +**Objective:** Define dependency selection, cycle detection, and skipped-task behavior. + +**Files:** + +- Create: `tests/orchestrator.test.ts` +- Create later: `src/orchestrator.ts` + +**Steps:** + +1. Test ready tasks with dependencies are ordered in waves. +2. Test cycle detection fails before mutation. +3. Test a blocked dependency causes downstream tasks to become `skipped`. +4. Test `--max-concurrency 4` fails because first cap is 3. +5. Run: + ```bash + npx vitest run tests/orchestrator.test.ts + ``` + +### Task 8: Implement dry-run orchestration planning + +**Objective:** Add `orchestrateTasks()` dry-run mode without mutation. + +**Files:** + +- Create: `src/orchestrator.ts` +- Modify: `src/tasks.ts` if helper exports are needed. +- Modify: `tests/orchestrator.test.ts` + +**Steps:** + +1. Implement task selection. +2. Implement dependency graph validation. +3. Implement wave planning. +4. Reuse `prepareRun()` to show eligibility and commands. +5. Assert dry-run writes no `.codex/runs/**`, locks, or task status changes. +6. Run: + ```bash + npx vitest run tests/orchestrator.test.ts + ``` + +### Task 9: Implement serial orchestration execution + +**Objective:** Make `maxConcurrency: 1` execute selected tasks safely. + +**Files:** + +- Modify: `src/orchestrator.ts` +- Modify: `tests/orchestrator.test.ts` + +**Steps:** + +1. Create orchestration run directory. +2. Write `orchestration.json` and append `events.jsonl`. +3. Execute tasks one at a time using `executeTaskRun()` or a shared lower-level lifecycle helper. +4. Update task status and `lastRunId`. +5. Mark downstream tasks skipped when blockers occur. +6. Run: + ```bash + npx vitest run tests/orchestrator.test.ts tests/tasks.test.ts + ``` + +### Task 10: Implement bounded parallel orchestration + +**Objective:** Execute non-conflicting ready tasks concurrently up to the cap. + +**Files:** + +- Modify: `src/orchestrator.ts` +- Modify: `tests/orchestrator.test.ts` + +**Steps:** + +1. Start ready tasks in batches constrained by lock availability and `maxConcurrency`. +2. Await task promises with failure isolation. +3. Release locks in `finally` blocks. +4. Add fake Codex sleep tests proving concurrency without hosted calls. +5. Run: + ```bash + npx vitest run tests/orchestrator.test.ts + ``` + +### Task 11: Add CLI command + +**Objective:** Expose orchestration through `opengamestudio task orchestrate`. + +**Files:** + +- Modify: `src/cli.ts` +- Modify: `tests/cli-prompt-surface.test.ts` +- Modify: `tests/functionality-gap-pass.test.ts` + +**Steps:** + +1. Add command and options. +2. Print dry-run wave plan and execution summary. +3. Set nonzero exit code when orchestrator status is blocked. +4. Update future-surface tests so `parallel` is not blanket-forbidden, but hosted/unbounded/background surfaces remain forbidden. +5. Run: + ```bash + npx vitest run tests/cli-prompt-surface.test.ts tests/functionality-gap-pass.test.ts + ``` + +### Task 12: Add workflow recipe tests + +**Objective:** Define explicit workflow-to-task creation without hidden execution. + +**Files:** + +- Create: `tests/workflow-recipes.test.ts` +- Create later: `src/workflow-recipes.ts` + +**Steps:** + +1. Test `vertical-slice` dry-run prints proposed tasks and dependencies without writing. +2. Test non-dry creates tasks with `workflowId`, `groupId`, dependencies, files, and writeFiles. +3. Test recipe creation does not call Codex. +4. Run: + ```bash + npx vitest run tests/workflow-recipes.test.ts + ``` + +### Task 13: Implement initial workflow recipes + +**Objective:** Add task graph creation for a small high-value workflow set. + +**Files:** + +- Create: `src/workflow-recipes.ts` +- Modify: `src/cli.ts` +- Modify: `tests/workflow-recipes.test.ts` + +**Steps:** + +1. Implement `vertical-slice`, `bugfix`, `ui-ux-review`, and `release-checklist` recipes. +2. Add `workflow create-tasks ` CLI. +3. Keep workflow shortcut commands render-only. +4. Run: + ```bash + npx vitest run tests/workflow-recipes.test.ts tests/functionality-gap-pass.test.ts + ``` + +### Task 14: Add CCGS adaptation registry tests + +**Objective:** Make the curated CCGS adaptation decisions executable and reviewable. + +**Files:** + +- Create: `tests/ccgs-adaptation.test.ts` +- Create later: `src/ccgs-adaptation.ts` + +**Steps:** + +1. Test every listed CCGS role has an adaptation decision. +2. Test no legacy underscore role IDs become built-in role IDs. +3. Test high-value skill additions are categorized as workflow/template/recipe/deferred. +4. Run: + ```bash + npx vitest run tests/ccgs-adaptation.test.ts + ``` + +### Task 15: Implement CCGS adaptation registry + +**Objective:** Record curated adaptation decisions in code, not only docs. + +**Files:** + +- Create: `src/ccgs-adaptation.ts` +- Modify: `tests/ccgs-adaptation.test.ts` +- Modify: `src/validation.ts` if validation should report registry coverage. + +**Steps:** + +1. Add role decision table. +2. Add skill decision table. +3. Add helper functions for reporting unmapped/high-value candidates. +4. Optionally add validation diagnostics for registry consistency. +5. Run: + ```bash + npx vitest run tests/ccgs-adaptation.test.ts tests/validation.test.ts + ``` + +### Task 16: Add first curated roles only if justified + +**Objective:** Add no more than `lead-programmer` and `qa-lead` as built-ins if tests show current roles cannot own those workflows cleanly. + +**Files:** + +- Modify: `src/roles.ts` +- Modify: `src/config.ts` +- Modify: `src/agents.ts` if generated prompt coverage changes. +- Modify: `tests/roles.test.ts` +- Modify: `tests/functionality-gap-pass.test.ts` + +**Steps:** + +1. Add failing tests for role package presence and active-role selection. +2. Add role packages with concise responsibilities, expected outputs, quality gates, and handoff templates. +3. Do not add engine sub-specialist roles. +4. Run: + ```bash + npx vitest run tests/roles.test.ts tests/functionality-gap-pass.test.ts + ``` + +### Task 17: Add high-value CCGS-derived workflows/templates + +**Objective:** Fill real workflow gaps without importing all CCGS skills. + +**Files:** + +- Modify: `src/workflows.ts` +- Modify: `src/templates.ts` +- Add package templates under `templates/` only where structured output is needed. +- Modify: `tests/functionality-gap-pass.test.ts` +- Modify: `tests/agents-templates.test.ts` + +**Steps:** + +1. Add only the first batch: `gate-check`, `project-stage-detect`, `scope-check`, `estimate`, `tech-debt`, `smoke-check`, `test-evidence-review`, `asset-audit`, `balance-check`, `map-systems`, `ux-design`. +2. Add templates only for workflows that need durable structured artifacts. +3. Keep template selection bounded. +4. Run: + ```bash + npx vitest run tests/functionality-gap-pass.test.ts tests/agents-templates.test.ts + ``` + +### Task 18: Update validation and future-surface guards + +**Objective:** Validate orchestration without allowing hosted/unbounded drift. + +**Files:** + +- Modify: `src/validation.ts` +- Modify: `src/behavioral-evaluation.ts` +- Modify: `tests/validation.test.ts` +- Modify: `tests/behavioral-evaluation.test.ts` + +**Steps:** + +1. Add validation checks for task schema v2, orchestration command availability, lock directory safety, and recipe registry consistency. +2. Update forbidden drift phrases to forbid hosted/background/unbounded orchestration, not explicit local bounded orchestration. +3. Add absence checks for daemon/hosted/unbounded CLI/help/config surfaces. +4. Run: + ```bash + npx vitest run tests/validation.test.ts tests/behavioral-evaluation.test.ts + ``` + +### Task 19: Update docs and generated truth surfaces + +**Objective:** Keep product docs, architecture docs, and Truthmark docs in sync with implemented behavior. + +**Files:** + +- Modify: `README.md` +- Modify: `docs/development-rules.md` +- Modify: `docs/known-upstream-differences.md` +- Modify: `docs/migration-from-claude.md` +- Modify: `docs/workflow-validation.md` +- Modify: `docs/architecture/flows/role-run-lifecycle.md` +- Create: `docs/architecture/flows/task-orchestration.md` +- Modify relevant `docs/truthmark/**` docs after code behavior lands. + +**Steps:** + +1. Document `task orchestrate` and `workflow create-tasks` examples. +2. Document bounded parallelism cap and lock behavior. +3. Document CCGS adaptation as curated translation, not parity-by-copying. +4. Run Truthmark refresh only if `truthmark check` reports stale surfaces: + ```bash + npx truthmark check + npx truthmark index + ``` + +### Task 20: Full verification + +**Objective:** Prove the feature works and product boundaries remain intact. + +Run: + +```bash +npm run typecheck +npm run build +npm test +npm run validate +npx truthmark check +npx truthmark index +git diff --check +``` + +Expected: + +- Typecheck passes. +- Build passes. +- Tests pass. +- Validation reports bounded local orchestration as implemented. +- Validation still reports hosted/background/unbounded orchestration surfaces absent. +- Truthmark check/index pass. +- Diff has no whitespace errors. + +--- + +## 11. Acceptance Criteria + +Implementation is complete when all are true: + +1. Existing `task run` behavior remains compatible. +2. Schema v1 task stores still read correctly. +3. `task create` supports dependencies, read files, write files, workflow IDs, and priority. +4. `task orchestrate --dry-run` writes no files and prints task waves, lock sets, approvals, and commands. +5. `task orchestrate` serial mode runs ready tasks in dependency order. +6. `task orchestrate --max-concurrency 2` runs non-conflicting tasks concurrently in tests. +7. Conflicting write sets do not run concurrently. +8. Tasks without write sets do not run concurrently with mutating tasks. +9. Strict-studio approvals are checked before any non-dry orchestration side effects. +10. Blocked tasks cause dependent tasks to become `skipped` with readable notes. +11. Orchestration run metadata and task outputs are persisted under `.codex/runs/**`. +12. Locks are released on success, failure, and thrown exceptions. +13. Help/validation exposes no hosted, daemon, background loop, or unbounded parallelism surface. +14. CCGS roles and skills have a curated adaptation registry with explicit keep/add/defer decisions. +15. Initial high-value CCGS additions improve OGS coverage without copying Claude-specific hooks/rules/skills wholesale. +16. Docs and Truthmark-backed behavior claims match code. + +--- + +## 12. Explicit Non-Goals for This Implementation + +- No hosted service. +- No remote worker. +- No background daemon. +- No auto-cleaning stale locks in the first pass. +- No generalized arbitrary DAG language. +- No unbounded `--max-concurrency 0` or `--max-concurrency unlimited` behavior. +- No automatic task generation from free-form LLM output without showing/writing reviewable task specs first. +- No import of `.claude/**` files into generated projects. +- No engine sub-specialist role explosion until selected-context references prove insufficient. + +--- + +## 13. Recommended Implementation Order + +1. Task schema v2. +2. CLI task creation enhancements. +3. Async Codex runtime. +4. Lock store. +5. Dry-run orchestration planner. +6. Serial orchestration execution. +7. Bounded parallel execution. +8. CLI command. +9. Workflow recipe creation. +10. CCGS adaptation registry. +11. First curated role/workflow/template additions. +12. Validation and docs. + +This order keeps each step testable and avoids shipping a broad orchestration surface before locking, approvals, and failure behavior are explicit. diff --git a/docs/truthmark/engineering/codex/roles-and-workflows.md b/docs/truthmark/engineering/codex/roles-and-workflows.md index 2c13179..24bc532 100644 --- a/docs/truthmark/engineering/codex/roles-and-workflows.md +++ b/docs/truthmark/engineering/codex/roles-and-workflows.md @@ -88,7 +88,8 @@ This doc was created from the editable engineering-behavior template at docs/tru - Every built-in template records description and role/workflow use hints. - The project config template must parse as JSON. - Workflow shortcuts render prompts only. -- Workflow shortcuts do not imply hidden planner, telemetry, ownership, or parallel orchestration behavior. +- `workflow create-tasks` is a separate explicit recipe path that writes file-backed tasks for supported workflows without launching Codex. +- Workflow shortcuts do not imply hidden planner, telemetry, ownership, hosted orchestration, background loops, or unbounded parallel behavior. Explicit local task orchestration is provided only through reviewable `.codex/**` task, lock, and run state. - Custom IDs must use the `custom-*` prefix. - Custom file references must be project-safe relative paths. - Custom entries must not replace built-in role, workflow, or template IDs. @@ -139,7 +140,8 @@ This doc was created from the editable engineering-behavior template at docs/tru - Decision (2026-05-28): Use Codex-native hyphenated role IDs as the user- and project-facing role contract. - Decision (2026-05-28): Keep workflow shortcuts render-only for this pass. -- Decision (2026-05-28): Keep future planner, next, telemetry, ownership enforcement, and parallel orchestration hidden. +- Decision (2026-05-28): Keep future planner, next, telemetry, ownership enforcement, hosted orchestration, background loops, and unbounded parallelism hidden. +- Decision (2026-06-25): Allow explicit local task orchestration as a product-boundary feature area when implemented through bounded selected context, file-backed task state, run metadata, locks, approvals, validation, and docs. - Decision (2026-06-13): Workflow prompts use the same context-contract renderer as role-run prompts. - Decision (2026-06-13): Workflow prompts include only selected workflow context. - Decision (2026-06-14): Add one specialist role ID per supported engine. diff --git a/docs/truthmark/engineering/codex/runtime-and-tasks.md b/docs/truthmark/engineering/codex/runtime-and-tasks.md index 3c51104..98ae970 100644 --- a/docs/truthmark/engineering/codex/runtime-and-tasks.md +++ b/docs/truthmark/engineering/codex/runtime-and-tasks.md @@ -10,7 +10,7 @@ last_reviewed: 2026-06-25 Runtime and task execution connects prepared Codex Game Studio prompts to the Codex CLI. -It also preserves explicit task state and runs bounded verification, review, and fix loops without hidden orchestration. +It also preserves explicit task state and runs bounded verification, review, and fix loops. The product boundary now allows local, file-backed task orchestration when task state, locks, approvals, selected context, run metadata, and failures stay reviewable. ## Scope @@ -25,6 +25,8 @@ It does not own role prompt content, project scaffolding, or public CLI help wor - A user invokes `run ... --project ` to render or execute a role prompt. - A user creates a file-backed task through `task create`. - A user runs a file-backed task through `task run`. +- A user orchestrates ready file-backed tasks through `task orchestrate`. +- A user creates explicit task graphs through `workflow create-tasks `. - A run includes structured verification, review, or bounded fix-pass options. ## Inputs @@ -35,6 +37,8 @@ It does not own role prompt content, project scaffolding, or public CLI help wor - A studio role ID or task ID. - A non-empty task or objective. - Optional included artifacts. +- Optional declared write files for task approval and orchestration locks. +- Optional task dependencies, workflow IDs, group IDs, and priority. - Optional verification command and arguments. - Optional review flag, fix flag, and max fix-pass count. @@ -44,7 +48,7 @@ Runtime execution prepares bounded Codex prompts before side effects. It evaluates studio write policy before mutation. -It records visible run and task state for non-inspection paths. It reports verification, review, and fix outcomes without hidden orchestration. +It records visible run and task state for non-inspection paths. It reports verification, review, and fix outcomes without hidden orchestration; future orchestration work must keep those outcomes explicit in `.codex/**` state. ## Execution Model @@ -100,6 +104,11 @@ It records visible run and task state for non-inspection paths. It reports verif - Implementation and fix phases classify mutating eligibility. - Allowed mutating policies map to `danger-full-access` unless constrained sandbox is explicitly requested. - Task runs mutate task status only for non-dry execution. +- Task orchestration preflights selected tasks without writing run, lock, or task state. +- Non-dry task orchestration records an orchestration run under `.codex/runs//`, writes per-task prompt/output metadata under `tasks//`, and uses `.codex/locks/` for transient write locks. +- Bounded parallel orchestration caps `--max-concurrency` at 3. +- Mutating orchestrated tasks without declared `writeFiles` use a conservative project-wide write lock. +- `files` are read/context inputs; `writeFiles` are mutation approval and lock inputs. ## Steps @@ -122,9 +131,11 @@ It records visible run and task state for non-inspection paths. It reports verif ## State, Retry, And Failure Behavior - Task stores live at `.codex/tasks.json`. -- Task stores use schema version 1 and unique `task-###` IDs. +- Task stores use schema version 2 and unique `task-###` IDs; schema version 1 stores are normalized on read and rewritten as version 2 when saved. - `task create` requires a valid studio project before writing task state. -- Task statuses are `ready`, `running`, `blocked`, and `done`. +- Task statuses are `ready`, `running`, `blocked`, `done`, `cancelled`, and `skipped`. +- Task dependency records require dependent tasks to reach `done`. +- Orchestration serializes task-store writes while bounded tasks execute. - Verification commands use bounded stdout and stderr capture. - Verification commands use a default timeout. - Timed-out verification receives SIGTERM, then SIGKILL after the configured grace period. @@ -140,6 +151,8 @@ It records visible run and task state for non-inspection paths. It reports verif - Print-prompt output is the deterministic prompt body. - Non-dry run output reports implementation, verification, review, fix-pass, and final-status summaries. - Task creation prints the new task ID. +- Task orchestration dry-runs print planned tasks, dependencies, locks, selected context, and Codex commands. +- Non-dry orchestration output reports per-task status and final orchestration status. ## Product Truth Links @@ -165,6 +178,9 @@ It records visible run and task state for non-inspection paths. It reports verif - Decision (2026-06-17): Route custom role runs through the same write-policy, sandbox, context, cache, and template contracts as built-in roles. - Decision (2026-06-17): Do not introduce a separate plugin runtime for custom roles. - Decision (2026-06-17): Honor review/fix flags for custom role runs with real lifecycle prompts. +- Decision (2026-06-25): Move explicit local task orchestration into the product boundary while keeping hosted orchestration, background loops, hidden planners, and unbounded parallelism out of scope. +- Decision (2026-06-25): Implement orchestration as a foreground `task orchestrate` command with side-effect-free preflight, schema-version-2 task state, transient `.codex/locks/`, and bounded concurrency capped at 3. +- Decision (2026-06-25): Bind task approval and orchestration locks to declared `writeFiles`; keep `files` as read/context inputs. - Decision (2026-06-17): Use a read-only QA review prompt and a bounded custom-role fix prompt. ## Rationale @@ -175,7 +191,8 @@ Non-dry runs use visible cache paths and verification output. Read-only review p ## Non-Goals -- This workflow does not implement hidden parallel execution. +- This workflow implements explicit bounded local task orchestration; it does not implement hidden parallel execution. +- This workflow does not implement hosted orchestration, background autonomous loops, or unbounded parallelism. - This workflow does not implement telemetry. - This workflow does not implement ownership enforcement. - This workflow does not implement a planner or next queue. @@ -197,10 +214,17 @@ Non-dry runs use visible cache paths and verification output. Read-only review p - ../../../../src/context-manifest.ts - ../../../../src/prompt-context.ts - ../../../../src/tasks.ts +- ../../../../src/orchestrator.ts +- ../../../../src/orchestrator-locks.ts +- ../../../../src/workflow-recipes.ts +- ../../../../src/ccgs-adaptation.ts - ../../../../src/codex-runtime.ts - ../../../../src/verification.ts - ../../../../tests/runner.test.ts - ../../../../tests/studio-policy.test.ts - ../../../../tests/tasks.test.ts +- ../../../../tests/orchestrator.test.ts +- ../../../../tests/workflow-recipes.test.ts +- ../../../../tests/ccgs-adaptation.test.ts - ../../../../tests/verification.test.ts - ../../../../tests/codex-runtime.test.ts diff --git a/docs/truthmark/engineering/contracts/cli-and-validation.md b/docs/truthmark/engineering/contracts/cli-and-validation.md index 1fda4c8..e1d02e6 100644 --- a/docs/truthmark/engineering/contracts/cli-and-validation.md +++ b/docs/truthmark/engineering/contracts/cli-and-validation.md @@ -128,7 +128,7 @@ It does not own project scaffolding internals, role prompt content, or Codex run - Node support requires a package engine floor that includes Node >=24. - Packaged files must include `dist/`, `engine_configs/`, `engine_reference/`, and `templates/`. - Future-only command surfaces stay hidden until implemented intentionally. -- Future-only examples include `next`, `telemetry`, `parallel`, and ownership enforcement. +- Future-only examples include `next`, `telemetry`, hosted/background orchestration, unbounded parallelism, and ownership enforcement. ## Versioning And Migration @@ -158,7 +158,8 @@ It does not own project scaffolding internals, role prompt content, or Codex run - Decision (2026-06-14): Validate engine reference packs by registered file presence and seed-review metadata shape. - Decision (2026-06-14): Validate active-engine materialized references without judging prose quality. - Decision (2026-06-17): Expose the expanded workflow catalog as render-only CLI shortcuts. -- Decision (2026-06-17): Keep future planner, next, telemetry, parallel orchestration, and ownership enforcement hidden. +- Decision (2026-06-17): Keep future planner, next, telemetry, hosted/background orchestration, unbounded parallelism, and ownership enforcement hidden. +- Decision (2026-06-25): Treat explicit local task orchestration as in-boundary once it has CLI behavior, validation, tests, and truth docs; keep hosted/background orchestration and unbounded parallelism hidden. - Decision (2026-06-17): Add local deterministic behavioral-evaluation subchecks. - Decision (2026-06-17): Do not use hosted evaluators, telemetry, hidden memory, or LLM judges for those checks. - Decision (2026-06-17): Support project-local customization as an extend-only `custom-*` overlay. diff --git a/docs/truthmark/engineering/repository/overview.md b/docs/truthmark/engineering/repository/overview.md index 7a140b7..c1755ea 100644 --- a/docs/truthmark/engineering/repository/overview.md +++ b/docs/truthmark/engineering/repository/overview.md @@ -32,7 +32,7 @@ They are listed in `docs/truthmark/routes/areas/repository.md`. - Role run commands render deterministic Codex prompts. - Unless in inspection mode, role run commands execute Codex with optional verification, review, and bounded fix passes. - Workflow shortcut commands are render-only prompt surfaces. -- Workflow shortcuts do not imply hidden planner, parallel orchestration, telemetry, or ownership enforcement behavior. +- Workflow shortcuts do not imply hidden planner, hosted orchestration, background loops, unbounded parallelism, telemetry, or ownership enforcement behavior. Explicit local task orchestration is now allowed by the product boundary only when backed by reviewable `.codex/**` state. - Validation checks package metadata, source files, templates, role/workflow rendering, behavioral-evaluation scenarios, and customization packs. - It also checks future-surface guardrails, build output, and package install smoke behavior. @@ -93,7 +93,7 @@ Bounded truth surfaces make the architecture easier to review. They also prevent ## Non-Goals - This doc does not replace specific leaf truth docs. -- This repository does not expose hidden parallel orchestration. +- This repository does not expose hidden parallel orchestration, hosted orchestration, background autonomous loops, or unbounded parallelism. - This repository does not expose telemetry. - This repository does not expose planner/next queues. - This repository does not expose ownership enforcement as public CLI behavior. diff --git a/docs/truthmark/product/open-game-studio-cli.md b/docs/truthmark/product/open-game-studio-cli.md index 3588e11..a36967b 100644 --- a/docs/truthmark/product/open-game-studio-cli.md +++ b/docs/truthmark/product/open-game-studio-cli.md @@ -30,7 +30,7 @@ It covers project initialization, role/workflow prompt rendering, direct Codex e It also covers generated project files and validation. -This capability excludes game-engine functionality and hosted orchestration. +This capability excludes game-engine functionality and hosted orchestration. It includes explicit local task orchestration when state, approvals, selected context, locks, runs, and failures are reviewable in `.codex/**`. It also excludes background autonomous control, CI/release enforcement, hidden checkpoint/memory systems, and mandatory studio ceremony for small prototypes. @@ -93,12 +93,14 @@ Production templates are package-shipped assets selected by relevance. ## Product Decisions - 2026-06-13: Open Game Studio is a local-first Codex-native CLI/package for game-development repository workflows. -- 2026-06-13: Open Game Studio is not a hosted studio service, daemon, general orchestrator, or game engine. +- 2026-06-13: Open Game Studio is not a hosted studio service, daemon, general-purpose orchestrator, or game engine. +- 2026-06-25: Explicit local task orchestration is inside the product boundary when it remains Codex-native, file-backed, bounded by selected context, policy-gated, and validated. +- 2026-06-25: The first orchestration implementation is a foreground `task orchestrate` CLI with bounded concurrency, transient `.codex/locks/`, workflow task recipes, and no hosted/background/unbounded behavior. - 2026-06-13: Studio depth is optional and mode-controlled. - 2026-06-13: Lifecycle stage must remain separate from process strictness. - 2026-06-13: Generated project instruction contracts use `AGENTS.md` and `.codex/**`. - 2026-06-13: Generated project instruction contracts do not use `CODEX.md` or legacy compatibility shims. -- 2026-06-13: Planner/next, telemetry, parallel orchestration, hard ownership enforcement, and similar future-only surfaces remain absent until implemented, tested, and documented. +- 2026-06-13: Planner/next, telemetry, hard ownership enforcement, hosted/background orchestration, unbounded parallelism, and similar future-only surfaces remain absent until implemented, tested, and documented. - 2026-06-13: Truthmark-backed docs guard repository truth in this checkout. - 2026-06-13: Truthmark workflow mechanics are not Open Game Studio product features. - 2026-06-17: Project-local customization is an extend-only, file-backed overlay for `custom-*` roles, workflows, and templates. diff --git a/docs/truthmark/routes/areas/repository.md b/docs/truthmark/routes/areas/repository.md index 06ed926..f99223d 100644 --- a/docs/truthmark/routes/areas/repository.md +++ b/docs/truthmark/routes/areas/repository.md @@ -132,11 +132,18 @@ Code surface: - src/context-manifest.ts - src/prompt-context.ts - src/tasks.ts +- src/orchestrator.ts +- src/orchestrator-locks.ts +- src/workflow-recipes.ts +- src/ccgs-adaptation.ts - src/codex-runtime.ts - src/verification.ts - tests/runner.test.ts - tests/studio-policy.test.ts - tests/tasks.test.ts +- tests/orchestrator.test.ts +- tests/workflow-recipes.test.ts +- tests/ccgs-adaptation.test.ts - tests/verification.test.ts - tests/codex-runtime.test.ts diff --git a/docs/workflow-validation.md b/docs/workflow-validation.md index 1ac705e..67269d8 100644 --- a/docs/workflow-validation.md +++ b/docs/workflow-validation.md @@ -2,7 +2,7 @@ Validation exits nonzero when any check fails. -Repo validation checks package scripts, build output, package assets, engine configs, expanded role rendering, canonical workflow rendering, deterministic behavioral-evaluation scenarios, templates, package packing, installed-bin asset loading, future-only CLI surfaces, and Codex CLI readiness. +Repo validation checks package scripts, build output, package assets, engine configs, expanded role rendering, canonical workflow rendering, deterministic behavioral-evaluation scenarios, templates, package packing, installed-bin asset loading, future-only CLI surfaces, and Codex CLI readiness. Tests also cover explicit task orchestration, workflow task recipes, and curated CCGS adaptation registry consistency. Project validation checks `.codex/studio.json` full `roles`, mode-specific `activeRoles`, registry-derived `workflows`, `.codex/studio/config.json` customization packs, `AGENTS.md`, generated project-specific role prompts, workflow files, engine source files, starter docs, timeline sections, forbidden legacy artifacts, and read-only `status`/`resume` behavior. @@ -13,7 +13,7 @@ npm exec opengamestudio -- run --help | grep -- "--dry-run" ! npm exec opengamestudio -- --help | grep -E " next|telemetry" ``` -Workflow shortcut commands such as `market`, `analytics`, `design-spec`, `feel-review`, `art-direction`, `ui-review`, `milestone`, and `handoff` render prompts only. They do not launch Codex or create run records. +Workflow shortcut commands such as `market`, `analytics`, `design-spec`, `feel-review`, `art-direction`, `ui-review`, `milestone`, and `handoff` render prompts only. They do not launch Codex or create run records. `workflow create-tasks ` is the explicit path for turning supported workflow recipes into `.codex/tasks.json` tasks; it still does not launch Codex. Behavioral evaluation scenarios are local deterministic validation subchecks. They render built-in role and workflow prompts, assert required prompt obligations, selected context categories, relevant templates, output-contract coverage, and forbidden future-only drift. They do not call hosted evaluators, telemetry, hidden memory, or LLM judges. diff --git a/src/agents.ts b/src/agents.ts index dd41245..2cc8108 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -102,7 +102,7 @@ Run project validation before claiming parity or readiness. Use AGENTS.md, .codex/studio.json, the current role prompt, and task-relevant context only. Codex is the default runtime for \`opengamestudio run \`; use \`--dry-run\` or \`--print-prompt\` only for inspection. -Do not use telemetry, planner/next, parallel orchestration, or ownership enforcement in this build. +Do not use telemetry, planner/next, hosted/background orchestration, unbounded parallelism, or ownership enforcement in this build. Explicit local task orchestration must stay reviewable in .codex state. `; } diff --git a/src/behavioral-evaluation.ts b/src/behavioral-evaluation.ts index 709f4a2..e0cd3ef 100644 --- a/src/behavioral-evaluation.ts +++ b/src/behavioral-evaluation.ts @@ -35,7 +35,7 @@ export type BehavioralEvaluationResult = { presentForbiddenTemplateIds: TemplateId[]; }; -const defaultForbiddenDrift = ["CODEX.md", "telemetry", "parallel orchestration", "hidden memory", "hosted service", "planner/next"]; +const defaultForbiddenDrift = ["CODEX.md", "telemetry", "hidden memory", "hosted service", "hosted orchestration", "background loop", "unbounded parallel", "planner/next"]; function withDefaultForbidden(extra: string[] = []): string[] { return [...new Set([...defaultForbiddenDrift, ...extra])]; diff --git a/src/ccgs-adaptation.ts b/src/ccgs-adaptation.ts new file mode 100644 index 0000000..ebcf1d1 --- /dev/null +++ b/src/ccgs-adaptation.ts @@ -0,0 +1,109 @@ +import { studioRoleIds, type StudioRoleId } from "./roles.js"; +import { workflowIds, type WorkflowId } from "./workflows.js"; + +export type CcgsAdaptationDecision = "built-in-existing" | "built-in-add-candidate" | "specialty-context" | "custom-pack-example" | "workflow" | "recipe" | "template" | "defer" | "out-of-scope"; + +export type CcgsRoleAdaptation = { + sourceId: string; + target: string; + decision: CcgsAdaptationDecision; + notes: string; +}; + +export type CcgsSkillAdaptation = { + sourceId: string; + target: string; + decision: CcgsAdaptationDecision; + notes: string; +}; + +export const ccgsInventory = { + source: "https://github.com/Donchitos/Claude-Code-Game-Studios", + reviewedAt: "2026-06-25", + roleCountAtReview: 49, + skillCountAtReview: 73, + policy: "Curated reference only; not parity-by-copying and not a Claude runtime compatibility target." +} as const; + +export const ccgsRoleAdaptations: CcgsRoleAdaptation[] = [ + { sourceId: "producer", target: "producer", decision: "built-in-existing", notes: "Production planning maps directly." }, + { sourceId: "creative-director", target: "creative-director", decision: "built-in-existing", notes: "Creative direction maps directly." }, + { sourceId: "game-designer", target: "game-designer", decision: "built-in-existing", notes: "Implementation-level design maps directly." }, + { sourceId: "systems-designer", target: "systems-designer", decision: "built-in-existing", notes: "Systems design maps directly." }, + { sourceId: "economy-designer", target: "economy-designer", decision: "built-in-existing", notes: "Economy design maps directly." }, + { sourceId: "level-designer", target: "level-designer", decision: "built-in-existing", notes: "Level design maps directly." }, + { sourceId: "world-builder", target: "world-builder", decision: "built-in-existing", notes: "World building maps directly." }, + { sourceId: "writer", target: "writer", decision: "built-in-existing", notes: "Writing maps directly." }, + { sourceId: "gameplay-programmer", target: "gameplay-programmer", decision: "built-in-existing", notes: "Gameplay implementation maps directly." }, + { sourceId: "ai-programmer", target: "ai-programmer", decision: "built-in-existing", notes: "AI implementation maps directly." }, + { sourceId: "network-programmer", target: "network-programmer", decision: "built-in-existing", notes: "Networking implementation maps directly." }, + { sourceId: "ui-programmer", target: "ui-programmer", decision: "built-in-existing", notes: "UI implementation maps directly." }, + { sourceId: "engine-programmer", target: "engine-programmer", decision: "built-in-existing", notes: "Engine implementation maps directly." }, + { sourceId: "tools-programmer", target: "tools-programmer", decision: "built-in-existing", notes: "Tools implementation maps directly." }, + { sourceId: "technical-director", target: "technical-director", decision: "built-in-existing", notes: "Technical direction maps directly." }, + { sourceId: "devops-engineer", target: "devops-engineer", decision: "built-in-existing", notes: "DevOps maps directly." }, + { sourceId: "security-engineer", target: "security-engineer", decision: "built-in-existing", notes: "Security review maps directly." }, + { sourceId: "performance-analyst", target: "performance-analyst", decision: "built-in-existing", notes: "Performance review maps directly." }, + { sourceId: "technical-artist", target: "technical-artist", decision: "built-in-existing", notes: "Technical art maps directly." }, + { sourceId: "audio-director", target: "audio-director", decision: "built-in-existing", notes: "Audio direction maps directly." }, + { sourceId: "sound-designer", target: "sound-designer", decision: "built-in-existing", notes: "Sound design maps directly." }, + { sourceId: "accessibility-specialist", target: "accessibility-specialist", decision: "built-in-existing", notes: "Accessibility maps directly." }, + { sourceId: "localization-lead", target: "localization-lead", decision: "built-in-existing", notes: "Localization maps directly." }, + { sourceId: "live-ops-designer", target: "live-ops-designer", decision: "built-in-existing", notes: "Live ops maps directly." }, + { sourceId: "community-manager", target: "community-manager", decision: "built-in-existing", notes: "Community management maps directly." }, + { sourceId: "release-manager", target: "release-manager", decision: "built-in-existing", notes: "Release management maps directly." }, + { sourceId: "godot-specialist", target: "godot-specialist", decision: "built-in-existing", notes: "Active engine specialist." }, + { sourceId: "unity-specialist", target: "unity-specialist", decision: "built-in-existing", notes: "Active engine specialist." }, + { sourceId: "unreal-specialist", target: "unreal-specialist", decision: "built-in-existing", notes: "Active engine specialist." }, + { sourceId: "analytics-engineer", target: "data-scientist", decision: "built-in-existing", notes: "Analytics engineering maps to data-scientist plus analytics templates." }, + { sourceId: "art-director", target: "senior-game-artist", decision: "built-in-existing", notes: "Art direction maps to senior-game-artist and art-direction workflow." }, + { sourceId: "narrative-director", target: "narrative-designer", decision: "built-in-existing", notes: "Narrative direction maps to narrative-designer/world-builder." }, + { sourceId: "ux-designer", target: "ui-ux-designer", decision: "built-in-existing", notes: "UX maps to UI/UX designer." }, + { sourceId: "qa-lead", target: "qa-lead", decision: "built-in-add-candidate", notes: "Add only if QA strategy needs a separate owner from qa-playtester." }, + { sourceId: "qa-tester", target: "qa-playtester", decision: "defer", notes: "Current QA playtester covers test cases and evidence; split later if needed." }, + { sourceId: "lead-programmer", target: "lead-programmer", decision: "built-in-add-candidate", notes: "Add only if technical-director is too broad for code ownership." }, + { sourceId: "prototyper", target: "prototype", decision: "workflow", notes: "Keep as workflow/recipe, not a separate role." }, + { sourceId: "godot-gdscript-specialist", target: "engine-reference/godot", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "godot-csharp-specialist", target: "engine-reference/godot", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "godot-gdextension-specialist", target: "engine-reference/godot", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "godot-shader-specialist", target: "engine-reference/godot", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "unity-addressables-specialist", target: "engine-reference/unity", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "unity-dots-specialist", target: "engine-reference/unity", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "unity-shader-specialist", target: "engine-reference/unity", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "unity-ui-specialist", target: "engine-reference/unity", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "ue-blueprint-specialist", target: "engine-reference/unreal", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "ue-gas-specialist", target: "engine-reference/unreal", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "ue-replication-specialist", target: "engine-reference/unreal", decision: "specialty-context", notes: "Task-keyword selected engine context." }, + { sourceId: "ue-umg-specialist", target: "engine-reference/unreal", decision: "specialty-context", notes: "Task-keyword selected engine context." } +]; + +export const ccgsSkillAdaptations: CcgsSkillAdaptation[] = [ + { sourceId: "vertical-slice", target: "vertical-slice", decision: "recipe", notes: "Create explicit task graph before orchestration." }, + { sourceId: "bug-triage", target: "bugfix", decision: "recipe", notes: "Bugfix recipe covers repro/fix/verify." }, + { sourceId: "team-ui", target: "ui-ux-review", decision: "recipe", notes: "Team orchestration becomes explicit task graph." }, + { sourceId: "team-release", target: "release-checklist", decision: "recipe", notes: "Release team becomes explicit task graph." }, + { sourceId: "gate-check", target: "gate-check", decision: "workflow", notes: "High-value future workflow." }, + { sourceId: "project-stage-detect", target: "project-stage-detect", decision: "workflow", notes: "High-value future workflow." }, + { sourceId: "scope-check", target: "scope-check", decision: "workflow", notes: "High-value future workflow." }, + { sourceId: "estimate", target: "estimate", decision: "workflow", notes: "High-value future workflow." }, + { sourceId: "asset-audit", target: "asset-audit", decision: "workflow", notes: "High-value future workflow." }, + { sourceId: "balance-check", target: "balance-check", decision: "workflow", notes: "High-value future workflow." }, + { sourceId: "skill-improve", target: "none", decision: "out-of-scope", notes: "Claude skill maintenance is not an OGS product surface." }, + { sourceId: "skill-test", target: "none", decision: "out-of-scope", notes: "Claude skill maintenance is not an OGS product surface." } +]; + +export function validateCcgsAdaptation(): string[] { + const problems: string[] = []; + const builtIns = new Set(studioRoleIds); + const workflows = new Set(workflowIds()); + for (const item of ccgsRoleAdaptations) { + if (item.decision === "built-in-existing" && !builtIns.has(item.target)) problems.push(`${item.sourceId} targets missing built-in role ${item.target}`); + } + for (const item of ccgsSkillAdaptations) { + if ((item.decision === "workflow" || item.decision === "recipe") && item.target !== "none" && !workflows.has(item.target as WorkflowId)) { + // Future workflow candidates are allowed to be named before implementation. + if (!["gate-check", "project-stage-detect", "scope-check", "estimate", "asset-audit", "balance-check"].includes(item.target)) problems.push(`${item.sourceId} targets missing workflow ${item.target}`); + } + } + return problems; +} diff --git a/src/cli.ts b/src/cli.ts index 06c2db3..6e5fdc6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,7 +17,9 @@ import { runValidation } from "./validation.js"; import { executeRunLifecycle, prepareRun } from "./runner.js"; import { checkCodexAvailability } from "./codex-runtime.js"; import { createTask, executeTaskRun, readTaskStore, resolveTaskProject } from "./tasks.js"; +import { orchestrateTasks } from "./orchestrator.js"; import { renderWorkflowPrompt, workflowAliases, workflowRegistry, type WorkflowId } from "./workflows.js"; +import { createWorkflowTasks } from "./workflow-recipes.js"; import { isStudioRoleId, unknownStudioRoleMessage } from "./roles.js"; import type { ProjectStage, StudioMode } from "./studio-policy.js"; @@ -180,7 +182,7 @@ approval if (task.role !== opts.role) throw new Error(`Task ${task.id} is assigned to role ${task.role}; approval role ${opts.role} does not match.`); objective = task.title; objectiveLabel = task.title; - approvedFiles = task.files.length > 0 ? task.files : undefined; + approvedFiles = task.writeFiles.length > 0 ? task.writeFiles : undefined; objectiveSha256 = canonicalObjectiveSha256({ role: opts.role, objective: task.title, @@ -310,6 +312,12 @@ task .description("Create a ready task in .codex/tasks.json") .requiredOption("--project ", "project path") .requiredOption("--role ", "studio role") + .option("--file ", "read/context file for the task; repeat for multiple files", (value, previous: string[] = []) => [...previous, value], []) + .option("--write-file ", "literal project-relative file this task may mutate; repeat for multiple files", (value, previous: string[] = []) => [...previous, value], []) + .option("--depends-on ", "task dependency that must be done; repeat for multiple dependencies", (value, previous: string[] = []) => [...previous, value], []) + .option("--workflow ", "source workflow id for this task") + .option("--group ", "task group id") + .option("--priority ", "task priority; higher values run earlier", "0") .option("--verify-command ", "structured verification command") .option("--verify-arg ", "structured verification argument; repeat for multiple args", (value, previous: string[] = []) => [...previous, value], []) .argument("") @@ -317,7 +325,17 @@ task if (!isStudioRoleId(opts.role)) throw new Error(unknownStudioRoleMessage(opts.role)); const projectRoot = resolveTaskProject(opts.project); const verification = opts.verifyCommand ? { command: opts.verifyCommand as string, args: opts.verifyArg as string[] } : undefined; - const created = createTask(projectRoot, { title: titleParts.join(" "), role: opts.role, verification }); + const created = createTask(projectRoot, { + title: titleParts.join(" "), + role: opts.role, + verification, + files: opts.file, + writeFiles: opts.writeFile, + dependencies: opts.dependsOn, + workflowId: opts.workflow, + groupId: opts.group, + priority: Number(opts.priority) + }); console.log(created.id); }); task @@ -346,19 +364,60 @@ task console.log(opts.dryRun ? result.prepared.output : `${result.prepared.output}\n${result.lifecycle?.output ?? ""}\n${taskId} ${result.task.status}`); if (result.lifecycle?.finalStatus === "blocked") process.exitCode = 1; }); +task + .command("orchestrate") + .description("Run ready tasks with explicit local orchestration and bounded parallelism") + .requiredOption("--project ", "project path") + .option("--workflow ", "select ready tasks from one workflow") + .option("--max-concurrency ", "maximum concurrent task runs, capped at 3", "1") + .option("--dry-run", "show task waves, locks, approvals, and commands without mutation") + .option("--review", "run a schema-driven review pass per task") + .option("--fix", "run bounded fix pass prompts when blocked") + .option("--max-fix-passes ", "maximum automatic fix passes", "1") + .option("--approval-scope ", "approval diagnostic scope for task run objective hashing; repeat for multiple scopes", collectScope, []) + .option("--approved-by-user", "explicitly approve a guided-studio local override") + .option("--constrained-sandbox", "use Codex workspace-write instead of the default full-access sandbox") + .argument("[task-id...]", "specific task ids to orchestrate") + .action(async (taskIds: string[], opts) => { + const result = await orchestrateTasks({ + project: opts.project, + taskIds: taskIds.length ? taskIds : undefined, + workflowId: opts.workflow, + maxConcurrency: Number(opts.maxConcurrency), + dryRun: opts.dryRun, + review: opts.review, + fix: opts.fix, + maxFixPasses: Number(opts.maxFixPasses), + approvedByUser: opts.approvedByUser, + constrainedSandbox: opts.constrainedSandbox, + approvalScope: opts.approvalScope + }); + console.log(result.output); + if (result.status === "blocked") process.exitCode = 1; + }); function renderWorkflowCommand(workflow: string, opts: { project: string }): void { const projectRoot = resolveTaskProject(opts.project); console.log(renderWorkflowPrompt(projectRoot, workflow)); } -program - .command("workflow") +const workflow = program.command("workflow").description("Render a built-in or project-local workflow prompt by id"); +workflow + .command("render ", { isDefault: true }) .description("Render a built-in or project-local workflow prompt by id") - .argument("") .requiredOption("--project ", "project path") .option("--dry-run", "render prompt without launching Codex") - .action((workflow: string, opts) => renderWorkflowCommand(workflow, opts)); + .action((workflowId: string, opts) => renderWorkflowCommand(workflowId, opts)); + +const workflowTasks = workflow.command("create-tasks ").description("Create explicit file-backed tasks from a workflow recipe"); +workflowTasks + .requiredOption("--project ", "project path") + .option("--dry-run", "show proposed tasks without writing .codex/tasks.json") + .action((workflowId: string, opts) => { + const projectRoot = resolveTaskProject(opts.project); + const result = createWorkflowTasks(projectRoot, workflowId, { dryRun: opts.dryRun }); + console.log(result.output); + }); function addWorkflowCommand(name: "review" | "ship-check"): void { program diff --git a/src/codex-runtime.ts b/src/codex-runtime.ts index 3261d81..de0d1b5 100644 --- a/src/codex-runtime.ts +++ b/src/codex-runtime.ts @@ -43,13 +43,28 @@ export async function checkCodexAvailability(options: { codexBin?: string } = {} return { ok: true, command, stdout: result.stdout ?? "", stderr: result.stderr ?? "" }; } -export async function executeCodexPrompt(prompt: string, options: CodexRuntimeOptions): Promise { - const command = options.codexBin ?? resolveCodexCommand(); - const args = buildCodexExecArgs(options); +export async function executeCodexCommand( + command: { command: string; args: string[] }, + input: string, + options: { cwd: string; timeoutMs?: number } +): Promise { return await new Promise((resolve) => { - const child = spawn(command, args, { cwd: options.projectRoot, shell: false, stdio: ["pipe", "pipe", "pipe"] }); + const child = spawn(command.command, command.args, { cwd: options.cwd, shell: false, stdio: ["pipe", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; + let settled = false; + const finish = (result: CodexExecutionResult): void => { + if (settled) return; + settled = true; + if (timer) clearTimeout(timer); + resolve(result); + }; + const timer = options.timeoutMs + ? setTimeout(() => { + child.kill("SIGTERM"); + finish({ status: null, signal: "SIGTERM", stdout, stderr, error: new Error(`Codex command timed out after ${options.timeoutMs}ms`) }); + }, options.timeoutMs) + : undefined; child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk: string) => { @@ -58,8 +73,14 @@ export async function executeCodexPrompt(prompt: string, options: CodexRuntimeOp child.stderr.on("data", (chunk: string) => { stderr += chunk; }); - child.on("error", (error) => resolve({ status: null, signal: null, stdout, stderr, error })); - child.on("close", (status, signal) => resolve({ status, signal, stdout, stderr })); - child.stdin.end(prompt); + child.on("error", (error) => finish({ status: null, signal: null, stdout, stderr, error })); + child.on("close", (status, signal) => finish({ status, signal, stdout, stderr })); + child.stdin.end(input); }); } + +export async function executeCodexPrompt(prompt: string, options: CodexRuntimeOptions): Promise { + const command = options.codexBin ?? resolveCodexCommand(); + const args = buildCodexExecArgs(options); + return await executeCodexCommand({ command, args }, prompt, { cwd: options.projectRoot }); +} diff --git a/src/orchestrator-locks.ts b/src/orchestrator-locks.ts new file mode 100644 index 0000000..32d02a4 --- /dev/null +++ b/src/orchestrator-locks.ts @@ -0,0 +1,102 @@ +import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, closeSync } from "node:fs"; +import path from "node:path"; +import { createHash } from "node:crypto"; +import { projectRelativePath } from "./customization.js"; +import type { StudioTask } from "./tasks.js"; + +export type TaskLock = { + schemaVersion: 1; + lockKey: string; + taskId: string; + orchestrationRunId: string; + role: string; + writeFile: string; + acquiredAt: string; + expiresAt: string; + releasedAt?: string; +}; + +export type AcquiredTaskLock = { + file: string; + lock: TaskLock; +}; + +export const projectWriteLockKey = "__project_write__"; + +export function locksDir(projectRoot: string): string { + return path.join(projectRoot, ".codex", "locks"); +} + +function hashLockKey(value: string): string { + return createHash("sha256").update(value).digest("hex").slice(0, 24); +} + +function lockFile(projectRoot: string, lockKey: string): string { + return path.join(locksDir(projectRoot), `${hashLockKey(lockKey)}.json`); +} + +export function normalizeWriteFile(projectRoot: string, value: string): string { + if (value.includes("*") || value.includes("?") || value.includes("[")) throw new Error(`writeFiles must be literal file paths, not globs: ${value}`); + if (value.endsWith("/") || value.endsWith("\\")) throw new Error(`writeFiles must be file paths, not directories: ${value}`); + const safe = projectRelativePath(projectRoot, value); + if (!safe.ok) throw new Error(`Unsafe writeFile ${value}: ${safe.error}`); + if (safe.display === ".git" || safe.display.startsWith(".git/")) throw new Error(`writeFiles cannot target .git: ${value}`); + return safe.display; +} + +export function taskLockKeys(projectRoot: string, task: Pick): string[] { + if (task.writeFiles.length === 0) return [projectWriteLockKey]; + return [...new Set(task.writeFiles.map((file) => normalizeWriteFile(projectRoot, file)))]; +} + +export function acquireTaskLocks(projectRoot: string, task: Pick, orchestrationRunId: string, ttlMs = 60 * 60 * 1000): AcquiredTaskLock[] { + mkdirSync(locksDir(projectRoot), { recursive: true }); + const acquired: AcquiredTaskLock[] = []; + const now = new Date(); + try { + for (const lockKey of taskLockKeys(projectRoot, task)) { + const file = lockFile(projectRoot, lockKey); + if (existsSync(file)) { + let existing: TaskLock | undefined; + try { + existing = JSON.parse(readFileSync(file, "utf8")) as TaskLock; + } catch { + // malformed lock still blocks + } + throw new Error(`Lock already held for ${lockKey}${existing ? ` by ${existing.taskId}` : ""}`); + } + const lock: TaskLock = { + schemaVersion: 1, + lockKey, + taskId: task.id, + orchestrationRunId, + role: task.role, + writeFile: lockKey, + acquiredAt: now.toISOString(), + expiresAt: new Date(now.getTime() + ttlMs).toISOString() + }; + const fd = openSync(file, "wx"); + try { + writeFileSync(fd, `${JSON.stringify(lock, null, 2)}\n`); + } finally { + closeSync(fd); + } + acquired.push({ file, lock }); + } + return acquired; + } catch (error) { + releaseTaskLocks(acquired); + throw error; + } +} + +export function releaseTaskLocks(locks: AcquiredTaskLock[]): void { + for (const acquired of locks.reverse()) { + try { + writeFileSync(acquired.file, `${JSON.stringify({ ...acquired.lock, releasedAt: new Date().toISOString() }, null, 2)}\n`); + rmSync(acquired.file, { force: true }); + } catch { + // Best-effort cleanup; callers still record task failure. + } + } +} diff --git a/src/orchestrator.ts b/src/orchestrator.ts new file mode 100644 index 0000000..475fd28 --- /dev/null +++ b/src/orchestrator.ts @@ -0,0 +1,341 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; +import { resolveProjectRoot } from "./paths.js"; +import { executeRunLifecycle, prepareRun, type PreparedRun, type RunLifecycleResult } from "./runner.js"; +import { acquireTaskLocks, releaseTaskLocks, taskLockKeys, type AcquiredTaskLock } from "./orchestrator-locks.js"; +import { getTask, readTaskStore, writeTaskStore, type StudioTask, type StudioTaskStatus, type TaskStore } from "./tasks.js"; + +export type OrchestrateOptions = { + project: string; + taskIds?: string[]; + workflowId?: string; + maxConcurrency?: number; + dryRun?: boolean; + review?: boolean; + fix?: boolean; + maxFixPasses?: number; + approvedByUser?: boolean; + constrainedSandbox?: boolean; + approvalScope?: string[]; + codexBin?: string; +}; + +export type OrchestrationResult = { + runId: string; + status: "planned" | "done" | "blocked"; + selectedTaskIds: string[]; + startedTaskIds: string[]; + blockedTaskIds: string[]; + skippedTaskIds: string[]; + output: string; +}; + +type PlannedTask = { + task: StudioTask; + prepared: PreparedRun; +}; + +type RunningTask = { + taskId: string; + locks: AcquiredTaskLock[]; + promise: Promise<{ taskId: string; lifecycle?: RunLifecycleResult; error?: Error }>; +}; + +const maxConcurrencyCap = 3; + +function nowIso(): string { + return new Date().toISOString(); +} + +function newRunId(): string { + return `${new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 17)}-${process.pid}-${randomUUID().slice(0, 8)}`; +} + +function taskRank(task: StudioTask): string { + return `${String(999999 - task.priority).padStart(6, "0")}:${task.id}`; +} + +function selectedTasks(store: TaskStore, options: OrchestrateOptions): StudioTask[] { + if (options.taskIds?.length) return options.taskIds.map((id) => getTask(store, id)); + if (options.workflowId) return store.tasks.filter((task) => task.workflowId === options.workflowId && task.status === "ready").sort((a, b) => taskRank(a).localeCompare(taskRank(b))); + return store.tasks.filter((task) => task.status === "ready").sort((a, b) => taskRank(a).localeCompare(taskRank(b))); +} + +function validateMaxConcurrency(value: number): number { + if (!Number.isFinite(value) || value < 1 || Math.floor(value) !== value) throw new Error("--max-concurrency must be a positive integer"); + if (value > maxConcurrencyCap) throw new Error(`--max-concurrency cannot exceed ${maxConcurrencyCap}`); + return value; +} + +function validateGraph(store: TaskStore, selected: StudioTask[], options: OrchestrateOptions): void { + const all = new Map(store.tasks.map((task) => [task.id, task])); + const selectedIds = new Set(selected.map((task) => task.id)); + for (const task of selected) { + for (const dependency of task.dependencies) { + const dep = all.get(dependency.taskId); + if (!dep) throw new Error(`Task ${task.id} depends on unknown task ${dependency.taskId}`); + if (["blocked", "cancelled", "skipped"].includes(dep.status) && !(options.dryRun && options.taskIds?.length === 1)) { + throw new Error(`Task ${task.id} depends on ${dep.status} task ${dep.id}`); + } + if (dep.status !== dependency.requiredStatus && !selectedIds.has(dep.id)) { + throw new Error(`Task ${task.id} depends on ${dep.id}; include it in this run or complete it first`); + } + } + } + + const visiting = new Set(); + const visited = new Set(); + const visit = (task: StudioTask): void => { + if (visited.has(task.id)) return; + if (visiting.has(task.id)) throw new Error(`Task dependency cycle includes ${task.id}`); + visiting.add(task.id); + for (const dependency of task.dependencies) { + const dep = all.get(dependency.taskId); + if (dep && selectedIds.has(dep.id)) visit(dep); + } + visiting.delete(task.id); + visited.add(task.id); + }; + for (const task of selected) visit(task); +} + +function dependenciesSatisfied(task: StudioTask, store: TaskStore, selectedIds: Set): boolean { + for (const dependency of task.dependencies) { + const dep = getTask(store, dependency.taskId); + if (dep.status !== dependency.requiredStatus && selectedIds.has(dep.id)) return false; + if (dep.status !== dependency.requiredStatus && !selectedIds.has(dep.id)) return false; + } + return true; +} + +function dependenciesBlocked(task: StudioTask, store: TaskStore): string | undefined { + for (const dependency of task.dependencies) { + const dep = getTask(store, dependency.taskId); + if (["blocked", "cancelled", "skipped"].includes(dep.status)) return dep.id; + } + return undefined; +} + +function updateTask(store: TaskStore, taskId: string, status: StudioTaskStatus, note?: string, lastRunId?: string): StudioTask { + const task = getTask(store, taskId); + task.status = status; + task.updatedAt = nowIso(); + if (lastRunId) task.lastRunId = lastRunId; + if (note) task.notes.push(`${nowIso()} ${note}`); + return task; +} + +function runDir(projectRoot: string, runId: string): string { + return path.join(projectRoot, ".codex", "runs", runId); +} + +function taskRunDir(projectRoot: string, runId: string, taskId: string): string { + return path.join(runDir(projectRoot, runId), "tasks", taskId); +} + +function appendEvent(projectRoot: string, runId: string, event: Record): void { + const file = path.join(runDir(projectRoot, runId), "events.jsonl"); + writeFileSync(file, `${JSON.stringify({ timestamp: nowIso(), ...event })}\n`, { flag: "a" }); +} + +function writeRunMetadata(projectRoot: string, runId: string, metadata: Record): void { + mkdirSync(runDir(projectRoot, runId), { recursive: true }); + writeFileSync(path.join(runDir(projectRoot, runId), "orchestration.json"), `${JSON.stringify(metadata, null, 2)}\n`); +} + +function planSummary(projectRoot: string, planned: PlannedTask[], maxConcurrency: number): string { + const lines = [`Orchestration plan: ${planned.length} task(s), max concurrency ${maxConcurrency}`]; + for (const { task, prepared } of planned) { + const locks = prepared.eligibility.allowFileEdits ? taskLockKeys(projectRoot, task).join(", ") : "read-only"; + lines.push(`- ${task.id} [${task.role}] ${task.title}`); + lines.push(` status: ${task.status}; deps: ${task.dependencies.map((dep) => dep.taskId).join(", ") || "none"}; locks: ${locks}`); + lines.push(` context: ${prepared.contextFiles.join(", ") || "none"}`); + lines.push(` codex: ${prepared.codexCommand.display}`); + } + return lines.join("\n"); +} + +async function executePlannedTask(projectRoot: string, runId: string, planned: PlannedTask): Promise<{ taskId: string; lifecycle?: RunLifecycleResult; error?: Error }> { + const dir = taskRunDir(projectRoot, runId, planned.task.id); + mkdirSync(dir, { recursive: true }); + writeFileSync(path.join(dir, "prompt.md"), planned.prepared.prompt); + writeFileSync( + path.join(dir, "metadata.json"), + `${JSON.stringify( + { + schemaVersion: 1, + taskId: planned.task.id, + role: planned.task.role, + title: planned.task.title, + promptChars: planned.prepared.prompt.length, + contextFiles: planned.prepared.contextFiles, + writeFiles: planned.task.writeFiles, + writePolicy: planned.prepared.eligibility.writePolicy, + codexSandbox: planned.prepared.eligibility.codexSandbox, + eligibility: planned.prepared.eligibility + }, + null, + 2 + )}\n` + ); + try { + const lifecycle = await executeRunLifecycle(planned.prepared); + writeFileSync(path.join(dir, "output.txt"), `${lifecycle.output}\n`); + return { taskId: planned.task.id, lifecycle }; + } catch (error) { + writeFileSync(path.join(dir, "output.txt"), `error: ${(error as Error).message}\n`); + return { taskId: planned.task.id, error: error as Error }; + } +} + +export async function orchestrateTasks(options: OrchestrateOptions): Promise { + const projectRoot = resolveProjectRoot(options.project, process.cwd()); + const maxConcurrency = validateMaxConcurrency(options.maxConcurrency ?? 1); + let store = readTaskStore(projectRoot); + const selected = selectedTasks(store, options); + validateGraph(store, selected, options); + const runId = newRunId(); + const selectedTaskIds = selected.map((task) => task.id); + const selectedIds = new Set(selectedTaskIds); + const planned = selected.map((task) => ({ + task, + prepared: prepareRun( + task.role, + { + project: projectRoot, + task: task.title, + noWrite: true, + dryRun: options.dryRun, + codexBin: options.codexBin, + includeArtifact: task.files, + approvedFiles: task.writeFiles.length ? task.writeFiles : undefined, + verifyCommand: task.verification, + review: options.review ?? task.runPolicy?.review, + fix: options.fix, + maxFixPasses: options.maxFixPasses ?? task.runPolicy?.maxFixPasses, + approvedByUser: options.approvedByUser, + constrainedSandbox: options.constrainedSandbox ?? task.runPolicy?.constrainedSandbox, + approvalScope: options.approvalScope + }, + process.cwd() + ) + })); + + if (options.dryRun) { + return { runId, status: "planned", selectedTaskIds, startedTaskIds: [], blockedTaskIds: [], skippedTaskIds: [], output: planSummary(projectRoot, planned, maxConcurrency) }; + } + + const output: string[] = [planSummary(projectRoot, planned, maxConcurrency)]; + const startedTaskIds: string[] = []; + const blockedTaskIds: string[] = []; + const skippedTaskIds: string[] = []; + const remaining = new Map(planned.map((item) => [item.task.id, item])); + const running: RunningTask[] = []; + + writeRunMetadata(projectRoot, runId, { + schemaVersion: 1, + product: "codex-game-studio", + runId, + startedAt: nowIso(), + projectRoot, + maxConcurrency, + requestedTaskIds: options.taskIds ?? [], + selectedTaskIds, + dryRun: false, + review: Boolean(options.review), + fix: Boolean(options.fix), + status: "running", + summary: [] + }); + appendEvent(projectRoot, runId, { event: "orchestration.started", selectedTaskIds, maxConcurrency }); + + const persist = (): void => writeTaskStore(projectRoot, store); + + const startReady = (): void => { + const ready = [...remaining.values()] + .filter(({ task }) => task.status === "ready" && dependenciesSatisfied(getTask(store, task.id), store, selectedIds)) + .sort((a, b) => taskRank(a.task).localeCompare(taskRank(b.task))); + for (const plannedTask of ready) { + if (running.length >= maxConcurrency) return; + const current = getTask(store, plannedTask.task.id); + let locks: AcquiredTaskLock[] = []; + try { + if (plannedTask.prepared.eligibility.allowFileEdits) locks = acquireTaskLocks(projectRoot, current, runId); + } catch (error) { + updateTask(store, current.id, "blocked", `Lock acquisition failed: ${(error as Error).message}`); + blockedTaskIds.push(current.id); + remaining.delete(current.id); + persist(); + appendEvent(projectRoot, runId, { event: "task.blocked", taskId: current.id, reason: (error as Error).message }); + continue; + } + updateTask(store, current.id, "running", "Orchestration task run started", runId); + persist(); + remaining.delete(current.id); + startedTaskIds.push(current.id); + appendEvent(projectRoot, runId, { event: "task.started", taskId: current.id, locks: locks.map((lock) => lock.lock.lockKey) }); + const promise = executePlannedTask(projectRoot, runId, plannedTask); + running.push({ taskId: current.id, locks, promise }); + } + }; + + while (remaining.size > 0 || running.length > 0) { + startReady(); + for (const [taskId, task] of [...remaining.entries()]) { + const blocker = dependenciesBlocked(task.task, store); + if (blocker) { + updateTask(store, taskId, "skipped", `Dependency ${blocker} did not complete`); + skippedTaskIds.push(taskId); + remaining.delete(taskId); + persist(); + appendEvent(projectRoot, runId, { event: "task.skipped", taskId, blocker }); + } + } + if (running.length === 0) { + if (remaining.size > 0) { + for (const taskId of [...remaining.keys()]) { + updateTask(store, taskId, "blocked", "No runnable dependency wave remained"); + blockedTaskIds.push(taskId); + appendEvent(projectRoot, runId, { event: "task.blocked", taskId, reason: "no runnable dependency wave" }); + remaining.delete(taskId); + } + persist(); + } + continue; + } + const finished = await Promise.race(running.map((item) => item.promise)); + const index = running.findIndex((item) => item.taskId === finished.taskId); + const runningTask = running.splice(index, 1)[0]; + releaseTaskLocks(runningTask.locks); + appendEvent(projectRoot, runId, { event: "task.unlocked", taskId: finished.taskId }); + store = readTaskStore(projectRoot); + const finalStatus: StudioTaskStatus = finished.lifecycle?.finalStatus === "done" ? "done" : "blocked"; + updateTask(store, finished.taskId, finalStatus, finished.error ? `Task failed: ${finished.error.message}` : `Task finished: ${finished.lifecycle?.finalStatus ?? "blocked"}`, runId); + if (finalStatus === "blocked") blockedTaskIds.push(finished.taskId); + persist(); + appendEvent(projectRoot, runId, { event: finalStatus === "done" ? "task.finished" : "task.blocked", taskId: finished.taskId, status: finalStatus }); + output.push(`${finished.taskId}: ${finalStatus}`); + } + + const status: "done" | "blocked" = blockedTaskIds.length === 0 && skippedTaskIds.length === 0 ? "done" : "blocked"; + writeRunMetadata(projectRoot, runId, { + schemaVersion: 1, + product: "codex-game-studio", + runId, + startedAt: nowIso(), + finishedAt: nowIso(), + projectRoot, + maxConcurrency, + requestedTaskIds: options.taskIds ?? [], + selectedTaskIds, + dryRun: false, + review: Boolean(options.review), + fix: Boolean(options.fix), + status, + summary: selectedTaskIds.map((taskId) => ({ taskId, status: getTask(readTaskStore(projectRoot), taskId).status, runPath: path.relative(projectRoot, taskRunDir(projectRoot, runId, taskId)) })) + }); + appendEvent(projectRoot, runId, { event: "orchestration.finished", status }); + output.push(`orchestration status: ${status}`); + return { runId, status, selectedTaskIds, startedTaskIds, blockedTaskIds, skippedTaskIds, output: output.join("\n") }; +} diff --git a/src/runner.ts b/src/runner.ts index c4fea96..22c649e 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,9 +1,8 @@ -import { spawnSync } from "node:child_process"; import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs"; import path from "node:path"; import { explainApprovalMismatch, normalizeApprovalScope, readApprovalStore, type ApprovalMismatchDiagnostic } from "./approvals.js"; import { readProjectAgentPrompt } from "./agents.js"; -import { buildCodexExecArgs, resolveCodexCommand, type CodexExecutionResult } from "./codex-runtime.js"; +import { buildCodexExecArgs, executeCodexCommand, resolveCodexCommand, type CodexExecutionResult } from "./codex-runtime.js"; import { renderCodexPrompt } from "./codex-prompts.js"; import { createCodexStudioSession, type VerificationCommand } from "./codex-session.js"; import { selectContextEntries, type ContextManifestEntry, type ContextRequestEntry } from "./context-manifest.js"; @@ -24,6 +23,7 @@ export type RunOptions = { dryRun?: boolean; codexBin?: string; includeArtifact?: string[]; + approvedFiles?: string[]; approvalScope?: string[]; allowBroadContext?: boolean; verifyCommand?: VerificationCommand; @@ -32,6 +32,7 @@ export type RunOptions = { maxFixPasses?: number; approvedByUser?: boolean; constrainedSandbox?: boolean; + noWrite?: boolean; }; export type PreparedRun = { @@ -92,24 +93,12 @@ export function codexExecInvocation(projectRoot: string, codexBin = resolveCodex return { command: codexBin, args, display: [codexBin, ...args.map((arg) => JSON.stringify(arg))].join(" ") }; } -export function executeCodexPromptSync(run: PreparedRun, prompt: string, command = run.codexCommand): CodexExecutionResult { - const result = spawnSync(command.command, command.args, { - cwd: run.projectRoot, - encoding: "utf8", - input: prompt, - shell: false - }); - return { - status: result.status, - signal: result.signal, - stdout: result.stdout ?? "", - stderr: result.stderr ?? "", - error: result.error - }; +export async function executeCodexPromptForRun(run: PreparedRun, prompt: string, command = run.codexCommand): Promise { + return await executeCodexCommand(command, prompt, { cwd: run.projectRoot }); } -export function executeCodexRun(run: PreparedRun): CodexExecutionResult { - return executeCodexPromptSync(run, run.prompt); +export async function executeCodexRun(run: PreparedRun): Promise { + return await executeCodexPromptForRun(run, run.prompt); } function safeArtifact(projectRoot: string, artifact: string): { full: string; display: string } { @@ -212,10 +201,10 @@ export function parseReviewJson(raw: string): ReviewResult { return { blockers: parsed.blockers, warnings: parsed.warnings, summary: parsed.summary, needsFix: parsed.needsFix }; } -function runReviewPass(run: PreparedRun, previousSummary = ""): ReviewPassResult | undefined { +async function runReviewPass(run: PreparedRun, previousSummary = ""): Promise { if (!run.reviewPrompt) return undefined; const prompt = `${run.reviewPrompt}\n\n# Implementation and verification output\n\n${previousSummary}\n`; - const execution = executeCodexPromptSync(run, prompt, run.reviewCodexCommand ?? run.codexCommand); + const execution = await executeCodexPromptForRun(run, prompt, run.reviewCodexCommand ?? run.codexCommand); const raw = execution.stdout.trim() || execution.stderr.trim(); if (executionFailed(execution)) return { execution, raw, malformed: execution.error?.message ?? `review exited with status ${execution.status}` }; try { @@ -269,7 +258,7 @@ function blockedAfter(implementation: CodexExecutionResult, verification?: Verif export async function executeRunLifecycle(run: PreparedRun): Promise { const output: string[] = []; - const implementation = executeCodexRun(run); + const implementation = await executeCodexRun(run); output.push(...formatCodex("implementation", implementation)); let verification: VerificationResult | undefined; @@ -278,7 +267,7 @@ export async function executeRunLifecycle(run: PreparedRun): Promise extends infer role: role.id, objective: task, approvedGlobs: approvalScopes, - approvedFiles: artifactDisplays.length ? artifactDisplays : undefined, + approvedFiles: options.approvedFiles ?? (artifactDisplays.length ? artifactDisplays : undefined), projectStage: studio.mode, studioMode: studio.studioMode }); @@ -447,7 +436,7 @@ function prepareCustomRun(role: ReturnType extends infer const runDir = path.join(projectRoot, ".codex", "runs", `${runId}-${role.id}`); const promptPath = path.join(runDir, "prompt.md"); const metadataPath = path.join(runDir, "metadata.json"); - if (!options.printPrompt && !options.dryRun) { + if (!options.printPrompt && !options.dryRun && !options.noWrite) { mkdirSync(runDir, { recursive: true }); writeFileSync(promptPath, prompt); writeFileSync( @@ -515,7 +504,7 @@ export function prepareRun(roleInput: string, options: RunOptions, cwd = process role, objective: task, approvedGlobs: approvalScopes, - approvedFiles: artifactDisplays.length ? artifactDisplays : undefined, + approvedFiles: options.approvedFiles ?? (artifactDisplays.length ? artifactDisplays : undefined), projectStage: studio.mode, studioMode: studio.studioMode }); @@ -616,7 +605,7 @@ export function prepareRun(roleInput: string, options: RunOptions, cwd = process const runDir = path.join(projectRoot, ".codex", "runs", `${runId}-${role}`); const promptPath = path.join(runDir, "prompt.md"); const metadataPath = path.join(runDir, "metadata.json"); - if (!options.printPrompt && !options.dryRun) { + if (!options.printPrompt && !options.dryRun && !options.noWrite) { mkdirSync(runDir, { recursive: true }); writeFileSync(promptPath, prompt); writeFileSync( diff --git a/src/tasks.ts b/src/tasks.ts index a049940..3e65340 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -2,12 +2,24 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync import path from "node:path"; import { createCodexStudioSession, type VerificationCommand } from "./codex-session.js"; import { renderCodexPrompt } from "./codex-prompts.js"; +import { projectRelativePath } from "./customization.js"; import { resolveProjectRoot } from "./paths.js"; import { readStudioProject } from "./projects.js"; import { isStudioRoleId, type StudioRoleId } from "./roles.js"; import { executeRunLifecycle, prepareRun, type PreparedRun, type RunLifecycleResult } from "./runner.js"; -export type StudioTaskStatus = "ready" | "running" | "blocked" | "done"; +export type StudioTaskStatus = "ready" | "running" | "blocked" | "done" | "cancelled" | "skipped"; + +export type StudioTaskDependency = { + taskId: string; + requiredStatus: "done"; +}; + +export type StudioTaskRunPolicy = { + maxFixPasses?: number; + review?: boolean; + constrainedSandbox?: boolean; +}; export type StudioTask = { id: string; @@ -15,17 +27,27 @@ export type StudioTask = { role: StudioRoleId; status: StudioTaskStatus; files: string[]; + writeFiles: string[]; + dependencies: StudioTaskDependency[]; + workflowId?: string; + groupId?: string; + priority: number; verification?: VerificationCommand; + runPolicy?: StudioTaskRunPolicy; notes: string[]; + createdAt: string; + updatedAt: string; + lastRunId?: string; }; export type TaskStore = { - schemaVersion: 1; + schemaVersion: 2; tasks: StudioTask[]; }; export type ExecuteTaskRunOptions = { dryRun?: boolean; + noWrite?: boolean; codexBin?: string; review?: boolean; fix?: boolean; @@ -41,37 +63,109 @@ export type ExecuteTaskRunResult = { lifecycle?: RunLifecycleResult; }; +const migrationTimestamp = "1970-01-01T00:00:00.000Z"; +const statuses = new Set(["ready", "running", "blocked", "done", "cancelled", "skipped"]); + +type LegacyTask = { + id: string; + title: string; + role: StudioRoleId; + status: StudioTaskStatus; + files?: string[]; + verification?: VerificationCommand; + notes?: string[]; +}; + +type RawTask = Partial & LegacyTask; + export function taskStorePath(projectRoot: string): string { return path.join(projectRoot, ".codex", "tasks.json"); } +function nowIso(): string { + return new Date().toISOString(); +} + +function assertLiteralTaskPath(projectRoot: string, value: string, label: string): string { + if (value.includes("*") || value.includes("?") || value.includes("[")) throw new Error(`${label} must be a literal relative file path, not a glob: ${value}`); + if (value.endsWith("/") || value.endsWith("\\")) throw new Error(`${label} must be a file path, not a directory: ${value}`); + const safe = projectRelativePath(projectRoot, value); + if (!safe.ok) throw new Error(`${label} ${safe.error}: ${value}`); + if (safe.display === ".git" || safe.display.startsWith(".git/")) throw new Error(`${label} cannot target .git: ${value}`); + return safe.display; +} + +export function normalizeTaskFiles(projectRoot: string, values: string[] = [], label = "task file"): string[] { + return [...new Set(values.map((value) => assertLiteralTaskPath(projectRoot, value, label)))]; +} + +function normalizeDependency(value: string | StudioTaskDependency): StudioTaskDependency { + if (typeof value === "string") return { taskId: value, requiredStatus: "done" }; + return { taskId: value.taskId, requiredStatus: "done" }; +} + +function normalizeTask(raw: RawTask, projectRoot: string, timestamp = migrationTimestamp): StudioTask { + if (!raw.id || typeof raw.id !== "string") throw new Error("Invalid task id"); + if (!raw.title || typeof raw.title !== "string") throw new Error(`Invalid task title: ${raw.id}`); + if (!isStudioRoleId(raw.role)) throw new Error(`Invalid task role: ${raw.role}`); + if (!statuses.has(raw.status)) throw new Error(`Invalid task status: ${raw.status}`); + const files = normalizeTaskFiles(projectRoot, raw.files ?? [], "task file"); + const writeFiles = normalizeTaskFiles(projectRoot, raw.writeFiles ?? [], "task writeFile"); + const dependencies = (raw.dependencies ?? []).map(normalizeDependency); + const notes = raw.notes ?? []; + if (!Array.isArray(notes) || !notes.every((note) => typeof note === "string")) throw new Error(`Invalid task notes: ${raw.id}`); + return { + id: raw.id, + title: raw.title, + role: raw.role, + status: raw.status, + files, + writeFiles, + dependencies, + workflowId: raw.workflowId, + groupId: raw.groupId, + priority: Number.isFinite(raw.priority) ? Number(raw.priority) : 0, + verification: raw.verification, + runPolicy: raw.runPolicy, + notes, + createdAt: raw.createdAt ?? timestamp, + updatedAt: raw.updatedAt ?? timestamp, + lastRunId: raw.lastRunId + }; +} + +function validateTaskStore(store: TaskStore): TaskStore { + const ids = new Set(); + for (const task of store.tasks) { + if (ids.has(task.id)) throw new Error(`Duplicate task id: ${task.id}`); + ids.add(task.id); + for (const dependency of task.dependencies) { + if (!dependency.taskId) throw new Error(`Invalid dependency on task ${task.id}`); + } + } + return store; +} + export function readTaskStore(projectRoot: string): TaskStore { const file = taskStorePath(projectRoot); - if (!existsSync(file)) return { schemaVersion: 1, tasks: [] }; - let parsed: TaskStore; + if (!existsSync(file)) return { schemaVersion: 2, tasks: [] }; + let parsed: { schemaVersion?: number; tasks?: RawTask[] }; try { - parsed = JSON.parse(readFileSync(file, "utf8")) as TaskStore; + parsed = JSON.parse(readFileSync(file, "utf8")) as { schemaVersion?: number; tasks?: RawTask[] }; } catch (error) { throw new Error(`Invalid task store JSON: ${(error as Error).message}`); } - if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.tasks)) throw new Error("Invalid task store schema"); - const ids = new Set(); - for (const task of parsed.tasks) { - if (ids.has(task.id)) throw new Error(`Duplicate task id: ${task.id}`); - ids.add(task.id); - if (!isStudioRoleId(task.role)) throw new Error(`Invalid task role: ${task.role}`); - if (!["ready", "running", "blocked", "done"].includes(task.status)) throw new Error(`Invalid task status: ${task.status}`); - if (!Array.isArray(task.files) || !Array.isArray(task.notes)) throw new Error(`Invalid task shape: ${task.id}`); - } - return parsed; + if ((parsed.schemaVersion !== 1 && parsed.schemaVersion !== 2) || !Array.isArray(parsed.tasks)) throw new Error("Invalid task store schema"); + return validateTaskStore({ schemaVersion: 2, tasks: parsed.tasks.map((task) => normalizeTask(task, projectRoot)) }); } export function writeTaskStore(projectRoot: string, store: TaskStore): void { + validateTaskStore(store); const dir = path.join(projectRoot, ".codex"); mkdirSync(dir, { recursive: true }); const tmp = path.join(dir, `tasks.${process.pid}.${Date.now()}.tmp`); try { - writeFileSync(tmp, `${JSON.stringify(store, null, 2)}\n`); + writeFileSync(tmp, `${JSON.stringify({ schemaVersion: 2, tasks: store.tasks }, null, 2)}\n`); renameSync(tmp, taskStorePath(projectRoot)); } catch (error) { rmSync(tmp, { force: true }); @@ -87,33 +181,61 @@ function nextTaskId(tasks: StudioTask[]): string { return `task-${String(max + 1).padStart(3, "0")}`; } -function getTask(store: TaskStore, taskId: string): StudioTask { +export function getTask(store: TaskStore, taskId: string): StudioTask { const task = store.tasks.find((candidate) => candidate.id === taskId); if (!task) throw new Error(`Unknown task ${taskId}`); return task; } -export function createTask(projectRoot: string, input: { title: string; role: StudioRoleId; verification?: VerificationCommand; files?: string[] }): StudioTask { +export function createTask( + projectRoot: string, + input: { + title: string; + role: StudioRoleId; + verification?: VerificationCommand; + files?: string[]; + writeFiles?: string[]; + dependencies?: string[]; + workflowId?: string; + groupId?: string; + priority?: number; + runPolicy?: StudioTaskRunPolicy; + } +): StudioTask { if (!input.title.trim()) throw new Error("task title is required"); readStudioProject(projectRoot); const store = readTaskStore(projectRoot); + for (const dependency of input.dependencies ?? []) { + if (!store.tasks.some((task) => task.id === dependency)) throw new Error(`Unknown dependency task: ${dependency}`); + } + const timestamp = nowIso(); const task: StudioTask = { id: nextTaskId(store.tasks), title: input.title.trim(), role: input.role, status: "ready", - files: input.files ?? [], + files: normalizeTaskFiles(projectRoot, input.files ?? [], "--file"), + writeFiles: normalizeTaskFiles(projectRoot, input.writeFiles ?? [], "--write-file"), + dependencies: (input.dependencies ?? []).map((taskId) => ({ taskId, requiredStatus: "done" })), + workflowId: input.workflowId, + groupId: input.groupId, + priority: input.priority ?? 0, verification: input.verification, - notes: [] + runPolicy: input.runPolicy, + notes: [], + createdAt: timestamp, + updatedAt: timestamp }; - writeTaskStore(projectRoot, { schemaVersion: 1, tasks: [...store.tasks, task] }); + writeTaskStore(projectRoot, { schemaVersion: 2, tasks: [...store.tasks, task] }); return task; } -export function updateTaskStatus(projectRoot: string, taskId: string, status: StudioTaskStatus, note?: string): StudioTask { +export function updateTaskStatus(projectRoot: string, taskId: string, status: StudioTaskStatus, note?: string, updates: Partial> = {}): StudioTask { const store = readTaskStore(projectRoot); const task = getTask(store, taskId); task.status = status; + task.updatedAt = nowIso(); + if (updates.lastRunId) task.lastRunId = updates.lastRunId; if (note?.trim()) task.notes.push(`${new Date().toISOString()} ${note.trim()}`); writeTaskStore(projectRoot, store); return task; @@ -142,19 +264,21 @@ export async function executeTaskRun(projectRoot: string, taskId: string, option project: projectRoot, task: task.title, dryRun: options.dryRun, + noWrite: options.noWrite, codexBin: options.codexBin, includeArtifact: task.files, + approvedFiles: task.writeFiles.length ? task.writeFiles : undefined, verifyCommand: task.verification, - review: options.review, + review: options.review ?? task.runPolicy?.review, fix: options.fix, - maxFixPasses: options.maxFixPasses, + maxFixPasses: options.maxFixPasses ?? task.runPolicy?.maxFixPasses, approvedByUser: options.approvedByUser, - constrainedSandbox: options.constrainedSandbox, + constrainedSandbox: options.constrainedSandbox ?? task.runPolicy?.constrainedSandbox, approvalScope: options.approvalScope }, process.cwd() ); - if (options.dryRun) return { task, prepared }; + if (options.dryRun || options.noWrite) return { task, prepared }; updateTaskStatus(projectRoot, taskId, "running", "Codex task run started"); try { diff --git a/src/validation.ts b/src/validation.ts index 2109f3a..c96a80a 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -233,7 +233,7 @@ export async function validateRepo(root = process.cwd()): Promise; + output: string; +}; + +export const workflowTaskRecipes: Partial> = { + "vertical-slice": { + workflowId: "vertical-slice", + title: "Vertical Slice Task Graph", + tasks: [ + { key: "plan", title: "Plan the smallest production-quality vertical slice", role: "producer", files: ["documentation/design/gdd.md"], writeFiles: [], dependencies: [] }, + { key: "design", title: "Define vertical-slice acceptance criteria and feature rules", role: "game-designer", files: ["documentation/design/gdd.md"], writeFiles: [], dependencies: ["plan"] }, + { key: "implement", title: "Implement the vertical-slice core loop", role: "gameplay-programmer", files: ["documentation/design/gdd.md"], writeFiles: [], dependencies: ["design"] }, + { key: "qa", title: "Verify vertical-slice playability and blockers", role: "qa-playtester", files: ["documentation/design/gdd.md"], writeFiles: [], dependencies: ["implement"] } + ] + }, + bugfix: { + workflowId: "bugfix", + title: "Bugfix Task Graph", + tasks: [ + { key: "repro", title: "Reproduce and document the bug", role: "qa-playtester", files: [], writeFiles: [], dependencies: [] }, + { key: "fix", title: "Implement the smallest safe bug fix", role: "gameplay-programmer", files: [], writeFiles: [], dependencies: ["repro"] }, + { key: "verify", title: "Verify the bug fix and regression risk", role: "qa-playtester", files: [], writeFiles: [], dependencies: ["fix"] } + ] + }, + "ui-ux-review": { + workflowId: "ui-ux-review", + title: "UI/UX Review Task Graph", + tasks: [ + { key: "ux", title: "Review the UI flow and interaction risks", role: "ui-ux-designer", files: ["documentation/design/gdd.md"], writeFiles: [], dependencies: [] }, + { key: "accessibility", title: "Review accessibility gaps in the UI flow", role: "accessibility-specialist", files: ["documentation/design/gdd.md"], writeFiles: [], dependencies: ["ux"] }, + { key: "qa", title: "Verify UI/UX review evidence and blockers", role: "qa-playtester", files: ["documentation/design/gdd.md"], writeFiles: [], dependencies: ["accessibility"] } + ] + }, + "release-checklist": { + workflowId: "release-checklist", + title: "Release Checklist Task Graph", + tasks: [ + { key: "qa", title: "Validate release test evidence", role: "qa-playtester", files: ["documentation/production/timeline.md"], writeFiles: [], dependencies: [] }, + { key: "perf", title: "Review release performance risks", role: "performance-analyst", files: ["documentation/production/timeline.md"], writeFiles: [], dependencies: [] }, + { key: "security", title: "Review release security risks", role: "security-engineer", files: ["documentation/production/timeline.md"], writeFiles: [], dependencies: [] }, + { key: "release", title: "Synthesize ship or no-ship release checklist", role: "release-manager", files: ["documentation/production/timeline.md"], writeFiles: [], dependencies: ["qa", "perf", "security"] } + ] + } +}; + +export function workflowRecipeIds(): WorkflowId[] { + return Object.keys(workflowTaskRecipes) as WorkflowId[]; +} + +function assertRecipe(id: string): WorkflowTaskRecipe { + if (!workflowRegistry[id as WorkflowId]) throw new Error(`Unknown workflow "${id}"`); + const recipe = workflowTaskRecipes[id as WorkflowId]; + if (!recipe) throw new Error(`Workflow "${id}" does not have a task recipe`); + return recipe; +} + +function formatRecipe(recipe: WorkflowTaskRecipe, groupId: string, created?: StudioTask[]): string { + const lines = [`Workflow task recipe: ${recipe.workflowId}`, `Group: ${groupId}`]; + for (const item of recipe.tasks) { + const createdTask = created?.find((task) => task.title === item.title); + lines.push(`- ${createdTask?.id ?? item.key}: [${item.role}] ${item.title}`); + lines.push(` deps: ${item.dependencies.join(", ") || "none"}; writeFiles: ${item.writeFiles.join(", ") || "none"}`); + } + return lines.join("\n"); +} + +export function createWorkflowTasks(projectRoot: string, workflowId: string, options: { dryRun?: boolean } = {}): CreateWorkflowTasksResult { + const recipe = assertRecipe(workflowId); + const groupId = `${recipe.workflowId}-${randomUUID().slice(0, 8)}`; + if (options.dryRun) return { workflowId: recipe.workflowId, groupId, dryRun: true, tasks: recipe.tasks, output: formatRecipe(recipe, groupId) }; + + const createdByKey = new Map(); + const created: StudioTask[] = []; + for (const item of recipe.tasks) { + const dependencies = item.dependencies.map((key) => { + const dependency = createdByKey.get(key); + if (!dependency) throw new Error(`Recipe ${recipe.workflowId} has unresolved dependency ${key}`); + return dependency.id; + }); + const task = createTask(projectRoot, { + title: item.title, + role: item.role, + files: item.files, + writeFiles: item.writeFiles, + dependencies, + workflowId: recipe.workflowId, + groupId, + verification: item.verification + }); + createdByKey.set(item.key, task); + created.push(task); + } + return { workflowId: recipe.workflowId, groupId, dryRun: false, tasks: created, output: formatRecipe(recipe, groupId, created) }; +} diff --git a/tests/ccgs-adaptation.test.ts b/tests/ccgs-adaptation.test.ts new file mode 100644 index 0000000..328ee9c --- /dev/null +++ b/tests/ccgs-adaptation.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "vitest"; +import { ccgsInventory, ccgsRoleAdaptations, ccgsSkillAdaptations, validateCcgsAdaptation } from "../src/ccgs-adaptation.js"; +import { studioRoleIds } from "../src/roles.js"; + +describe("curated CCGS adaptation registry", () => { + test("documents a pinned reviewed source without claiming clone parity", () => { + expect(ccgsInventory.source).toContain("Claude-Code-Game-Studios"); + expect(ccgsInventory.reviewedAt).toBe("2026-06-25"); + expect(ccgsInventory.policy).toMatch(/Curated reference/i); + }); + + test("maps CCGS roles without reintroducing legacy underscore ids", () => { + expect(ccgsRoleAdaptations.length).toBeGreaterThanOrEqual(49); + for (const legacy of ["producer_agent", "qa_agent", "master_orchestrator"]) { + expect(studioRoleIds).not.toContain(legacy as never); + } + expect(ccgsRoleAdaptations.find((item) => item.sourceId === "lead-programmer")?.decision).toBe("built-in-add-candidate"); + expect(ccgsRoleAdaptations.find((item) => item.sourceId === "godot-gdscript-specialist")?.decision).toBe("specialty-context"); + }); + + test("maps team skills to recipes and skill-maintenance surfaces out of scope", () => { + expect(ccgsSkillAdaptations.find((item) => item.sourceId === "team-ui")?.decision).toBe("recipe"); + expect(ccgsSkillAdaptations.find((item) => item.sourceId === "skill-test")?.decision).toBe("out-of-scope"); + expect(validateCcgsAdaptation()).toEqual([]); + }); +}); diff --git a/tests/cli-prompt-surface.test.ts b/tests/cli-prompt-surface.test.ts index b1a7247..c4adca5 100644 --- a/tests/cli-prompt-surface.test.ts +++ b/tests/cli-prompt-surface.test.ts @@ -78,15 +78,20 @@ describe("built CLI prompt surface", () => { path.join(projectRoot, ".codex", "tasks.json"), `${JSON.stringify( { - schemaVersion: 1, + schemaVersion: 2, tasks: [ { id: "task-001", title: "Implement jump feel", role: "gameplay-programmer", status: "ready", - files: ["source/player.gd"], - notes: [] + files: ["documentation/design/gdd.md"], + writeFiles: ["source/player.gd"], + dependencies: [], + priority: 0, + notes: [], + createdAt: "2026-06-25T00:00:00.000Z", + updatedAt: "2026-06-25T00:00:00.000Z" } ] }, @@ -329,4 +334,20 @@ describe("built CLI prompt surface", () => { const constrained = runCli(["run", "gameplay-programmer", "--project", projectRoot, "--dry-run", "--approved-by-user", "--constrained-sandbox", "Implement jump"], cwd); expect(constrained).toContain("Sandbox: workspace-write"); }); + + test("workflow recipes and task orchestrate are visible through the built CLI", () => { + const { cwd, projectRoot } = initCliProject("ogs-cli-orchestrate-", "Orchestrate Game"); + + const recipe = runCli(["workflow", "create-tasks", "vertical-slice", "--project", projectRoot, "--dry-run"], cwd); + expect(recipe).toContain("Workflow task recipe: vertical-slice"); + + const render = runCli(["workflow", "vertical-slice", "--project", projectRoot], cwd); + expect(render).toContain("# Codex Game Studio Session"); + + const created = runCli(["workflow", "create-tasks", "bugfix", "--project", projectRoot], cwd); + expect(created).toContain("Workflow task recipe: bugfix"); + + const orchestration = runCli(["task", "orchestrate", "--project", projectRoot, "--dry-run"], cwd); + expect(orchestration).toContain("Orchestration plan: 3 task(s), max concurrency 1"); + }); }); diff --git a/tests/orchestrator.test.ts b/tests/orchestrator.test.ts new file mode 100644 index 0000000..b605059 --- /dev/null +++ b/tests/orchestrator.test.ts @@ -0,0 +1,63 @@ +import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } 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 { createTask, readTaskStore } from "../src/tasks.js"; +import { orchestrateTasks } from "../src/orchestrator.js"; + +describe("task orchestration", () => { + test("dry-run orchestration plans waves without mutating task state or runs", async () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-orchestrate-dry-")); + const { projectRoot } = initProject({ name: "Dry Orchestration Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd); + const task = createTask(projectRoot, { title: "Implement jump", role: "gameplay-programmer", files: ["documentation/design/gdd.md"], writeFiles: ["source/project-dry-orchestration-game/player.gd"] }); + const before = readFileSync(path.join(projectRoot, ".codex", "tasks.json"), "utf8"); + + const result = await orchestrateTasks({ project: projectRoot, taskIds: [task.id], dryRun: true, maxConcurrency: 1 }); + + expect(result.status).toBe("planned"); + expect(result.output).toContain(task.id); + expect(result.output).toContain("locks: source/project-dry-orchestration-game/player.gd"); + expect(readFileSync(path.join(projectRoot, ".codex", "tasks.json"), "utf8")).toBe(before); + expect(existsSync(path.join(projectRoot, ".codex", "runs", result.runId))).toBe(false); + }); + + test("bounded orchestration runs dependency-ordered tasks and records local metadata", async () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-orchestrate-run-")); + const { projectRoot } = initProject({ name: "Run Orchestration Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd); + const log = path.join(cwd, "codex-log.jsonl"); + const stub = path.join(cwd, "codex-stub.mjs"); + writeFileSync( + stub, + `#!/usr/bin/env node +import { appendFileSync, readFileSync } from "node:fs"; +const input = readFileSync(0, "utf8"); +appendFileSync(${JSON.stringify(log)}, JSON.stringify({ args: process.argv.slice(2), objective: /Objective: (.*)/.exec(input)?.[1] ?? "" }) + "\\n"); +console.log("ok"); +` + ); + chmodSync(stub, 0o755); + + const first = createTask(projectRoot, { title: "Implement movement", role: "gameplay-programmer", writeFiles: ["source/project-run-orchestration-game/player.gd"] }); + const second = createTask(projectRoot, { title: "Verify movement", role: "qa-playtester", dependencies: [first.id] }); + + const result = await orchestrateTasks({ project: projectRoot, taskIds: [first.id, second.id], maxConcurrency: 2, codexBin: stub }); + + expect(result.status).toBe("done"); + const store = readTaskStore(projectRoot); + expect(store.tasks.find((task) => task.id === first.id)?.status).toBe("done"); + expect(store.tasks.find((task) => task.id === second.id)?.status).toBe("done"); + expect(existsSync(path.join(projectRoot, ".codex", "runs", result.runId, "orchestration.json"))).toBe(true); + expect(existsSync(path.join(projectRoot, ".codex", "runs", result.runId, "tasks", first.id, "output.txt"))).toBe(true); + const invocations = readFileSync(log, "utf8").trim().split("\n"); + expect(invocations).toHaveLength(2); + }); + + test("max concurrency is capped", async () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-orchestrate-cap-")); + const { projectRoot } = initProject({ name: "Cap Orchestration Game", engine: "godot", mode: "prototype", studioMode: "fast-prototype", nonInteractive: true }, cwd); + createTask(projectRoot, { title: "Implement jump", role: "gameplay-programmer" }); + + await expect(orchestrateTasks({ project: projectRoot, maxConcurrency: 4, dryRun: true })).rejects.toThrow(/cannot exceed 3/i); + }); +}); diff --git a/tests/workflow-recipes.test.ts b/tests/workflow-recipes.test.ts new file mode 100644 index 0000000..608ce0a --- /dev/null +++ b/tests/workflow-recipes.test.ts @@ -0,0 +1,41 @@ +import { existsSync, 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 { readTaskStore } from "../src/tasks.js"; +import { createWorkflowTasks, workflowRecipeIds } from "../src/workflow-recipes.js"; + +describe("workflow task recipes", () => { + test("vertical-slice dry-run prints proposed task graph without writing tasks", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-recipe-dry-")); + const { projectRoot } = initProject({ name: "Recipe Dry Game", engine: "godot", mode: "prototype", nonInteractive: true }, cwd); + const before = readTaskStore(projectRoot).tasks.length; + + const result = createWorkflowTasks(projectRoot, "vertical-slice", { dryRun: true }); + + expect(result.dryRun).toBe(true); + expect(result.output).toContain("vertical-slice"); + expect(result.output).toContain("Implement the vertical-slice core loop"); + expect(readTaskStore(projectRoot).tasks.length).toBe(before); + }); + + test("release-checklist recipe creates explicit dependent tasks", () => { + const cwd = mkdtempSync(path.join(tmpdir(), "ogs-recipe-run-")); + const { projectRoot } = initProject({ name: "Recipe Run Game", engine: "godot", mode: "development", nonInteractive: true }, cwd); + + const result = createWorkflowTasks(projectRoot, "release-checklist"); + + expect(result.dryRun).toBe(false); + const store = readTaskStore(projectRoot); + const tasks = store.tasks.filter((task) => task.workflowId === "release-checklist"); + expect(tasks).toHaveLength(4); + const release = tasks.find((task) => task.role === "release-manager"); + expect(release?.dependencies).toHaveLength(3); + expect(existsSync(path.join(projectRoot, ".codex", "tasks.json"))).toBe(true); + }); + + test("recipe registry exposes only implemented explicit recipes", () => { + expect(workflowRecipeIds()).toEqual(expect.arrayContaining(["vertical-slice", "bugfix", "ui-ux-review", "release-checklist"])); + }); +});