chore(skills): asset/tooling skills sweep — atlas/glb-gen/image-gen/audio-gen/sprite-gen/bug-report/ticket (T-1103)
Live-command corrections (atlas corridor-status, real body-ID naming), failure-proofed glb-gen/sprite-gen render scripts, Trellis API reference extracted. image-gen: fixed the output-path bug and de-forked the local image_connector.py to the canonical tooling/db/ copy. ticket skill consolidated to point at ticket-cli.md (setparent-none fix applied there too). Part of T-1099. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/<manifest>.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-<NNN>.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/<manifest>.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/<manifest>.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/<manifest>.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/<manifest>.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.
|
||||
|
||||
@@ -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_*
|
||||
|
||||
|
||||
@@ -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 <id>` (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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) |
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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()
|
||||
@@ -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/<name>.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 <name> /path/to/godot`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <epic>` 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 <type> <title> [--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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user