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:
2026-07-13 12:16:49 +02:00
co-authored by Claude Fable 5
parent 32021bd550
commit 33c14531da
17 changed files with 386 additions and 442 deletions
+1 -1
View File
@@ -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
+56 -32
View File
@@ -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-S1first 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}-beltasteroid 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.
+25 -8
View File
@@ -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_*
+61 -17
View File
@@ -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.
+46 -46
View File
@@ -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) |
+21 -3
View File
@@ -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 = {
+14 -7
View File
@@ -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()
+16 -4
View File
@@ -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`.
+27 -3
View File
@@ -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
+16 -46
View File
@@ -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
+15 -10
View File
@@ -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