diff --git a/.claude/rules/ticket-cli.md b/.claude/rules/ticket-cli.md index a54b7f856..997ef48d3 100644 --- a/.claude/rules/ticket-cli.md +++ b/.claude/rules/ticket-cli.md @@ -37,7 +37,7 @@ pql ticket board [--team T] # kanban view pql ticket status T-440 in_progress # backlog|ready|in_progress|review|done|cancelled pql ticket assign T-440 dudley pql ticket team T-440 server -pql ticket setparent T-9 T-2 # (clear with no parent arg) +pql ticket setparent T-9 T-2 # (clear with: setparent T-9 none) pql ticket append T-440 "extra context" # also --file PATH / --stdin # Dependencies diff --git a/.claude/skills/atlas/SKILL.md b/.claude/skills/atlas/SKILL.md index e1c84293a..97e8dcfd8 100644 --- a/.claude/skills/atlas/SKILL.md +++ b/.claude/skills/atlas/SKILL.md @@ -4,7 +4,7 @@ description: > Query and manage celestial bodies and stations in systems.db. Use when the user says "atlas", "show system", "list bodies", "list stations", "author system", "show body", or invokes /atlas. Wraps the Rust atlas CLI binary. Also provides - the full per-system authoring pipeline via "author-system" subcommand. + the full per-system authoring pipeline via the /atlas author-system workflow. user-invocable: true allowed-tools: Bash, Read, Grep, Glob, Write, Edit, Agent --- @@ -115,15 +115,13 @@ tooling/atlas stats ``` Returns: system count, body count by type, inhabited count, station count by type. -### Bulk populate from system data (classifier pass) +### Corridor status (remaining unfinished systems) ```bash -tooling/atlas populate --dry-run # preview what would be created -tooling/atlas populate # create body/station records +tooling/atlas corridor-status ``` -Reads `habitable_planet_count`, `inhabited_planet_count`, `gas_giant`, -`asteroid_belt`, and `horizon_station` from existing system data. -Creates: planets (inhabited first), gas giants, asteroid belts, oort cloud per -system, horizon station per oort cloud. Skips systems that already have bodies. +Shows remaining unfinished systems grouped by geographic sector and hop +distance. Supersedes the old bulk-populate classifier pass (removed) — use +the per-system `author` → review → `commit-system` workflow below instead. ### Update a system-level field ```bash @@ -149,20 +147,33 @@ the commit message (defaults to `east_reach`). ## Body ID Naming Convention +Real forms, verified against the ~3,300 IDs in `docs/atlas/proposals/`: + ``` -GJ-{n} — star (single) -GJ-{n}A — primary star (binary) -GJ-{n}B — secondary star (binary) -GJ-{n}b/c/d... — planets, innermost first -GJ-{n}Ab/c/d... — planets orbiting primary only -GJ-{n}Bb/c/d... — planets orbiting secondary only -GJ-{n}d-1 — first moon of third planet -GJ-{n}d-S1 — first station orbiting third planet -GJ-{n}-oort — oort cloud region -GJ-{n}-belt — asteroid belt -GJ-{n}-oort-S1 — horizon station in oort cloud +GJ {n} — system_id (space, e.g. "GJ 105A"; "A"/"B" suffix for binary) +GJ{n}A — primary star's body prefix (no dash after GJ) +GJ{n}Ab/c/d... — planets orbiting primary, innermost first (no dash) +GJ{n}Bb/c/d... — planets orbiting secondary only +GJ{n}Ae-m1 — first moon of body "e" (both forms seen — see below) +GJ{n}Ae-1 — first moon of body "e" (bare-number form, also seen) +GJ{n}Ac-S1 — first station orbiting body "c" +GJ{n}-belt — asteroid belt +GJ{n}-oort — oort cloud region +GJ{n}-oort-S1 — horizon station in oort cloud ``` +**Moon suffixes are not standardized** — both `-m{n}` (e.g. `GJ105Ae-m1`) and +bare `-{n}` (e.g. `GJ0d-1`) are in live use across proposals in roughly equal +numbers (~208 vs ~228 of 436 moon entries as of this check). Match whichever +form the system's existing proposal already uses; don't silently convert one +to the other. + +**Wiki slugs use the dashed form** (`GJ-105A`, spaces→dashes) for the system +directory under `wiki/star-systems/`, but body/station IDs inside that +directory keep the undashed body-ID form (e.g. +`wiki/star-systems/GJ-105A/bodies/GJ105Ac`) — the dash only applies to the +system-level slug, never to body IDs. + ## Entity Hierarchy ``` @@ -200,19 +211,32 @@ See `references/author-system.md` for the full workflow. - `references/review-ozzie.md` — Ozzie's review checklist (includes astrophysical sanity) - `references/integrity-check.md` — automated verification rules -## Scope — A True Atlas +## Asset Pipeline Relationship -This skill will grow to cover the full geographic hierarchy of the Reach: +See `.claude/rules/asset-pipeline.md` (the "Golden Rule": edit sources, not +`systems.db`) and D-223. The bodies/stations catalog this skill authors is +**atlas-CLI-owned** and survives `make regen-db`: `import_economics` only +*enriches* existing body rows (radius, axial tilt, biosphere class — see +`tooling/economy-db/economy_import/bodies.py`), it never deletes or +regenerates the catalog. What `import_economics` *does* clear on every regen +is the separate `atlas_*` geometry index tables (`atlas_cities`, +`atlas_roads`, etc., D-223) — those hold cascade-computed geometry, not the +hand-authored body/station identity data. So: **atlas write commands +(`add-body`, `add-station`, `author`/`commit-system`) are the sanctioned path +for the bodies/stations catalog**; they don't need a regen-db round-trip and +aren't at risk from one. -- **Galactic:** 301 systems, gate topology, sector/corridor data -- **System:** orbital bodies, stations, oort cloud -- **Planetary surface:** continents, oceans, mountain ranges, rivers, biome regions -- **Settlement:** cities, towns, villages, outposts, rail lines, road hierarchy -- **District:** neighborhoods, zones, named areas/provinces -- **Local:** named locations, landmarks, facilities +## Scope — Atlas Data Ownership -As the development cascade progresses through Phase 1 (wiki content) → Phase 3 -(planetary maps / Atlas of the Reach), the atlas CLI and its schema will extend -to cover each level. The goal is a single queryable geographic database from -galaxy scale to street level — the in-game implant's atlas app reads from this -same data. +The atlas CLI owns the **galactic** and **system** levels — star systems, +orbital bodies, stations — as hand-authored, reviewed data via this skill's +`author-system` pipeline. That catalog is durable (see "Asset Pipeline +Relationship" above). + +Everything below system scale — planetary surface, settlements, districts, +local detail — is **not** grown by extending this CLI or its schema. D-223 +retired the Python atlas geometry generator; the deterministic server-side +generation cascade (Phase 4, the active phase — see CLAUDE.md's Development +Cascade) is the sole producer of that geometry going forward. The `atlas_*` +index tables exist so the implant Atlas viewer can display cascade output, +not as a target for this skill to populate by hand. diff --git a/.claude/skills/atlas/references/integrity-check.md b/.claude/skills/atlas/references/integrity-check.md index fc736fa76..4f90b0c37 100644 --- a/.claude/skills/atlas/references/integrity-check.md +++ b/.claude/skills/atlas/references/integrity-check.md @@ -2,48 +2,64 @@ Run these checks on the proposal JSON before commit. All must pass. +`tooling/atlas-verify` only ever opens the proposal JSON file — it never opens +`systems.db`. Checks that require comparing against the live database (2, 4) +are **not enforced by the script** and must be performed manually or by the +authoring agent before commit. + ## Checks -1. **Inhabited bodies have names:** +1. **Inhabited bodies have names** — *(script-checked)* Every body with `inhabited: true` must have `proper_name` set (not null). -2. **Uninhabited bodies have no names:** - Every body with `inhabited: false` must have `proper_name: null`. +2. **Uninhabited bodies have no names** — *(manual/agent-performed — the + script explicitly skips this check)* + Every body with `inhabited: false` should have `proper_name: null`. Exception: bodies that serve as named research outposts (e.g., "Corrosion Bench") where the name IS the body's identity. -3. **All stations have names:** +3. **All stations have names** — *(script-checked)* Every station must have `proper_name` set. Use the station_id as the name if the station has no cultural name (e.g., "GJ406g-S1" for an anonymous fuel depot). -4. **No name collisions:** +4. **No name collisions** — *(manual/agent-performed — requires querying + `systems.db`, which the script never opens)* ```sql SELECT proper_name FROM bodies WHERE proper_name IS NOT NULL UNION SELECT proper_name FROM stations WHERE proper_name IS NOT NULL ``` No proposed name may duplicate an existing name in the database. -5. **Body count minimum:** +5. **Body count minimum** — *(script-checked)* - G/F stars: ≥ 8 planets (excluding moons, belts, oort) - K stars: ≥ 7 planets - - M dwarfs: ≥ 6 planets - - A/B stars: ≥ 6 planets + - M dwarfs and A/B stars: ≥ 6 planets (same threshold, script does not + distinguish these two cases) -6. **Required structures:** +6. **Required structures** — *(script-checked)* - Oort cloud body present - Horizon station present with `has_gate_infrastructure: true` - At least 1 asteroid belt (unless wiki explicitly contradicts) -7. **Orbit consistency:** +7. **Orbit consistency** — *(script-checked)* - `orbit_index` increases monotonically for top-level bodies - No duplicate `orbit_index` at the same parent level - - Moon `orbit_index` starts at 1 within parent + - Same monotonic + no-duplicate check applied per-parent to moon + `orbit_index` values (the script does not separately verify that moon + numbering starts at 1) -8. **Parent references valid:** +8. **Parent references valid** — *(script-checked)* - Every moon's `parent_body_id` references a body in the same proposal - Every station's `orbits_body_id` references a body in the same proposal +9. **star_type vs spectral_class consistency** — *(script-checked, not + previously documented here)* + The leading letter of `star_type` (e.g. "K") must match the leading class + letter of `spectral_class` after stripping dwarf/subdwarf prefixes (e.g. + "K3V" → "K"). Skipped when `star_type` is `"binary"` or `"unusual"`, or + when either field is missing. + ## Running the Check ```bash @@ -57,3 +73,12 @@ make atlas-verify The script reports all failures per file, not just the first one. Exit code 1 if any file has errors. + +**Exclusions:** `GJ0.json` (Sol) and `GJ1221.json` are permanently excluded +from the script's checks. Sol is hand-authored with different rules (named +uninhabited bodies, real Earth/Mars/Luna geography) and is exempt from the +normal generator/naming pipeline (D-223). GJ1221 is a dead-star remnant system +("stellar consumption remnant" per its proposal notes) with zero planets — +just a belt, an oort cloud, and a research horizon station — which would +otherwise fail the body-count-minimum check (5) that assumes every system has +planets. diff --git a/.claude/skills/atlas/references/naming-brief.md b/.claude/skills/atlas/references/naming-brief.md index 069cc91df..013f07c75 100644 --- a/.claude/skills/atlas/references/naming-brief.md +++ b/.claude/skills/atlas/references/naming-brief.md @@ -23,7 +23,8 @@ Collision avoidance — these names are already in use: UNION SELECT proper_name FROM stations WHERE proper_name IS NOT NULL} Name ALL inhabited bodies AND ALL stations in one pass. -Uninhabited bodies do NOT get names (proper_name stays null). +Uninhabited bodies do NOT get names (proper_name stays null) — unless the +name IS the body's identity (a named lore outpost, e.g. "Corrosion Bench"). ONE name + one-line rationale per location. Message only, no file. ``` @@ -38,4 +39,6 @@ ONE name + one-line rationale per location. Message only, no file. "The Exchange"). They should feel like real place names that happened to be assigned to transit/exchange facilities. - Every inhabited body and every station MUST get a proper_name. -- Uninhabited bodies MUST have proper_name = null (body_id is the UI label). +- Uninhabited bodies default to proper_name = null (body_id is the UI label) — + unless the name IS the body's identity (named lore outposts, e.g. "Corrosion + Bench"), matching the exception in `references/integrity-check.md` check 2. diff --git a/.claude/skills/audio-gen/SKILL.md b/.claude/skills/audio-gen/SKILL.md index 4c820e289..8d80072c7 100644 --- a/.claude/skills/audio-gen/SKILL.md +++ b/.claude/skills/audio-gen/SKILL.md @@ -19,6 +19,12 @@ Asset descriptions, filenames, bus routing, and design intent are documented in `docs/assets/audio/`. This skill provides the prompt system, generation workflow, and quality validation. +`audio-health`, `audio-generate`, and `audio-batch` re-exec into the project +`.venv` on startup (`audio-post` doesn't need to — it only shells out to +ffmpeg). On a fresh clone with no `.venv` yet, they fail fast with `error: +.venv not found — run make setup-venv first.` — run that once before using +this skill. + ## API Access **Never call the API directly.** Use the wrapper scripts: @@ -39,7 +45,7 @@ tooling/db/audio-generate "prompt text" \ --output-ogg client/assets/audio/final.ogg # Batch-generate from a manifest (preferred for multiple assets) -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json +tooling/db/audio-batch docs/assets/audio/.json ``` ### Parameters @@ -87,11 +93,12 @@ approvals to 2: one Write (manifest) + one Bash (batch run). ### 1. Create the manifest -Write a JSON manifest to `docs/assets/audio/batch-{sprint}-{ticket}.json`: +Write a JSON manifest to `docs/assets/audio/batch-T-.json` (current +ticket-ID scheme, e.g. `batch-T-327.json` — not the retired sprint scheme): ```json { - "description": "Sprint 10 ambient + world SFX batch", + "description": "T-327 ambient + world SFX batch", "output_dir": "client/assets/audio", "gen_dir": "client/assets/audio/gen", "defaults": { @@ -134,20 +141,27 @@ Write a JSON manifest to `docs/assets/audio/batch-{sprint}-{ticket}.json`: Asset `id` values must match IDs in `docs/assets/audio/{category}.md` (e.g., AMB-001, SFX-002, UI-005). This couples the manifest to the asset inventory. +**`lufs`/`quality` defaults apply to `synth` assets only.** `audio_batch.py`'s +SAO path (`run_sao_generate`) only forwards `steps`/`cfg`/`timeout` to the +connector — `--lufs`/`--quality` aren't even exposed as CLI flags on +`audio_connector.py generate`, so a manifest's `defaults.lufs`/`defaults.quality` +are silently ignored for `method: "sao"` assets. SAO post-processing is fixed +at -16 LUFS / quality 6 regardless of what the manifest says. + ### 2. Run the batch ```bash # Full run -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json +tooling/db/audio-batch docs/assets/audio/.json # Dry run — preview what would be generated -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --dry-run +tooling/db/audio-batch docs/assets/audio/.json --dry-run # Generate only specific assets -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --only AMB-001,AMB-002 +tooling/db/audio-batch docs/assets/audio/.json --only AMB-001,AMB-002 # Skip assets that already have OGG files -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --skip-existing +tooling/db/audio-batch docs/assets/audio/.json --skip-existing ``` ### 3. Update asset docs with prompts @@ -250,6 +264,9 @@ After generating, verify: ## File Placement Generated assets go to `client/assets/audio/` with exact filenames from the -asset docs. Intermediates go to `client/assets/audio/gen/` (gitignored). +asset docs. Intermediates go to `client/assets/audio/gen/` — untracked (the +directory doesn't exist until first use and isn't in `.gitignore`); delete +intermediates yourself after selecting the final candidate rather than +relying on git to ignore them. AudioManager discovers assets by directory scan — filenames must match exactly. diff --git a/.claude/skills/audio-gen/references/sonic-palette.md b/.claude/skills/audio-gen/references/sonic-palette.md index d9d2751d8..829aee682 100644 --- a/.claude/skills/audio-gen/references/sonic-palette.md +++ b/.claude/skills/audio-gen/references/sonic-palette.md @@ -32,7 +32,10 @@ For environmental sounds, human cognition, physical world. **Character:** Human warmth. Natural attack and decay. Reverberant — exists in physical space. The sound of a real place with real materials. -**Frequency range:** 60Hz-3kHz primary. Soft or natural attack, room tail. +**Frequency range:** 300Hz-1.5kHz primary. Soft or natural attack, room tail. +(The wider 60Hz-3kHz span belongs to the Station Baseline layered bed below — +low-frequency foundation through high-frequency detail — not to a single +Organic source sound.) **Used for:** fog_recognition, all amb_* ambient loops, all sfx_footstep_* diff --git a/.claude/skills/bug-report/SKILL.md b/.claude/skills/bug-report/SKILL.md index c176bc354..f14d0749b 100644 --- a/.claude/skills/bug-report/SKILL.md +++ b/.claude/skills/bug-report/SKILL.md @@ -6,7 +6,7 @@ description: > or invokes /bug-report. Scans the user:// bug-reports directory, summarizes each report, and offers investigation, ticket creation, or dismissal. user-invocable: true -allowed-tools: Bash, Read, Grep, Glob, Write +allowed-tools: Bash, Read, Grep, Glob --- # Bug Report Skill @@ -26,12 +26,13 @@ contains these files: | File | Purpose | |------|---------| -| `description.txt` | Tester notes + metadata (tick, room, stance, facing, position) | +| `description.txt` | Tester notes + metadata (tick, room, stance, facing, position, timestamp, ring-buffer depth) | | `render.txt` | Simplified text render of the game snapshot | +| `screenshot.png` | Screenshot of the game view at the moment the report was filed | | `snapshot.json` | Full JSON snapshot (entities, dialogue state, etc.) | | `inputs.jsonl` | Last 60 ticks of player input (replay format) | | `snapshots.jsonl` | Last 60 ticks of observer snapshots | -| `seed.txt` | RNG seed for deterministic replay | +| `seed.txt` | RNG seed for deterministic replay, or the literal string `unavailable` (see below) | ## Invocation @@ -42,10 +43,13 @@ contains these files: ### 1. Scan for unprocessed reports -List all report directories in the bug reports directory: +List all report directories in the bug reports directory. Match the +`gauntlet-*` naming pattern specifically, not a bare `*/` glob — this +excludes the `archive/` subdirectory (see "Create ticket" below) from being +mistaken for a report: ```bash -ls -1d "/var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/"*/ +ls -1d "/var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/"gauntlet-*/ ``` If no directories are found, report "No bug reports found." and stop. @@ -62,10 +66,17 @@ For each report directory, read the following files using the Read tool: - Tick number - Room name - Player stance, facing, position + - `Timestamp` (wall-clock time the report was filed) + - `RingBufferTicks` (depth of the `inputs.jsonl`/`snapshots.jsonl` history — + 60 today, but read it rather than assume) 2. **`render.txt`** — extract: - A brief description of what the text render shows (room layout, visible entities, player position marker) -3. **`snapshot.json`** — extract: +3. **`screenshot.png`** — view with the Read tool (it renders the image) and + note what it shows. Compare against `render.txt`/`snapshot.json` — a + mismatch between what the screenshot shows and what the snapshot data says + is itself a bug signal (see Tips: rendering vs simulation desync). +4. **`snapshot.json`** — extract: - Total entity count - Whether dialogue is active (look for `dialogue` or `conversation` keys with non-null/non-empty values) @@ -73,7 +84,11 @@ For each report directory, read the following files using the Read tool: non-empty values) - NPC names and positions if present - Any error or anomaly fields -4. **`seed.txt`** — note the seed value for reference +5. **`seed.txt`** — note the seed value for reference. If the file contains + the literal string `unavailable`, deterministic replay is not currently + possible (the server doesn't yet emit `rng_seed` on `ObserverSnapshot`) — + report "seed unavailable — replay not possible", don't paste the sentinel + into a ticket as if it were a usable seed. Do NOT read `inputs.jsonl` or `snapshots.jsonl` during the summary phase. These are large files reserved for the investigation step. @@ -121,9 +136,13 @@ Dig deeper into the report for root cause analysis: - State machine transitions that look wrong - Component values going out of expected range - Entities appearing or disappearing unexpectedly -4. Cross-reference with `render.txt` to confirm visual manifestation -5. Read `seed.txt` and note it — the seed plus `inputs.jsonl` should allow - deterministic replay of the scenario +4. Cross-reference with `render.txt` and `screenshot.png` to confirm visual + manifestation — a mismatch between the screenshot and the snapshot/render + data is itself the rendering-desync signal described in Tips +5. Read `seed.txt` and note it — if it holds a real seed, it plus + `inputs.jsonl` should allow deterministic replay of the scenario; if it + holds the `unavailable` sentinel, say so explicitly and skip the replay + step rather than treating the sentinel as a seed Present findings as a root cause analysis: @@ -143,8 +162,8 @@ Present findings as a root cause analysis: - {system 2}: {how it's involved} ### Reproduction -Seed: {seed} -Replay inputs.jsonl from tick {start} to reproduce. +Seed: {seed, or "unavailable — replay not possible" if seed.txt held the sentinel} +Replay inputs.jsonl from tick {start} to reproduce (seed permitting). ### Suggested fix {If identifiable from the snapshot data} @@ -166,20 +185,42 @@ nature of the bug: issues) Construct the ticket title and description from the report summary and any -investigation findings. Use the pql CLI: +investigation findings. Parent the ticket per the cascade model (CLAUDE.md +Development Cascade / `.claude/rules/ticket-cli.md`): +- **`--parent T-1037`** for maintenance/test-debt bugs (bugs in test + infrastructure, tooling, or process hygiene rather than game systems) +- **the active phase epic** for phase-scoped bugs (bugs in game systems + currently under construction) — don't hardcode the id; verify first with + `pql ticket show T-745 --tree` and use whichever phase epic has + `"status": "in_progress"` (currently `T-750`, Phase 4, but the cascade + advances) + +Use the pql CLI: ```bash -pql ticket new bug "{title}" --team {team} --description "{description}" +pql ticket new bug "{title}" --team {team} --description "{description}" --parent {T-1037 or the active phase epic} ``` The description should include: - Bug summary (from tester notes) - Tick, room, position - Key observations from snapshot analysis -- Seed for reproduction +- Seed for reproduction, or "seed unavailable — replay not possible" if + `seed.txt` held the sentinel - Report directory name for reference -After creating the ticket, report the ticket ID to the user. +After creating the ticket, **verify it** with `pql ticket show ` (per +`.claude/rules/ticket-cli.md`'s verify-after-create step) to confirm the +title and parenting landed clean, then report the ticket ID to the user. + +A report is "processed" once it has a resulting ticket (or was dismissed) — +after reporting the ticket ID, offer to either delete the report directory +(same confirmation rule as Dismiss below) or move it to +`{bug-reports-dir}/archive/{report-dir}/`, for cases where the tester or a +reviewer might want to re-open it later. **Always confirm with the user +first**, same as Dismiss. This is what makes "Remaining unprocessed" in the +final summary computable — without it, a ticketed report is indistinguishable +from a fresh one on the next run. #### Dismiss @@ -226,6 +267,9 @@ After all reports have been processed, present a summary: positions in the render should match positions in the snapshot. Mismatches are themselves a bug signal (rendering vs simulation desync). - The seed in `seed.txt` combined with `inputs.jsonl` enables deterministic - replay on the server. Note this in any ticket you create. + replay on the server — when a real seed is present. Today `seed.txt` holds + the literal string `unavailable` in every report (the server doesn't yet + emit `rng_seed` on `ObserverSnapshot`); note whichever is true in any + ticket you create, don't assume replay is possible. - If the bug-reports directory does not exist, the tester has not yet run any gauntlet sessions or has not filed any reports. This is not an error. diff --git a/.claude/skills/glb-gen/SKILL.md b/.claude/skills/glb-gen/SKILL.md index d971c6a45..cc41c47e2 100644 --- a/.claude/skills/glb-gen/SKILL.md +++ b/.claude/skills/glb-gen/SKILL.md @@ -18,7 +18,7 @@ Convert approved concept images to game-ready .glb models. | Service | Check | |---------|-------| | Trellis | `tooling/db/trellis_connector.py health` | -| Blender | `flatpak run org.blender.Blender --version` | +| Blender | `tooling/blender --version` | Trellis runs on tower-of-joy and may be switched off. Check before batching. @@ -41,54 +41,35 @@ python3 tooling/db/trellis_connector.py generate input.png \ ## Batch Usage -For multiple models, use the batch script: +`tooling/trellis-batch.sh` exists but is **hardcoded to one category**: it +takes no arguments and always processes the 16 character body types from +`.tmp/image-gen/characters/bodies` into `.tmp/glb-gen/characters/bodies`. +Running it for any other asset category (furniture, props, etc.) does +nothing useful — it will just re-run (or skip, if outputs already exist) the +same 16 character bodies regardless of what you intended. + +For any other category, loop `trellis_connector.py` calls yourself with a +cooldown between jobs: ```bash -bash tooling/trellis-batch.sh +for f in .tmp/image-gen/furniture/tables/*.png; do + name=$(basename "$f" .png) + python3 tooling/db/trellis_connector.py generate "$f" \ + --output ".tmp/glb-gen/furniture/tables/${name}.glb" \ + --simplify 0.95 --texture-size 1024 + sleep 15 +done ``` -The batch script runs one model at a time with 15s cooldown between jobs, -retries up to 3 times on failure, and skips already-generated GLBs. - **Never run Trellis jobs in parallel** — it uses the full GPU and concurrent jobs will OOM and corrupt the CUDA state. ## Trellis API Reference -The connector talks to a Gradio API. The parameter layout is fragile — document -changes here when the container is updated. - -### /image_to_3d — 9 inputs - -| Pos | Name | Type | Value | -|-----|------|------|-------| -| 0 | image | Image | preprocessed image from /preprocess_image_1 | -| 1 | multiimages | Gallery | `[]` for single-image mode | -| 2 | is_multiimage | State | `False` for single-image (**not a session — it's a boolean flag**) | -| 3 | seed | Slider | int, 0-2147483647 | -| 4 | ss_guidance | Slider | float, default 7.5 | -| 5 | ss_steps | Slider | int, default 12 | -| 6 | slat_guidance | Slider | float, default 3.0 | -| 7 | slat_steps | Slider | int, default 12 | -| 8 | multiimage_algo | Radio | `"stochastic"` or `"multidiffusion"` | - -### /extract_glb — 3 inputs - -| Pos | Name | Type | Value | -|-----|------|------|-------| -| 0 | output_buf | State | `None` — server uses internal state from image_to_3d | -| 1 | simplify | Slider | float, mesh simplification ratio (default 0.95) | -| 2 | texture_size | Slider | int, texture resolution (default 1024) | - -### Common Failure Modes - -| Error | Cause | Fix | -|-------|-------|-----| -| "needed 9, got 8" on image_to_3d | Missing `is_multiimage` at position 2 | Pass `False` | -| "needed 3, got 2" on extract_glb | Missing `output_buf` at position 0 | Pass `None` | -| `'float' cannot be interpreted as int` | numpy rejects float for linspace steps | Patch `flow_euler.py` on server: `int(steps)` | -| CUDA device mismatch / invalid argument | GPU state corrupted after crash | Restart the Trellis container | -| Repeated 500 after restart | Previous crash left tensors on wrong device | Full container restart (not just API restart) | +The connector talks to a fragile Gradio API (positional args, no field +names). Full parameter tables and common failure modes are in +`references/trellis-api.md` — read it when a Trellis call fails with a +position/count mismatch or a CUDA/device error. ## Intermediate and Output Directories @@ -112,7 +93,13 @@ Always mirror the category/subcategory path across stages so you can trace `image-gen/furniture/tables/foo_concept.png` → `glb-gen/furniture/tables/foo.glb`. Final game-ready assets are copied to `spikes/3dpipeline/models/` for spike -testing, or to `client/assets/models/` when ready for production. +testing. For production, character GLBs go to `client/assets/characters/` +(already in active use — see its `bodies/`, `clothing/`, `hair/`, +`skeleton/`, etc. subdirectories). `client/assets/models/` does not exist — +there is no established production path yet for non-character categories +(furniture, props); that's a Phase-5 concern (Player control & in-world +rendering, per CLAUDE.md's Development Cascade). Until then, keep +non-character output staged under `.tmp/glb-gen/postproc/`. ## Post-process in Blender @@ -125,7 +112,7 @@ Normalize scale, center, generate recolor mask, adjust materials: Or directly: ```bash -flatpak run org.blender.Blender --background \ +tooling/blender --background \ --python .claude/skills/glb-gen/scripts/postprocess_glb.py \ -- input.glb output.glb [--target-width N] [--color-threshold N] ``` @@ -139,7 +126,10 @@ For best Trellis results: - No text, labels, or watermarks - 3/4 front view at eye level gives Trellis the most information - **Do NOT force isometric angle** — Trellis reconstructs full 3D, the game camera handles the view -- Use a style anchor image (`--input`) to maintain consistency across batches +- To maintain consistency across a batch, generate the concept images with + `/image-gen` using its `--input` style anchor flag — `--input` is an + /image-gen flag, not a Trellis one; `trellis_connector.py` has no `--input` + argument and exits on unrecognized flags. These match `/image-gen` output with the Settled Reach style guide. @@ -159,6 +149,16 @@ Set in `project.godot` under `[gltf]` so new imports pick it up automatically. ## Material Convention -Post-processed .glb files use standard material slot names: -`mat_wood_primary`, `mat_metal_primary`, `mat_fabric_primary`, etc. -See `docs/design/character-visuals-spec.md` §5 for character materials. +`scripts/postprocess_glb.py` renames **every** material slot on the imported +mesh to a single slot, `mat_primary` by default (override with +`--material-name NAME`) — it does not produce per-material names like +`mat_wood_primary`/`mat_metal_primary`/`mat_fabric_primary`; those names are +not used anywhere in this pipeline. The script also keeps the original +Trellis-baked texture and sets roughness=1.0/specular=0.0 for toon +compatibility (see `setup_materials()`). + +Note `docs/design/character-visuals-spec.md` §5 ("Color Mesh Regions") is a +**different, unrelated system**: it defines 2D compositor color regions for +the character-visuals rendering pipeline (`skin_primary`, `hair_primary`, +`cloth_primary`/`cloth_secondary`/`cloth_accent`, `accessory_primary`, etc.), +not GLB material slot names — do not conflate the two when naming things. diff --git a/.claude/skills/glb-gen/references/trellis-api.md b/.claude/skills/glb-gen/references/trellis-api.md new file mode 100644 index 000000000..85830de50 --- /dev/null +++ b/.claude/skills/glb-gen/references/trellis-api.md @@ -0,0 +1,40 @@ +# Trellis API Reference + +The connector (`tooling/db/trellis_connector.py`) talks to a Gradio API. The +parameter layout is fragile — document changes here when the container is +updated. This is the canonical copy; the connector's module docstring carries +a duplicate for at-a-glance reference when reading the script directly — keep +both in sync if you change either, but treat this file as the source of truth +for the skill. + +## /image_to_3d — 9 inputs + +| Pos | Name | Type | Value | +|-----|------|------|-------| +| 0 | image | Image | preprocessed image from /preprocess_image_1 | +| 1 | multiimages | Gallery | `[]` for single-image mode | +| 2 | is_multiimage | State | `False` for single-image (**not a session — it's a boolean flag**) | +| 3 | seed | Slider | int, 0-2147483647 | +| 4 | ss_guidance | Slider | float, default 7.5 | +| 5 | ss_steps | Slider | int, default 12 | +| 6 | slat_guidance | Slider | float, default 3.0 | +| 7 | slat_steps | Slider | int, default 12 | +| 8 | multiimage_algo | Radio | `"stochastic"` or `"multidiffusion"` | + +## /extract_glb — 3 inputs + +| Pos | Name | Type | Value | +|-----|------|------|-------| +| 0 | output_buf | State | `None` — server uses internal state from image_to_3d | +| 1 | simplify | Slider | float, mesh simplification ratio (default 0.95) | +| 2 | texture_size | Slider | int, texture resolution (default 1024) | + +## Common Failure Modes + +| Error | Cause | Fix | +|-------|-------|-----| +| "needed 9, got 8" on image_to_3d | Missing `is_multiimage` at position 2 | Pass `False` | +| "needed 3, got 2" on extract_glb | Missing `output_buf` at position 0 | Pass `None` | +| `'float' cannot be interpreted as int` | numpy rejects float for linspace steps | Patch `flow_euler.py` on server: `int(steps)` | +| CUDA device mismatch / invalid argument | GPU state corrupted after crash | Restart the Trellis container | +| Repeated 500 after restart | Previous crash left tensors on wrong device | Full container restart (not just API restart) | diff --git a/.claude/skills/glb-gen/scripts/postprocess b/.claude/skills/glb-gen/scripts/postprocess index a6d48929a..235818d30 100755 --- a/.claude/skills/glb-gen/scripts/postprocess +++ b/.claude/skills/glb-gen/scripts/postprocess @@ -68,10 +68,28 @@ echo "Post-processing: $(basename "$INPUT_ABS")" echo " Input: $INPUT_REL" echo " Output: ${OUTPUT_ABS#$(cd "$PROJECT_ROOT" && pwd)/}" -flatpak run org.blender.Blender --background \ +LOG="${OUTPUT_ABS%.glb}_blender.log" + +# Blender does not reliably set a non-zero exit code on an uncaught Python +# exception (--python-exit-code is not set), so failures must be detected +# from the log content and from whether the output file actually appeared — +# not from Blender's own exit status alone. Capture the full log first +# (temporarily disabling -e so a non-zero Blender exit doesn't abort before +# we can inspect it), then filter for the console summary. +set +e +"$PROJECT_ROOT/tooling/blender" --background \ --python "$BLENDER_SCRIPT" \ - -- "$INPUT_ABS" "$OUTPUT_ABS" "$@" 2>&1 \ - | grep -E "^ |^=|WARNING|ERROR|Dominant|Mask" + -- "$INPUT_ABS" "$OUTPUT_ABS" "$@" > "$LOG" 2>&1 +BLENDER_EXIT=$? +set -e + +grep -iE "^ |^=|warning|error|traceback|dominant|mask" "$LOG" || true + +if [ "$BLENDER_EXIT" -ne 0 ] || [ ! -f "$OUTPUT_ABS" ]; then + echo "ERROR: Blender post-process failed (exit $BLENDER_EXIT, output present: $([ -f "$OUTPUT_ABS" ] && echo yes || echo no))" >&2 + echo " Full log: ${LOG#$(cd "$PROJECT_ROOT" && pwd)/}" >&2 + exit 1 +fi MASK="${OUTPUT_ABS%.glb}_mask.png" if [ -f "$MASK" ]; then diff --git a/.claude/skills/glb-gen/scripts/postprocess_glb.py b/.claude/skills/glb-gen/scripts/postprocess_glb.py index a238e930d..a0c6a5a7c 100644 --- a/.claude/skills/glb-gen/scripts/postprocess_glb.py +++ b/.claude/skills/glb-gen/scripts/postprocess_glb.py @@ -3,7 +3,7 @@ GLB post-processor for The Settled Reach asset pipeline. Run via Blender headless: - flatpak run org.blender.Blender --background --python postprocess_glb.py -- input.glb output.glb [options] + tooling/blender --background --python postprocess_glb.py -- input.glb output.glb [options] Operations: 1. Normalize scale to fit a target bounding box (default 1x1x1 world units) @@ -42,7 +42,7 @@ def get_script_args(): def parse_args(args): """Parse script arguments.""" if len(args) < 2: - print("Usage: postprocess_glb.py -- input.glb output.glb [--target-width N] [--color #hex] [--material-name name]") + print("Usage: postprocess_glb.py -- input.glb output.glb [--target-width N] [--target-height N] [--color-threshold N] [--material-name name]") sys.exit(1) result = { diff --git a/.claude/skills/image-gen/SKILL.md b/.claude/skills/image-gen/SKILL.md index 2f615dd80..5e61e6217 100644 --- a/.claude/skills/image-gen/SKILL.md +++ b/.claude/skills/image-gen/SKILL.md @@ -10,21 +10,27 @@ description: > # Image Generation — The Settled Reach -Generate concept images via the Gemini API. Standalone skill — does not chain -into other pipelines. Output is a PNG file. +Generate concept images via the Gemini API. Does not auto-invoke downstream +skills — /glb-gen consumes its output from `.tmp/image-gen/` when you choose +to feed a concept image into the 3D pipeline, but that's a manual next step, +not something this skill triggers itself. Output is a PNG file. ## Prerequisites Requires `GEMINI_API_KEY` in environment (set in `.claude/settings.local.json`). +Uses the canonical connector at `tooling/db/image_connector.py` (see +`.claude/rules/project-structure.md` — `tooling/db/` is the documented home +for asset/connector scripts; this skill has no local fork of it). + ```bash -python3 .claude/skills/image-gen/scripts/image_connector.py health +python3 tooling/db/image_connector.py health ``` ## Usage ```bash -python3 .claude/skills/image-gen/scripts/image_connector.py generate \ +python3 tooling/db/image_connector.py generate \ "prompt text" \ --output path/to/output.png \ --aspect 1:1 @@ -53,8 +59,8 @@ Always use `--output .tmp/image-gen/[category]/[subcategory]/[name]_concept.png` | Flag | Default | Description | |------|---------|-------------| | `--output` | auto-named in `~/Pictures/mcp-images/` | Output PNG path (use `.tmp/image-gen/...` instead) | -| `--aspect` | `1:1` | Aspect ratio (1:1, 16:9, 3:2, 4:3, etc.) | -| `--size` | standard | Resolution hint (1K, 2K, 4K) | +| `--aspect` | `1:1` | Real API parameter (`generationConfig.imageConfig.aspectRatio`). Valid: `1:1`, `3:2`, `2:3`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9` | +| `--size` | standard | Resolution **hint** only — appended to the prompt text, not a real API parameter; Gemini may not honor it | | `--input` | none | Input image for image-to-image variation | ## Art Style Guide — The Settled Reach @@ -81,7 +87,8 @@ For concept images intended as Trellis input (3D model generation): ## Batch Usage Generate multiple variants by running the script multiple times with different -prompts or seeds. Each call is independent — no state between calls. +prompts. Each call is independent — no state between calls, and there is no +`--seed` flag (the connector rejects unknown arguments). Show generated images to the user for review before proceeding to any downstream pipeline (e.g. /glb-gen). diff --git a/.claude/skills/image-gen/scripts/image_connector.py b/.claude/skills/image-gen/scripts/image_connector.py deleted file mode 100755 index 6c0884991..000000000 --- a/.claude/skills/image-gen/scripts/image_connector.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python3 -""" -Gemini image generator connector — direct API wrapper. - -Generates images via Google's Gemini 2.0 Flash image generation API. -API key from GEMINI_API_KEY env var or config.json. - -Usage: - python3 image_connector.py health - python3 image_connector.py generate "prompt" [--output file.png] [--aspect 1:1] [--size 1K] [--input image.png] -""" - -import base64 -import json -import os -import sys -import urllib.error -import urllib.request - -CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json") -DEFAULT_OUTPUT_DIR = os.path.expanduser("~/Pictures/mcp-images") - - -def get_api_key(): - """Get Gemini API key from env or config.""" - key = os.environ.get("GEMINI_API_KEY") - if key: - return key - try: - with open(CONFIG_PATH) as f: - config = json.load(f) - return config.get("gemini_api_key", "") - except Exception: - pass - print(json.dumps({ - "ok": False, - "error": "No GEMINI_API_KEY found in environment or config.json" - }, indent=2)) - sys.exit(1) - - -def health(): - """Check if the Gemini API is reachable with the configured key.""" - key = get_api_key() - url = f"https://generativelanguage.googleapis.com/v1beta/models?key={key}" - try: - req = urllib.request.Request(url, method="GET") - with urllib.request.urlopen(req, timeout=10) as resp: - data = json.loads(resp.read()) - models = [m.get("name", "") for m in data.get("models", []) - if "imagen" in m.get("name", "").lower() - or "flash" in m.get("name", "").lower()] - print(json.dumps({ - "ok": True, - "api": "gemini", - "image_capable_models": models[:5], - }, indent=2)) - except Exception as e: - print(json.dumps({ - "ok": False, - "error": str(e) - }, indent=2)) - sys.exit(1) - - -def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, - input_image=None): - """ - Generate an image from a text prompt using Gemini. - - Args: - prompt: Text description of the image to generate - output: Output file path (default: auto-named in ~/Pictures/mcp-images/) - aspect_ratio: Aspect ratio (1:1, 16:9, 3:2, etc.) - image_size: Resolution hint (1K, 2K, 4K) - may not be honored - input_image: Optional input image path for image-to-image generation - """ - key = get_api_key() - - # Gemini image generation model - model = "gemini-2.5-flash-image" - url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}" - - if output is None: - safe = "".join(c if c.isalnum() or c in "-_ " else "" for c in prompt[:40]) - safe = safe.strip().replace(" ", "_").lower() - os.makedirs(DEFAULT_OUTPUT_DIR, exist_ok=True) - output = os.path.join(DEFAULT_OUTPUT_DIR, f"{safe}.png") - - # Build the request - parts = [] - - # Add input image if provided (image-to-image) - if input_image: - if not os.path.isfile(input_image): - print(json.dumps({"ok": False, "error": f"Input image not found: {input_image}"}), indent=2) - sys.exit(1) - with open(input_image, "rb") as f: - image_data = base64.b64encode(f.read()).decode("utf-8") - # Detect mime type - ext = os.path.splitext(input_image)[1].lower() - mime = {"png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", - ".webp": "image/webp"}.get(ext, "image/png") - parts.append({ - "inlineData": { - "mimeType": mime, - "data": image_data - } - }) - - # Build enhanced prompt with aspect ratio and size hints - enhanced_prompt = prompt - if aspect_ratio and aspect_ratio != "1:1": - enhanced_prompt += f" Aspect ratio: {aspect_ratio}." - if image_size: - enhanced_prompt += f" Resolution: {image_size}." - - parts.append({"text": enhanced_prompt}) - - payload = json.dumps({ - "contents": [{"parts": parts}], - "generationConfig": { - "responseModalities": ["TEXT", "IMAGE"], - } - }) - - req = urllib.request.Request( - url, - data=payload.encode(), - headers={"Content-Type": "application/json"}, - method="POST" - ) - - print(f"Generating image...", file=sys.stderr) - print(f" Prompt: {prompt}", file=sys.stderr) - if input_image: - print(f" Input image: {input_image}", file=sys.stderr) - - try: - with urllib.request.urlopen(req, timeout=120) as resp: - result = json.loads(resp.read()) - except urllib.error.HTTPError as e: - body = e.read().decode("utf-8", errors="replace") - print(json.dumps({ - "ok": False, - "error": f"API error {e.code}: {e.reason}", - "details": body[:500] - }, indent=2)) - sys.exit(1) - except Exception as e: - print(json.dumps({"ok": False, "error": str(e)}), indent=2) - sys.exit(1) - - # Extract image data from response - candidates = result.get("candidates", []) - if not candidates: - print(json.dumps({ - "ok": False, - "error": "No candidates in response", - "response": json.dumps(result)[:500] - }, indent=2)) - sys.exit(1) - - image_saved = False - text_response = "" - - for candidate in candidates: - content = candidate.get("content", {}) - for part in content.get("parts", []): - if "inlineData" in part: - # Image data - image_b64 = part["inlineData"]["data"] - image_bytes = base64.b64decode(image_b64) - os.makedirs(os.path.dirname(os.path.abspath(output)), exist_ok=True) - with open(output, "wb") as f: - f.write(image_bytes) - image_saved = True - elif "text" in part: - text_response += part["text"] - - if not image_saved: - print(json.dumps({ - "ok": False, - "error": "No image data in response", - "text_response": text_response[:500], - "response": json.dumps(result)[:500] - }, indent=2)) - sys.exit(1) - - file_size = os.path.getsize(output) - print(json.dumps({ - "ok": True, - "file": output, - "size_bytes": file_size, - "prompt": prompt, - "aspect_ratio": aspect_ratio, - }, indent=2)) - - -def main(): - if len(sys.argv) < 2: - print("Usage:") - print(" image_connector.py health") - print(" image_connector.py generate 'prompt' [--output file.png] [--aspect 1:1] [--size 1K] [--input image.png]") - sys.exit(1) - - cmd = sys.argv[1] - - if cmd == "health": - health() - elif cmd == "generate": - if len(sys.argv) < 3: - print("Error: prompt required", file=sys.stderr) - sys.exit(1) - - prompt = sys.argv[2] - output = None - aspect_ratio = "1:1" - image_size = None - input_image = None - - i = 3 - while i < len(sys.argv): - if sys.argv[i] == "--output" and i + 1 < len(sys.argv): - output = sys.argv[i + 1] - i += 2 - elif sys.argv[i] == "--aspect" and i + 1 < len(sys.argv): - aspect_ratio = sys.argv[i + 1] - i += 2 - elif sys.argv[i] == "--size" and i + 1 < len(sys.argv): - image_size = sys.argv[i + 1] - i += 2 - elif sys.argv[i] == "--input" and i + 1 < len(sys.argv): - input_image = sys.argv[i + 1] - i += 2 - else: - print(f"Unknown argument: {sys.argv[i]}", file=sys.stderr) - sys.exit(1) - - generate(prompt, output=output, aspect_ratio=aspect_ratio, - image_size=image_size, input_image=input_image) - else: - print(f"Unknown command: {cmd}", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/.claude/skills/sprite-gen/SKILL.md b/.claude/skills/sprite-gen/SKILL.md index 63e5406f1..81653dae1 100644 --- a/.claude/skills/sprite-gen/SKILL.md +++ b/.claude/skills/sprite-gen/SKILL.md @@ -23,6 +23,12 @@ artwork via the Godot render pipeline at `renderer/`. models to 4-direction sprites for a 2D view — that role is **retired** (D-244). It now serves flat 2D artwork only. +## Authoring a New Scene + +To author a new scene, read `renderer/README.md` — "Adding a New Model" + +Material Guidelines (`MeshInstance3D` root centered at origin, flat +`StandardMaterial3D`, no emission/specularity). + ## Render Pipeline ```bash @@ -32,16 +38,22 @@ serves flat 2D artwork only. Scenes live at `renderer/models/.tscn`. The script still emits the legacy spike layout — 12 PNGs in `renderer/output/` (4 cardinal directions × 3 resolutions: 1024/256/64). For **flat artwork** the relevant output is the single front (south) -view; the multi-direction output is legacy object-sprite behaviour. A flat-artwork -(single-view) mode for the render script is the natural follow-up. +view; the multi-direction output is legacy object-sprite behaviour. ### After rendering 1. Inspect the output PNG — does it read as the intended artwork at in-world scale? -2. Promote the final PNG to `client/assets/` as the surface texture/decal. +2. Promote to `client/assets/textures/` or `client/assets/artwork/` (create on + first use) — **never `client/assets/sprites/`**, that path is the retired + object-sprite catalog (D-244). Register the promoted asset in a manifest + per D-241's promotion boundary (precedent: `client/assets/characters/manifest.json`). ### Troubleshooting -- **No output files:** Godot needs a display. If running headless, prefix with `xvfb-run`. +- **No output files / "No DISPLAY set":** Godot needs a display; the script + now detects a missing `DISPLAY` itself and prints the remedy. `xvfb-run` is + **not installed by default** on the dev host — install it first (e.g. + `xorg-x11-server-Xvfb` on Fedora/RHEL, `xvfb` on Debian/Ubuntu), then + re-run under `xvfb-run -a`. - **Scene not found:** check the `.tscn` exists in `renderer/models/`. - **Godot not found:** pass the Godot path as the second arg: `render.sh /path/to/godot`. diff --git a/.claude/skills/sprite-gen/scripts/render.sh b/.claude/skills/sprite-gen/scripts/render.sh index f015c90ec..6f8c74421 100755 --- a/.claude/skills/sprite-gen/scripts/render.sh +++ b/.claude/skills/sprite-gen/scripts/render.sh @@ -24,24 +24,48 @@ echo "Rendering: $MODEL_NAME" echo "Godot: $GODOT" echo "Project: $PROJECT" +# The render step (unlike --import) needs a display. Fail fast with a clear +# remedy instead of letting Godot die on a cryptic low-level GL/X error. +if [ -z "${DISPLAY:-}" ]; then + if command -v xvfb-run >/dev/null 2>&1; then + echo "No DISPLAY set — re-run under xvfb-run, e.g.:" + echo " xvfb-run -a $0 $MODEL_NAME${2:+ $2}" + exit 1 + else + echo "ERROR: No DISPLAY set and xvfb-run is not installed." + echo " Install it (e.g. 'xorg-x11-server-Xvfb' on Fedora/RHEL, 'xvfb' on" + echo " Debian/Ubuntu), then re-run under xvfb-run -a." + exit 1 + fi +fi + # Ensure resources are imported (headless, no display needed) echo "Importing resources..." "$GODOT" --path "$PROJECT" --headless --import 2>&1 || echo "Warning: import step had errors (may be non-fatal)" +# Clear stale output from any previous run of this model first, so a failed +# render can't be masked by leftover files from an earlier successful run. +rm -f "$OUTPUT/${MODEL_NAME}"_*.png + # Run Godot with the render scene, passing model name as user arg "$GODOT" --path "$PROJECT" "$SCENE" -- "$MODEL_NAME" 2>&1 # Check output -EXPECTED_FILES=0 +FOUND_FILES=0 for dir in north east south west; do for res in 1024 256 64; do f="$OUTPUT/${MODEL_NAME}_${dir}_${res}.png" if [ -f "$f" ]; then - EXPECTED_FILES=$((EXPECTED_FILES + 1)) + FOUND_FILES=$((FOUND_FILES + 1)) fi done done echo "" -echo "Generated $EXPECTED_FILES/12 files in $OUTPUT/" +echo "Generated $FOUND_FILES/12 files in $OUTPUT/" ls -la "$OUTPUT/${MODEL_NAME}"_*.png 2>/dev/null || echo "No output files found." + +if [ "$FOUND_FILES" -eq 0 ]; then + echo "ERROR: Godot produced no output files — render failed." >&2 + exit 1 +fi diff --git a/.claude/skills/ticket/SKILL.md b/.claude/skills/ticket/SKILL.md index ff2998fb6..4b8a4e44c 100644 --- a/.claude/skills/ticket/SKILL.md +++ b/.claude/skills/ticket/SKILL.md @@ -1,9 +1,12 @@ --- name: ticket description: > - Manage project tickets in the pql planning store. Use when the user says - "ticket", "create a ticket", "show tickets", "board", "phase", or invokes - /ticket. Wraps the pql CLI for structured project management. + Manage project tickets in the pql planning store — day-to-day ticket CLI + (list, create, update, board, phase epics). Use when the user says "ticket", + "create a ticket", "show tickets", "board", "phase epic", "what's in the + active phase", or invokes /ticket. Wraps the pql CLI for structured project + management. For changelog/planning-store internals (branch moves, recovery, + write-through mechanics), use the pql skill instead. user-invocable: true allowed-tools: Bash, Read, Grep, Glob --- @@ -16,58 +19,22 @@ Manage tickets via **pql** (`.pql/pql.db`, rebuilt from the git-tracked ## Commands -### List / browse -```bash -pql ticket list [--status S] [--team T] [--label L] [--assigned A] [--decision D-NNN] \ - [--unblocked] [--leaf] [--under T-N] -pql ticket board [--team T] # kanban view, grouped by status -pql ticket show T-440 [--with-context] [--with-children] [--with-blockers] [--tree] -pql ticket show T-1,T-2,T-3 # batch -``` -(pql ticket `list` has no `--type`/`--priority` filter — list and read the JSON, or use -`--under ` for an epic's subtree.) +List/show/create/update/dependencies/labels/refinement syntax is **not +repeated here** — see the full command reference linked above +(`.claude/rules/ticket-cli.md`), the one canonical copy. -### Create -```bash -pql ticket new [--parent T-N] [--priority P] [--decision D-NNN] [--team T] [--description TEXT] [--assign A] -``` -Types: `initiative`, `epic`, `story`, `task`, `bug`. Priorities: `critical`, `high`, `medium`, `low`. - -### Update -```bash -pql ticket status T-440 in_progress # backlog|ready|in_progress|review|done|cancelled (batch with commas) -pql ticket assign T-440 dudley -pql ticket team T-440 server # comma teams allowed: server,client -pql ticket setparent T-9 T-2 -pql ticket append T-440 "extra context" # --file PATH / --stdin -pql ticket label T-440 add|rm <label> -``` - -### Dependencies -```bash -pql ticket show T-440 --with-blockers # what blocks this -pql ticket block T-7 --by T-440 # T-440 blocks T-7 -pql ticket unblock T-7 --from T-440 -``` +**Negative knowledge (not in the rule file):** `pql ticket list` has **no** +`--type`/`--priority` filter — list and read the JSON, or use `--under +<epic>` for an epic's subtree. ### Phases (the cascade hierarchy, not labels or a milestone entity) ```bash -pql ticket list --under T-750 --unblocked # ready work in the active phase (Phase 4) +pql ticket list --under T-750 --unblocked # ready work in the active phase (currently T-750, Phase 4) pql ticket setparent T-990 T-750 # put a ticket in the phase (re-parent) pql ticket show T-745 --tree # the cascade: initiative -> 6 phase epics ``` The active phase is the `in_progress` phase epic under T-745 (sequential per D-166). -### Refinement & planning -```bash -pql ticket refine list # tickets with no description -pql ticket refine next [--skip N] -pql ticket refine write T-5 '{"description":"..."}' -pql plan whatsnext # next ticket to work on -pql plan review # next ticket awaiting review -pql plan status # decision + ticket dashboard / counts -``` - ## Workflow 1. Decisions from `governance/` records become **initiatives** @@ -77,3 +44,6 @@ pql plan status # decision + ticket dashboard / counts 5. **Phase gating** is the cascade hierarchy (initiative T-745 → six phase epics), not labels/milestones 6. `/whats-next` selects the next batch via `pql ticket list --under <active-phase-epic> --unblocked` 7. Mutations flow to the git-tracked `.pql/changelog/`; the pre-commit hook exports + stages it +8. `.pql/changelog/` is write-through data — never `git restore` or hand-edit + it; for non-routine planning-store ops (branch moves, recovery) load + `Skill(pql)` first diff --git a/tooling/db/image_connector.py b/tooling/db/image_connector.py index e6a8d7a81..83a58b41a 100755 --- a/tooling/db/image_connector.py +++ b/tooling/db/image_connector.py @@ -2,7 +2,7 @@ """ Gemini image generator connector — direct API wrapper. -Generates images via Google's Gemini 2.0 Flash image generation API. +Generates images via Google's gemini-2.5-flash-image generation API. API key from GEMINI_API_KEY env var or config.json. Usage: @@ -87,13 +87,13 @@ def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, # Add input image if provided (image-to-image) if input_image: if not os.path.isfile(input_image): - print(json.dumps({"ok": False, "error": f"Input image not found: {input_image}"}), indent=2) + print(json.dumps({"ok": False, "error": f"Input image not found: {input_image}"}, indent=2)) sys.exit(1) with open(input_image, "rb") as f: image_data = base64.b64encode(f.read()).decode("utf-8") # Detect mime type ext = os.path.splitext(input_image)[1].lower() - mime = {"png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", + mime = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp"}.get(ext, "image/png") parts.append({ "inlineData": { @@ -102,20 +102,25 @@ def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, } }) - # Build enhanced prompt with aspect ratio and size hints + # Build enhanced prompt with a size hint. Unlike aspect ratio below, Gemini + # has no dedicated resolution parameter for this model — this is a + # best-effort prompt hint only and may not be honored. enhanced_prompt = prompt - if aspect_ratio and aspect_ratio != "1:1": - enhanced_prompt += f" Aspect ratio: {aspect_ratio}." if image_size: enhanced_prompt += f" Resolution: {image_size}." parts.append({"text": enhanced_prompt}) + generation_config = {"responseModalities": ["TEXT", "IMAGE"]} + if aspect_ratio: + # Real API parameter (not a prompt hint). Valid values: 1:1, 3:2, + # 2:3, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9. + # https://ai.google.dev/gemini-api/docs/image-generation + generation_config["imageConfig"] = {"aspectRatio": aspect_ratio} + payload = json.dumps({ "contents": [{"parts": parts}], - "generationConfig": { - "responseModalities": ["TEXT", "IMAGE"], - } + "generationConfig": generation_config }) req = urllib.request.Request( @@ -142,7 +147,7 @@ def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, }, indent=2)) sys.exit(1) except Exception as e: - print(json.dumps({"ok": False, "error": str(e)}), indent=2) + print(json.dumps({"ok": False, "error": str(e)}, indent=2)) sys.exit(1) # Extract image data from response