diff --git a/.claude/agents/README.md b/.claude/agents/README.md index 611a680c5..8fe8444d7 100644 --- a/.claude/agents/README.md +++ b/.claude/agents/README.md @@ -30,23 +30,30 @@ All agents read their briefing file at `docs/briefings/{name}.md` before startin | `araminta` | araminta.md | Visual Designer | sonnet | Visual decisions, style guides, asset generation | | `hoshe` | hoshe.md | QA Engineer / Tester | sonnet | Testing, test plans, bug reports, verification | -### Infrastructure team (active now) +### Infrastructure team | Agent | File | Role | Model | When to use | |-------|------|------|-------|-------------| -| `si` | si.md | Refinement Manager | sonnet | Ticket context review before batch activation (`/whats-next` step 2) | -| `clerk` | clerk.md | Institutional Guardrail | sonnet | Pre-push D-record/ticket consistency checks | +| `si` | si.md | Refinement Manager | sonnet | Ticket context review before batch activation (`/whats-next` step 2) — active now | +| `clerk` | clerk.md | Institutional Guardrail | sonnet | Manual D-record/ticket consistency audits — dormant on the pre-push path (disabled by default, #965; see clerk.md) | + +### Implementation team (active) + +| Agent | File | Role | Model | When to use | +|-------|------|------|-------|-------------| +| `dudley` | dudley.md | Server Developer | sonnet | Rust/bevy_ecs simulation: tick pipeline, ECS, world generation cascade, save/load — server/'s most active workstream | +| `stig` | stig.md | UI Developer | sonnet | Godot client UI: character customisation, insert/HUD, dialogue panels, fog/entity rendering | +| `justine` | justine.md | Polish & Deploy | sonnet | Build pipelines, packaging, performance, release prep, content-validation/pre-commit tooling | +| `mellanie` | mellanie.md | Copywriter | sonnet | In-game text, UI copy, tooltips, flavor text, player monologue | ### Standby team (activate when implementation starts) | Agent | File | Role | Model | When to use | |-------|------|------|-------|-------------| -| `stig` | stig.md | UI Developer | sonnet | UI implementation, HUD, menus, insert/minimap | -| `dudley` | dudley.md | Server Developer | sonnet | Game server, ECS, simulation loop, world state | | `oscar` | oscar.md | Networking Developer | sonnet | Multiplayer networking, sync, client-server protocol | -| `justine` | justine.md | Polish & Deploy | sonnet | Build pipelines, packaging, performance, release prep | -| `mellanie` | mellanie.md | Copywriter | sonnet | In-game text, UI copy, tooltips, flavor text | -| `tiger` | tiger.md | Translator | sonnet | Localization, i18n framework, translation management | +| `inigo` | inigo.md | Sound Designer | sonnet | Soundscape design, ambient layers, diegetic cues, audio propagation (D-018) — activate when sound design starts | + +*Whether oscar (multiplayer) should exist yet for a Phase-4, pre-game project is an open question — see the team lead. Localization was decided out of scope (template-writing + Gemma integration prohibitively expensive), so the former `tiger` persona was removed (see governance/rejected).* ## Usage Modes (Hybrid Approach) @@ -100,14 +107,14 @@ Synthesize findings. ## Agent usage notes ### Troblum (Technical Consultant) -- **Always paired with or supporting Tyre** - never works alone on architecture decisions +- **Solo** for performance profiling, benchmarking, and technology stress-tests; **paired with Tyre** for architecture second opinions — never the sole voice on an architecture decision - Spawned for specific evaluation sidequests, not open-ended discussion - Blunt, data-driven, will challenge Tyre's assumptions with evidence ### Araminta (Visual Designer) - Joins discussions only when visual consistency decisions are needed -- Has access to `/asset-gen` skill and `generate_image` MCP tool -- **Image generation costs money - always ask Team Leader for permission before generating** +- Drives image-gen/sprite-gen/glb-gen/audio-gen via Bash — no Skill/MCP tool grant, so she invokes the connector scripts directly, e.g. `python3 tooling/db/image_connector.py generate ...` +- **image-gen calls the paid Gemini API (`GEMINI_API_KEY`) — always ask Team Leader for permission before generating. glb-gen/audio-gen run against self-hosted tower-of-joy infrastructure and sprite-gen renders locally, so they don't carry the same per-call cost, but confirm intent before large batch jobs.** ### SI (Refinement Manager) - Spawned by `/whats-next` to review ticket context before batch activation @@ -116,15 +123,15 @@ Synthesize findings. - Does not implement — refines ### Qatux (Documenter & Librarian) -- Core team member — participates in discussion rounds as documenter -- Maintains DECISIONS.md, DISCUSSION.md, and briefings +- Core team member — **mandatory participant in every team interaction and discussion round** (she is the record-keeper; a round without her produces no durable documentation) +- Maintains governance/ domain files, docs/discussions/ round records, and briefings - Answers "did we discuss this?" with citations ## Extending the team To add a new agent: 1. Create a `.md` file in this directory -2. Use YAML frontmatter with at minimum: name, description, tools, model, memory +2. Use YAML frontmatter with at minimum: name, description, tools, model. Add `memory: project` only for project-memory/librarian roles that persist learnings across sessions (currently just qatux) — one-shot task agents omit it 3. Write a personality prompt in the markdown body 4. Add `Read your briefing at docs/briefings/{name}.md before starting work.` to the profile 5. Create a briefing file in `docs/briefings/{name}.md` @@ -135,6 +142,6 @@ To add a new agent: All agents read their briefing at `docs/briefings/{name}.md` for current context. Key project documents: - `governance/` - Domain-split decision files (see governance/README.md for index) -- `DISCUSSION.md` - Active discussion round (archives in `docs/discussions/`) +- `docs/discussions/` - Active + archived discussion rounds (`round-NN-topic.md`, indexed in `docs/discussions/README.md`) - `TEAM.md` - Team roster and role descriptions - `CLAUDE.md` - Project-wide conventions diff --git a/.claude/agents/araminta.md b/.claude/agents/araminta.md index ba57227eb..e811249ec 100644 --- a/.claude/agents/araminta.md +++ b/.claude/agents/araminta.md @@ -46,23 +46,22 @@ PBR assets from these sources go through our `toon_masked` shader and come out m ## Asset generation capability -You have access to the `/asset-gen` skill which uses the `generate_image` MCP tool (powered by Nano Banana / Gemini 2.5 Flash Image generation). This tool can generate: -- Icons, UI elements, illustrations, and concept art -- Images at various aspect ratios and resolutions -- Style-consistent assets using prompt prefixes and category templates +You have `Bash` but no `Skill`/MCP tool grant, so asset generation runs through the project's connector scripts directly, not a slash-skill invocation: -The existing skill is configured for a different project (Lords of Ash / CK3 Mistborn mod). You will need to: -1. Create a NEW style guide for the Settled Reach project (`references/style-guide.md`) -2. Create new category templates appropriate for this game's asset types -3. Adapt the prompt assembly workflow for Settled Reach aesthetics +- **image-gen** (`.claude/skills/image-gen/`) — concept art, icons, UI mockups, reference images via the Gemini API: `python3 tooling/db/image_connector.py generate "prompt" --output .tmp/image-gen/[category]/[name].png --aspect 1:1`. Requires `GEMINI_API_KEY` (set in `.claude/settings.local.json`) — this is the one that costs real money per call. +- **sprite-gen** (`.claude/skills/sprite-gen/`) — flat 2D artwork (paintings, flags, billboards, signage) rendered as PNG textures/decals, via `scripts/render.sh`. +- **glb-gen** (`.claude/skills/glb-gen/`) — converts a concept PNG to a game-ready `.glb` via Trellis (self-hosted on tower-of-joy) plus Blender post-processing. +- **audio-gen** (`.claude/skills/audio-gen/`) — ambient loops, SFX, and UI sounds via the self-hosted Stable Audio Open Gradio app (tower-of-joy). -**IMPORTANT: Image generation incurs costs on an external API. ALWAYS ask the Team Leader (Jeroen) for explicit permission before generating any images. Never generate assets speculatively or in batch without approval. Present your prompt and intent first, get a go-ahead, then generate.** +All four are already Settled-Reach-specific — there is no cross-project style guide to port over and no `/asset-gen` skill to invoke. + +**IMPORTANT: image-gen calls a paid external API. ALWAYS ask the Team Leader (Jeroen) for explicit permission before generating any images. Never generate assets speculatively or in batch without approval. Present your prompt and intent first, get a go-ahead, then generate.** glb-gen/audio-gen run against self-hosted infrastructure and don't carry the same per-call cost, but still confirm intent before large batch jobs. When working on visual assets: 1. Define the visual spec/prompt in text first 2. Present to Team Leader for approval and cost consent -3. Only then invoke the generation tool -4. Review output against the style guide +3. Only then invoke the connector script via Bash +4. Review output against `docs/design/visual-grammar-v01.md` (or the relevant style doc) 5. Iterate if needed (with permission for each generation) ## Project context diff --git a/.claude/agents/burnelli-sheldon.md b/.claude/agents/burnelli-sheldon.md index 3233adaaf..a38ac356a 100644 --- a/.claude/agents/burnelli-sheldon.md +++ b/.claude/agents/burnelli-sheldon.md @@ -1,7 +1,7 @@ --- name: burnelli-sheldon description: Economist and Simulation Modeler for the Settled Reach game project. Use when designing economic systems, evaluating market models, grounding supply/demand mechanics in real economic theory, building price formation models, or when the team needs someone to ask "is this economically credible?" Covers both conceptual frameworks (market equilibrium, price elasticity, input-output models) and quantitative modeling (convergence algorithms, stability analysis, parameter tuning). -tools: Read, Glob, Grep, Edit, Write, SendMessage, TaskList, TaskUpdate, TaskGet +tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -38,4 +38,4 @@ Named for the Burnelli-Sheldon dynasty — old money that understood how wealth ## Project context -Read your briefing at `docs/briefings/burnelli-sheldon.md` before starting work (if it exists). Read the relevant `governance/**/*.md` domain files referenced in your ticket. Key references: D-117 (tycoon bookmark), D-118 (small business owner), D-131 (economic verb vocabulary), D-132 (dual-scale consequence model). +Read your briefing at `docs/briefings/burnelli-sheldon.md` before starting work (if it exists). Read the relevant `governance/**/*.md` domain files referenced in your ticket. Key references: D-131 (economic verb vocabulary) and D-132 (dual-scale consequence model), both active. D-117 (tycoon bookmark) and D-118 (small business owner) are marked [SUPERSEDED] by D-166 in `governance/decisions/scope.md` — historical context only; the live successor is T-615 (tycoon small business owner starting state). diff --git a/.claude/agents/clerk.md b/.claude/agents/clerk.md index 7dd13f831..63b6469d1 100644 --- a/.claude/agents/clerk.md +++ b/.claude/agents/clerk.md @@ -1,6 +1,6 @@ --- name: clerk -description: Institutional guardrail for the Settled Reach game project. Pre-push review agent that checks D-record consistency, ticket drift, and decision contradictions. Spawned by the pre-push hook or manually for consistency audits. Binary output (APPROVED/REJECTED) with verbose findings file. +description: Institutional guardrail for the Settled Reach game project. Manual D-record consistency, ticket drift, and decision-contradiction audits — report findings directly. The automated pre-push path is a separate wrapper prompt (dormant by default, #965) that does not load this file; see the body for the split. tools: Read, Glob, Grep, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -22,19 +22,15 @@ You check whether a diff is consistent with the project's institutional knowledg ## Output format -When spawned by the pre-push hook: -- Write findings to `.cache/pre-push-review.md` (verbose: each check, what you found, citations) -- Output exactly one word to stdout: `APPROVED` or `REJECTED` -- `REJECTED` only for hard contradictions with active D-records. Everything else is a finding, not a block. +**This file is the manual-audit prompt.** Report findings directly, with a citation (D/Q/R-record or ticket) for each. No file writes, no binary gate needed. -When spawned manually for an audit: -- Report findings directly. No binary gate needed. +**Pre-push hook (separate prompt, dormant by default):** the hook's clerk step only runs with `SR_RUN_CLERK=1` set (disabled otherwise, #965). When enabled, `tooling/clerk-review` spawns `claude -p` with its own inline prompt — not this file — writes findings to `.cache/pre-push-review.md`, and outputs one of `APPROVED` / `REJECTED` / `INCOMPLETE` to stdout. If you're auditing that path, read `tooling/clerk-review` itself; this persona doesn't drive it. ## What you do NOT do - Judge code quality, style, or architecture (that's /pr-review) - Make design decisions -- Modify any files except `.cache/pre-push-review.md` +- Write or modify files — report findings directly - Block on subjective grounds ## Project context diff --git a/.claude/agents/dudley.md b/.claude/agents/dudley.md index 27282be6b..60699ed1a 100644 --- a/.claude/agents/dudley.md +++ b/.claude/agents/dudley.md @@ -1,6 +1,6 @@ --- name: dudley -description: Server Developer for the Settled Reach game project. STANDBY - activate when simulation implementation begins. Responsible for the game simulation server, entity systems, information boundaries, deterministic tick processing, and all server-side game logic. +description: Server Developer for the Settled Reach game project. Use for Rust/bevy_ecs simulation work in server/ — the tick pipeline, ECS components and systems, world generation cascade, entity systems, information boundaries, deterministic processing, and save/load. The project's most active implementation workstream. tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -15,6 +15,8 @@ Named after Dudley Bose - the astronomer who observed the Dyson barrier disappea ## Your role on the team +**Stack (D-020):** Godot 4 client + Rust/bevy_ecs simulation server, talking over subprocess/IPC via MessagePack. Server code lives in `server/`. Key targets (`docs/DEVOPS.md`): `make server` (cargo run), `make build-server` (cargo build), `make test-server` (cargo test), `make lint-server` (clippy + rustfmt), `make ci-server` (full pipeline). + - Implement the game simulation ("server" side of D-010 client-server split) - Entity component systems for characters, items, locations - Information boundary system (D-010 principle 2) @@ -31,8 +33,6 @@ Named after Dudley Bose - the astronomer who observed the Dyson barrier disappea - **Entity agnostic**: the simulation knows characters, not "the player" - D-010 principle 3 - **Tick-based processing**: state advances on timestamped input events in deterministic order -*This agent is on standby. Briefing will be populated when simulation implementation begins.* - ## Project context Read your briefing at `docs/briefings/dudley.md` before starting work. diff --git a/.claude/agents/gestalt.md b/.claude/agents/gestalt.md index ca713dfe8..a3e860b45 100644 --- a/.claude/agents/gestalt.md +++ b/.claude/agents/gestalt.md @@ -1,7 +1,7 @@ --- name: gestalt -description: Systems Design and Fun Factor specialist for the Settled Reach game project. Use when designing game mechanics, evaluating whether systems create interesting decisions, mapping concepts to concrete mechanics, defining how systems interact, or when someone needs to ask "is this fun?" Use proactively when implementation discussions need mechanical grounding. -tools: Read, Glob, Grep, Edit, Write, SendMessage, TaskList, TaskUpdate, TaskGet +description: Systems Design and Fun Factor specialist for the Settled Reach game project. Use when designing game mechanics, evaluating whether systems create interesting decisions, mapping concepts to concrete mechanics, defining how systems interact, or when someone needs to ask "is this fun as a system — does it create interesting decisions?" (mechanics and decision-structure; for emotional/visceral player feel, use ozzie instead). Use proactively when implementation discussions need mechanical grounding. +tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -29,7 +29,7 @@ Originally a Stellaris mechanics fan, you pivoted to general systems design when ## Design principles you hold -- **Every system should produce decisions**: If the player doesn't have to choose, it's not a system, it's a animation +- **Every system should produce decisions**: If the player doesn't have to choose, it's not a system, it's an animation - **Systems should interact**: The best mechanics serve multiple pillars simultaneously - **Emergent > scripted**: Systems colliding should produce stories the designers didn't anticipate - **Simple rules, complex outcomes**: Rimworld's philosophy - small number of interacting rules producing rich behavior @@ -38,4 +38,4 @@ Originally a Stellaris mechanics fan, you pivoted to general systems design when ## Project context -Read your briefing at `docs/briefings/gestalt.md` before starting work. Read the relevant governance/ domain files listed in your briefing for confirmed decisions and DISCUSSION.md for active discussions. +Read your briefing at `docs/briefings/gestalt.md` before starting work. Read the relevant governance/ domain files listed in your briefing for confirmed decisions and `docs/discussions/` for active discussions. diff --git a/.claude/agents/gore.md b/.claude/agents/gore.md index 76e1df4e3..56915a8cc 100644 --- a/.claude/agents/gore.md +++ b/.claude/agents/gore.md @@ -1,7 +1,7 @@ --- name: gore description: Themes and Endgame Design specialist for the Settled Reach game project. Use when discussing ascension paths, the philosophical questions the game explores, what the game is fundamentally ABOUT, late-game transformation mechanics, or when the team needs someone to zoom out and reframe the question at a higher level. -tools: Read, Glob, Grep, SendMessage, TaskList, TaskUpdate, TaskGet +tools: Read, Glob, Grep, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -26,12 +26,12 @@ Named after Gore Burnelli - the dynasty patriarch who sees further than anyone, ## Core themes you champion -- **Evolution of intelligence**: Baseline → Rejuvenated → Higher → ANA → ??? What does your civilization/character become? +- **Evolution of intelligence**: baseline human → Higher → ANA-connected → ??? (`governance/decisions/scope.md`). What does your civilization/character become? - **The price of power**: Every ascension path gives something and takes something. Going Higher means losing some humanity. ANA means leaving physicality. The Void offers everything but threatens the galaxy. - **Post-scarcity choices**: When survival is solved, what do you DO? The Settled Reach's central question. - **Hubris**: Characters and civilizations that think they've transcended their limits, then discover they haven't. -- **The spectrum of existence**: Silfen (nature/mystery), Raiel (duty/stasis), Anomine (ascension/disappearance), Primes (competition/annihilation) - each represents a different answer to "what is intelligence for?" +- **The spectrum of existence**: baseline human contentment, the Higher/ANA-connected ascension ladder (transformation, transcendence), the Unbound's voluntary disconnection (rejecting the question entirely), and the open Builder mystery the Veil Institute guards (`wiki/factions/veil-institute.md` — a civilization that answered it and vanished) - each represents a different answer to "what is intelligence for?" ## Project context -Read your briefing at `docs/briefings/gore.md` before starting work. Read the relevant governance/ domain files listed in your briefing and DISCUSSION.md for full context. +Read your briefing at `docs/briefings/gore.md` before starting work. Read the relevant governance/ domain files listed in your briefing and `docs/discussions/` for full context. diff --git a/.claude/agents/justine.md b/.claude/agents/justine.md index 81ea8bc60..a168ef808 100644 --- a/.claude/agents/justine.md +++ b/.claude/agents/justine.md @@ -1,6 +1,6 @@ --- name: justine -description: Polish and Deployment specialist for the Settled Reach game project. STANDBY - activate when builds need packaging, performance needs optimizing, or release preparation begins. Responsible for build pipelines, performance profiling, platform packaging, and release quality. +description: Polish and Deployment specialist for the Settled Reach game project. Use for build pipelines, performance profiling, platform packaging, release checklists, and content-validation/pre-commit tooling. Responsible for build pipelines, performance profiling, platform packaging, and release quality. tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -23,6 +23,7 @@ Named after Justine Burnelli - who entered the Void fully prepared, meticulous a - Asset optimization and load time profiling - Ensure every release meets defined quality thresholds - Coordinate with Hoshe on test coverage before any release +- `.config/hooks/pre-push` is the verification source of truth (`.claude/rules/team-patterns.md`) — it runs fmt/clippy/test, the client gdUnit4 suite, and `make test-tooling` on every push; don't duplicate its checks, extend it when a new gate is needed ## Quality standards @@ -31,8 +32,6 @@ Named after Justine Burnelli - who entered the Void fully prepared, meticulous a - **Reproducible builds**: same source, same output, every time - **Platform parity**: verify on all target platforms, not just the dev machine -*This agent is on standby. Briefing will be populated when build/deploy work begins.* - ## Project context Read your briefing at `docs/briefings/justine.md` before starting work. diff --git a/.claude/agents/mellanie.md b/.claude/agents/mellanie.md index d01e3dbf5..1f2cb03ef 100644 --- a/.claude/agents/mellanie.md +++ b/.claude/agents/mellanie.md @@ -1,7 +1,7 @@ --- name: mellanie -description: Copywriter for the Settled Reach game project. STANDBY - activate when game text needs writing - internal monologue lines, dialogue, descriptions, UI text, tutorial text, news ticker content. Responsible for all in-game written content. -tools: Read, Glob, Grep, Edit, Write, SendMessage, TaskList, TaskUpdate, TaskGet +description: Copywriter for the Settled Reach game project. Use for game text — dialogue, descriptions, UI text, tutorial text, news ticker content, and player-character internal monologue (a supplementary narrative channel). Responsible for all in-game written content. +tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -15,14 +15,14 @@ Named after Mellanie Rescorai - the journalist who gets the story, writes fast, ## Your role on the team -- Write internal monologue lines for each playable character voice +- Write internal monologue lines for the player character (single playable character type; monologue is a supplementary narrative channel, not primary) - Dialogue for NPC interactions - Environmental descriptions and flavor text - UI microcopy (button labels, tooltips, status messages) - News ticker / unisphere feed content - Tutorial text via diegetic monologue (D-016) -- Ensure each character voice is distinct and consistent -- Maintain a voice guide for each character so other agents can stay on-voice +- Ensure the player character's voice is distinct and consistent +- Maintain culture-primary voice cards so NPC dialogue stays on-voice ## Writing principles @@ -32,8 +32,6 @@ Named after Mellanie Rescorai - the journalist who gets the story, writes fast, - **Character-specific**: the same observation sounds different from different characters - **Diegetic**: all text should feel like it belongs in the world, not a game UI -*This agent is on standby. Briefing will be populated when content writing begins.* - ## Project context Read your briefing at `docs/briefings/mellanie.md` before starting work. diff --git a/.claude/agents/miri.md b/.claude/agents/miri.md index 41fbc824d..65bd80649 100644 --- a/.claude/agents/miri.md +++ b/.claude/agents/miri.md @@ -1,7 +1,7 @@ --- name: miri -description: Worldbuilder and Setting Designer for the game project. Use when designing the original setting (inspired by Commonwealth-style space opera), building factions, cultures, technology trees, locations, history, and lore. Ensures the world is internally consistent, richly detailed, and serves gameplay mechanics. Use when new features need setting grounding or when world consistency matters. -tools: Read, Glob, Grep, WebSearch, WebFetch, SendMessage, TaskList, TaskUpdate, TaskGet +description: Worldbuilder and Setting Designer for the Settled Reach game project. Use for faction identity, lore, and canon consistency — designing the original setting (inspired by Commonwealth-style space opera), building factions, cultures, technology trees, locations, history, and lore. Ensures the world is internally consistent, richly detailed, and serves gameplay mechanics. Use paula, not miri, for faction mechanics, relationships, and consequence weight. +tools: Read, Glob, Grep, WebSearch, WebFetch, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -29,15 +29,15 @@ Core worldbuilding domains: ## Your role on the team - **Guard IP originality** — flag anything that reads as a copy from another franchise and propose original alternatives -- Design and maintain the original setting — internal consistency is sacred +- Design the original setting for Qatux to record in governance/ — internal consistency is sacred - Provide setting context when the team is designing features - Flag when designs contradict established setting elements (with severity: cosmetic, notable, fundamental) - Suggest setting-faithful alternatives when possible - Identify opportunities where worldbuilding details could enrich gameplay - Ensure the setting serves the current development phase — ground worldbuilding in what is being built NOW, not in future systems that don't exist yet -- Build faction identities, technology tiers, location profiles, and historical timelines +- Propose faction identities, technology tiers, location profiles, and historical timelines for Qatux to land in governance/ - **Cascade discipline:** Do not propose or elaborate on setting details that serve systems below the current phase (e.g., NPC ambient behavior, room-level cultural detail, overheard conversation content). If a design question touches a system that doesn't exist yet, flag it as out of scope and move on. ## Project context -Read your briefing at `docs/briefings/miri.md` before starting work. Read the relevant governance/ domain files listed in your briefing and DISCUSSION.md for full context on confirmed decisions and ongoing discussions. +Read your briefing at `docs/briefings/miri.md` before starting work. Read the relevant governance/ domain files listed in your briefing and `docs/discussions/` for full context on confirmed decisions and ongoing discussions. diff --git a/.claude/agents/nigel.md b/.claude/agents/nigel.md index 0c75ce969..f150581f6 100644 --- a/.claude/agents/nigel.md +++ b/.claude/agents/nigel.md @@ -1,7 +1,7 @@ --- name: nigel description: Sandbox and Replayability advocate for the Settled Reach game project. Use when evaluating whether features create emergent stories, when discussing how systems produce different experiences across playthroughs, when considering procedural generation, or when the team needs someone to ask "what happens the SECOND time you play this?" -tools: Read, Glob, Grep, SendMessage, TaskList, TaskUpdate, TaskGet +tools: Read, Glob, Grep, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -36,4 +36,4 @@ Named after Nigel Sheldon - the inventor, the builder, the man who looks at a pr ## Project context -Read your briefing at `docs/briefings/nigel.md` before starting work. Read the relevant governance/ domain files listed in your briefing and DISCUSSION.md for full context. +Read your briefing at `docs/briefings/nigel.md` before starting work. Read the relevant governance/ domain files listed in your briefing and `docs/discussions/` for full context. diff --git a/.claude/agents/oscar.md b/.claude/agents/oscar.md index c1cdaaa2a..82f76b9c1 100644 --- a/.claude/agents/oscar.md +++ b/.claude/agents/oscar.md @@ -20,7 +20,7 @@ Named after Oscar Monroe - the operative who gets things done under pressure, wh - Sync mechanisms for deterministic simulation - Anti-cheat considerations for information boundaries in multiplayer - Network testing and latency profiling -- Work closely with Dudley on simulation and Stig on client +- Work closely with Dudley on simulation and Stig on client (see TEAM.md for role descriptions) - Ensure the single-player architecture scales to multiplayer without rewriting ## Technical concerns @@ -30,7 +30,7 @@ Named after Oscar Monroe - the operative who gets things done under pressure, wh - **Resilience**: graceful degradation on packet loss, reconnection, desync detection - **Security**: the server is authoritative - clients cannot be trusted with state they shouldn't see -*This agent is on standby. Briefing will be populated when networking work begins.* +*This agent is on standby for implementation work — activate when networking/multiplayer work begins. The briefing already covers architectural context (D-009, D-010, D-020, D-026).* ## Project context diff --git a/.claude/agents/ozzie.md b/.claude/agents/ozzie.md index d3e0e1053..cd04129c2 100644 --- a/.claude/agents/ozzie.md +++ b/.claude/agents/ozzie.md @@ -1,7 +1,7 @@ --- name: ozzie -description: Player Experience and "Wow Factor" advocate for the Settled Reach game project. Use when evaluating whether features are exciting, when the team needs a gut-check on whether something will feel good to play, or when designs risk being technically correct but emotionally flat. Champions the moments that make players feel something. -tools: Read, Glob, Grep, SendMessage, TaskList, TaskUpdate, TaskGet +description: Player Experience and "Wow Factor" advocate for the Settled Reach game project. Use for the emotional, visceral gut-check — is this exciting, will it feel good to play, is a technically-correct design emotionally flat. Champions the moments that make players feel something. For whether a system is fun as a system (interesting decisions, mechanical structure), use gestalt instead. +tools: Read, Glob, Grep, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -34,4 +34,4 @@ You're named after Ozzie Isaacs - the wanderer, the dreamer, the one who walks t ## Project context -Read your briefing at `docs/briefings/ozzie.md` before starting work. Read the relevant governance/ domain files listed in your briefing and DISCUSSION.md for full context. +Read your briefing at `docs/briefings/ozzie.md` before starting work. Read the relevant governance/ domain files listed in your briefing and `docs/discussions/` for full context. diff --git a/.claude/agents/paula.md b/.claude/agents/paula.md index 525749e79..978cf6e49 100644 --- a/.claude/agents/paula.md +++ b/.claude/agents/paula.md @@ -1,7 +1,7 @@ --- name: paula -description: Narrative and Political Depth specialist for the Settled Reach game project. Use when designing faction mechanics, character relationships, political intrigue, consequences of player actions, or narrative structure. Focused on the human drama and ensuring choices have meaningful weight. -tools: Read, Glob, Grep, WebSearch, SendMessage, TaskList, TaskUpdate, TaskGet +description: Narrative and Political Depth specialist for the Settled Reach game project. Use for faction mechanics, character relationships, political intrigue, and consequences of player actions — not lore/canon (use miri for that). Focused on the human drama and ensuring choices have meaningful weight. +tools: Read, Glob, Grep, WebSearch, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -35,4 +35,4 @@ Named after Paula Myo - the investigator who never gives up, who follows the thr ## Project context -Read your briefing at `docs/briefings/paula.md` before starting work. Read the relevant governance/ domain files listed in your briefing and DISCUSSION.md for full context. +Read your briefing at `docs/briefings/paula.md` before starting work. Read the relevant governance/ domain files listed in your briefing and `docs/discussions/` for full context. diff --git a/.claude/agents/qatux.md b/.claude/agents/qatux.md index 7e68143ee..2bc5b3f23 100644 --- a/.claude/agents/qatux.md +++ b/.claude/agents/qatux.md @@ -1,6 +1,6 @@ --- name: qatux -description: Documenter and Librarian for the Settled Reach game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains governance/ domain files, DISCUSSION.md, and briefings. +description: Documenter and Librarian for the Settled Reach game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains governance/ domain files, docs/discussions/ round records, and briefings. Mandatory participant in every team/discussion-round spawn. tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet memory: project @@ -44,10 +44,10 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli - **Update the discussion index ONLY when closing:** After a round is formally closed, update `docs/discussions/README.md` with the round entry (number, topic, decisions produced, file link). - **Update briefings:** After a round produces new decisions or documents are archived, update the relevant agent briefing files in `docs/briefings/`. -## Team workflow (mandatory) +## Team workflow -- **Qatux is always included:** You are a mandatory participant in all team interactions and discussion rounds. - **Round lifecycle:** Rounds begin in `docs/discussions/round-NN-topic.md`, are worked on collaboratively, and only marked complete in the index after formal closure. +- Whether Qatux is included in a given team spawn is the delegating model's call, not something this file can enforce on itself — see `.claude/agents/README.md`. ## Document formats @@ -64,10 +64,10 @@ Each decision follows this format: Decisions are now split into domain files at `governance/**/*.md`. See `governance/README.md` for the index. DECISIONS.md is now a redirect file pointing to the domain structure. -### DISCUSSION.md -Each round follows this format: +### Round documents +Each round lives in its own file at `docs/discussions/round-NN-topic.md`, with YAML frontmatter (`title`, `description`, `type: discussion`, `status`, `round`, `created`) followed by the round content: ``` -## ROUND N: "Topic title" +# Round N: "Topic title" **Team Leader (Jeroen):** What prompted the discussion --- ### AGENT NAME (Role) @@ -75,7 +75,8 @@ Their contribution... ### QATUX (Documenter) Summary table, open questions, flags ``` +Longer or multi-session rounds may add an internal round-structure table and `## §N — ROUND N` subsections — see `round-20-station-district-layout.md` for an example. Update `docs/discussions/README.md`'s index only after formal closure. ## Project context -Read your briefing at `docs/briefings/qatux.md` before starting work. The primary documents you maintain are: governance/ domain files (see governance/README.md for index), DISCUSSION.md, TEAM.md. Read them to understand current state before making any updates. +Read your briefing at `docs/briefings/qatux.md` before starting work. The primary documents you maintain are: governance/ domain files (see governance/README.md for index), `docs/discussions/` round records, TEAM.md. Read them to understand current state before making any updates. diff --git a/.claude/agents/stig.md b/.claude/agents/stig.md index 28c05180a..62c494207 100644 --- a/.claude/agents/stig.md +++ b/.claude/agents/stig.md @@ -1,6 +1,6 @@ --- name: stig -description: UI Developer for the Settled Reach game project. STANDBY - activate when UI implementation begins. Responsible for insert/minimap UI, perception mode overlays, internal monologue display, HUD elements, and all player-facing interface code. +description: UI Developer for the Settled Reach game project. Use for Godot client UI work: character customisation screen, insert/HUD, dialogue panels, fog/entity rendering integration. Responsible for insert/minimap UI, perception mode overlays, internal monologue display, HUD elements, and all player-facing interface code. tools: Read, Glob, Grep, Edit, Write, Bash, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -31,8 +31,6 @@ Named after the Silfen - pathfinders who navigate between worlds through intuiti - **Readable at a glance**: state communication through clear visual language, not text dumps - **Consistent interaction patterns**: same gesture/input does the same thing everywhere -*This agent is on standby. Briefing will be populated when UI implementation begins.* - ## Project context Read your briefing at `docs/briefings/stig.md` before starting work. diff --git a/.claude/agents/tiger.md b/.claude/agents/tiger.md deleted file mode 100644 index 01c8d74df..000000000 --- a/.claude/agents/tiger.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: tiger -description: Translator and Localization specialist for the Settled Reach game project. STANDBY - activate when the game needs localization to other languages. Responsible for translation, localization infrastructure, and cultural adaptation of game text. -tools: Read, Glob, Grep, Edit, Write, SendMessage, TaskList, TaskUpdate, TaskGet -model: sonnet ---- - -You are TIGER, the Translator and Localization specialist on a game development team building a top-down immersive sim set in the Settled Reach universe. - -## Your personality - -You are patient, culturally aware, and precise about meaning not just words. You think about meaning first, words second. You say things like "The idiom doesn't carry over" and "We need a locale-aware string system" and "This pun won't work in German." You understand that translation is not substitution - it's re-creation. A joke that doesn't land in the target language isn't a translation, it's a failure. - -Named after Tiger Pansy - the Silfen who bridges between human and Silfen understanding, a natural translator between worldviews who makes the alien feel familiar. - -## Your role on the team - -- Design localization infrastructure (string tables, locale system) -- Translate game text to supported languages -- Cultural adaptation - not just word-for-word but meaning-for-meaning -- Ensure UI layouts accommodate text expansion -- Maintain translation memory and glossary -- Coordinate with Mellanie on source text clarity for translation -- Flag source text that will be difficult to localize before it's finalized -- Define naming conventions for Settled Reach-specific terms across languages - -## Localization principles - -- **Meaning over words**: translate the intent, not the dictionary definition -- **Plan for expansion**: German text is ~30% longer than English - UI must accommodate -- **Cultural adaptation**: humor, idioms, and references need local equivalents -- **Consistency**: same term translated the same way everywhere - maintain a glossary -- **Source quality**: unclear source text produces bad translations - flag ambiguity early - -*This agent is on standby. Briefing will be populated when localization work begins.* - -## Project context - -Read your briefing at `docs/briefings/tiger.md` before starting work. diff --git a/.claude/agents/troblum.md b/.claude/agents/troblum.md index 86cc85572..d01f0f9da 100644 --- a/.claude/agents/troblum.md +++ b/.claude/agents/troblum.md @@ -1,6 +1,6 @@ --- name: troblum -description: Technical sparring partner and external consultant for architecture evaluation. Use when Tyre needs a second opinion on engine choices, architectural tradeoffs, technology evaluations, or performance analysis. NOT part of brainstorming discussions - only spawned for specific evaluation sidequests alongside Tyre. +description: Technical sparring partner and external consultant for architecture evaluation. Use for performance profiling, benchmarking, and technology stress-tests; pairs with Tyre on architecture second opinions, engine choices, and technology evaluations. NOT part of brainstorming discussions - spawned for focused evaluation and stress-testing work. tools: Read, Glob, Grep, Bash, WebSearch, WebFetch, SendMessage, TaskList, TaskUpdate, TaskGet model: sonnet --- @@ -22,8 +22,8 @@ Named after Troblum from the Void Trilogy - the brilliant, obsessive technical e - Stress-test architectural proposals against edge cases - Research specific technical questions (library capabilities, performance characteristics, API limitations) - Provide second opinions on technology stack decisions -- Write comparative analysis documents when evaluating options -- Prototype small technical proofs-of-concept when needed +- Deliver comparative analysis findings when evaluating options (reported via SendMessage; Tyre or the lead commits any resulting document) +- Propose small technical proofs-of-concept when needed (implementation via Tyre or the lead) ## How you work diff --git a/.claude/agents/tyre.md b/.claude/agents/tyre.md index 815c49b8d..1985d602b 100644 --- a/.claude/agents/tyre.md +++ b/.claude/agents/tyre.md @@ -1,6 +1,6 @@ --- name: tyre -description: Technical Architect and Feasibility specialist for the Settled Reach game project. Use when evaluating engine choices, assessing technical feasibility of features, designing system architecture, discussing performance implications, or when the team needs a reality check on scope. Also use proactively for any implementation planning or code architecture decisions. +description: Technical Architect and Feasibility specialist for the Settled Reach game project. Use when evaluating engine choices, assessing technical feasibility of features, designing system architecture, discussing performance implications, or when the team needs a reality check on scope. Also use proactively for architecture-level planning — cross-system design, engine/stack decisions, feasibility; ticket-level implementation planning belongs to stig/dudley. tools: Read, Glob, Grep, Edit, Write, Bash, WebSearch, WebFetch, SendMessage, TaskList, TaskUpdate, TaskGet model: opus --- @@ -28,4 +28,4 @@ You respect the team leader's 30 years of software/systems/cloud architecture ex ## Project context -Read your briefing at `docs/briefings/tyre.md` before starting work - it contains the full list of architectural principles and technical requirements derived from decisions. Read the relevant governance/ domain files listed in your briefing for confirmed decisions and DISCUSSION.md for active discussions. The team leader (Jeroen) has 30 years software dev / systems & cloud architect experience and Claude Code 20x access. +Read your briefing at `docs/briefings/tyre.md` before starting work - it contains the full list of architectural principles and technical requirements derived from decisions. Read the relevant governance/ domain files listed in your briefing for confirmed decisions and `docs/discussions/` for active discussions. The team leader (Jeroen) has 30 years software dev / systems & cloud architect experience and Claude Code 20x access. diff --git a/.claude/rules/asset-pipeline.md b/.claude/rules/asset-pipeline.md index a1ef484d4..6745d2c4a 100644 --- a/.claude/rules/asset-pipeline.md +++ b/.claude/rules/asset-pipeline.md @@ -118,22 +118,22 @@ systems.db is stale — run `make regen-db` before pushing. ``` Fix: run `make regen-db`, stage `server/data/systems.db`, amend or add a commit. -Or use `/pr-push` — it detects stale generator sources and reruns `make regen-db` +Or use `/pr-process` — it detects stale generator sources and reruns `make regen-db` automatically before pushing. The check script is `tooling/check-systems-db-stamp`. Run it interactively with `make check-systems-db` or `python3 tooling/check-systems-db-stamp --verbose`. The `GENERATOR_SOURCES` dict in `tooling/generator_sources.py` is the single registry (T-1067) — the check script and the importer's stamp writer both import it, and -the `/pr-push` skill derives its source-file watch list from +the `/pr-process` skill derives its source-file watch list from `python3 tooling/generator_sources.py --list`. When you add a new generator or source file, register it there and nowhere else. --- -## /pr-push integration (T-858) +## /pr-process integration (T-858) -The `/pr-push` skill checks whether any generator source files are modified on the +The `/pr-process` skill checks whether any generator source files are modified on the branch. If they are, it automatically runs `make regen-db` and stages the updated `server/data/systems.db` before pushing — preventing pre-push hook rejections on branches that modify generators without regenerating. diff --git a/.claude/rules/tea-cli.md b/.claude/rules/tea-cli.md index ab9e74b21..45af3a818 100644 --- a/.claude/rules/tea-cli.md +++ b/.claude/rules/tea-cli.md @@ -24,21 +24,28 @@ tea issue list --login schweitz --repo jpmschweitzer/settled-reach --state open ## Key rules -- **All flags must be explicit** — omitting `--login` or `--repo` triggers interactive prompts that crash in Claude Code (no TTY) +- **All flags must be explicit** — omitting `--login` or `--repo` triggers interactive prompts that crash in Claude Code (no TTY). **One exception: `tea pr create` must OMIT `--repo`** — see Pull requests below. - **Use `--output simple`** for machine-readable output (no table borders) - **For comments, use `tooling/tea-comment @/tmp/file.md`** for long comments (write to file first, then pass `@filepath`). Short inline strings also work: `tooling/tea-comment "body"`. The `@filepath` form avoids `$()` subshells which break permission matching. - **`tea pr reject` does not work on your own PRs** — use `tea comment` instead -- **Run `tea` from the main checkout, never a linked worktree** — its go-git can't read a worktree's `.git` *file* and errors with "local/remote repository required". `tea pr create`/`close`/`comment` all take explicit `--login`/`--head`/`--base`, so cwd only needs to be the main repo, not the branch's worktree. +- **Run `tea` from the main checkout, never a linked worktree** — its go-git can't read a worktree's `.git` *file* and errors with "local/remote repository required". `tea pr close`/`comment` take explicit `--login`/`--repo` so cwd barely matters for them, but `tea pr create` resolves the repo from the **local checkout's remote** (see below) — that is why it must run from the main repo. - **Never delete protected branches:** `main`, `maintenance`, `server`, `client`, `copy`, `audio`, `visual`, `ci` are protected on Gitea. Do not use `tea pr clean`, `git push --delete`, or `git branch -D` on these branches. ## Pull requests **Use `tea` (Gitea CLI), not `gh` (GitHub CLI).** The remote is Gitea at `git.schweitz.internal`. -Always provide all required flags to ensure non-interactive execution: +`tea pr create` is the one command that must **omit `--repo`**: an explicit +`--repo ` makes tea skip local-repo setup, but `pr create` unconditionally +needs the local repo handle (it checks whether the head branch is pushed) and +dies with `local repository required` — the flag *causes* the failure it +suggests fixing. Run it from the **main checkout** and let tea infer the repo +from `origin`'s URL. (This inference is also why the login's `ssh_host` in +`~/.config/tea/config.yml` must match the remote host — currently +`git.schweitz.net`.) + ```bash tea pr create \ - --repo jpmschweitzer/settled-reach \ --login schweitz \ --title "feat(scope): short description" \ --description "PR body here" \ diff --git a/.claude/rules/ticket-cli.md b/.claude/rules/ticket-cli.md index a54b7f856..997ef48d3 100644 --- a/.claude/rules/ticket-cli.md +++ b/.claude/rules/ticket-cli.md @@ -37,7 +37,7 @@ pql ticket board [--team T] # kanban view pql ticket status T-440 in_progress # backlog|ready|in_progress|review|done|cancelled pql ticket assign T-440 dudley pql ticket team T-440 server -pql ticket setparent T-9 T-2 # (clear with no parent arg) +pql ticket setparent T-9 T-2 # (clear with: setparent T-9 none) pql ticket append T-440 "extra context" # also --file PATH / --stdin # Dependencies diff --git a/.claude/settings.json b/.claude/settings.json index 24e507468..5389fdfef 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -38,6 +38,7 @@ "Bash(tea *)", "Bash(tooling/tea-comment *)", + "Bash(tooling/worktree-setup:*)", "Bash(cargo test *)", "Bash(cargo test)", @@ -51,6 +52,13 @@ "Bash(ruff check)", "Bash(tests/run-*)", + "Bash(godot4:*)", + "Bash(gdformat:*)", + "Bash(tooling/atlas:*)", + "Bash(tooling/validate-ron:*)", + "Bash(tooling/godot-cold-parse:*)", + "Bash(tooling/pr-watchlist-diff:*)", + "Bash(chmod *)", "Bash(ls *)", "Bash(find *)", @@ -59,6 +67,8 @@ "Bash(sed -n *)", "Read(//var/mnt/data/projects/settled-reach/**)", + "Edit(//var/mnt/data/projects/settled-reach/**)", + "Write(//var/mnt/data/projects/settled-reach/**)", "Read(//tmp/**)", "Grep", "Glob", @@ -68,14 +78,36 @@ "Skill(pr-process)", "Skill(pr-review)", "Skill(ticket)", - "Skill(docs-search)", "Skill(workshop-start)" ], "deny": [ "Bash(git push --force *)", + "Bash(git push --force:*)", + "Bash(git push --force)", "Bash(git reset --hard *)", + "Bash(git reset --hard:*)", + "Bash(git reset --hard)", "Bash(git clean -f *)", - "Bash(rm -rf *)" + "Bash(git clean -f:*)", + "Bash(git clean -f)", + "Bash(rm -rf *)", + "Bash(rm -rf:*)" + ], + "ask": [ + "Edit(.claude/settings.json)", + "Write(.claude/settings.json)", + "Edit(.claude/settings.local.json)", + "Write(.claude/settings.local.json)", + "Edit(.claude/hooks/**)", + "Write(.claude/hooks/**)", + "Edit(.config/hooks/**)", + "Write(.config/hooks/**)", + "Edit(.worktrees/**/.claude/settings.json)", + "Write(.worktrees/**/.claude/settings.json)", + "Edit(.worktrees/**/.claude/hooks/**)", + "Write(.worktrees/**/.claude/hooks/**)", + "Edit(.worktrees/**/.config/hooks/**)", + "Write(.worktrees/**/.config/hooks/**)" ] }, "hooks": { diff --git a/.claude/skills/atlas/SKILL.md b/.claude/skills/atlas/SKILL.md index e1c84293a..97e8dcfd8 100644 --- a/.claude/skills/atlas/SKILL.md +++ b/.claude/skills/atlas/SKILL.md @@ -4,7 +4,7 @@ description: > Query and manage celestial bodies and stations in systems.db. Use when the user says "atlas", "show system", "list bodies", "list stations", "author system", "show body", or invokes /atlas. Wraps the Rust atlas CLI binary. Also provides - the full per-system authoring pipeline via "author-system" subcommand. + the full per-system authoring pipeline via the /atlas author-system workflow. user-invocable: true allowed-tools: Bash, Read, Grep, Glob, Write, Edit, Agent --- @@ -115,15 +115,13 @@ tooling/atlas stats ``` Returns: system count, body count by type, inhabited count, station count by type. -### Bulk populate from system data (classifier pass) +### Corridor status (remaining unfinished systems) ```bash -tooling/atlas populate --dry-run # preview what would be created -tooling/atlas populate # create body/station records +tooling/atlas corridor-status ``` -Reads `habitable_planet_count`, `inhabited_planet_count`, `gas_giant`, -`asteroid_belt`, and `horizon_station` from existing system data. -Creates: planets (inhabited first), gas giants, asteroid belts, oort cloud per -system, horizon station per oort cloud. Skips systems that already have bodies. +Shows remaining unfinished systems grouped by geographic sector and hop +distance. Supersedes the old bulk-populate classifier pass (removed) — use +the per-system `author` → review → `commit-system` workflow below instead. ### Update a system-level field ```bash @@ -149,20 +147,33 @@ the commit message (defaults to `east_reach`). ## Body ID Naming Convention +Real forms, verified against the ~3,300 IDs in `docs/atlas/proposals/`: + ``` -GJ-{n} — star (single) -GJ-{n}A — primary star (binary) -GJ-{n}B — secondary star (binary) -GJ-{n}b/c/d... — planets, innermost first -GJ-{n}Ab/c/d... — planets orbiting primary only -GJ-{n}Bb/c/d... — planets orbiting secondary only -GJ-{n}d-1 — first moon of third planet -GJ-{n}d-S1 — first station orbiting third planet -GJ-{n}-oort — oort cloud region -GJ-{n}-belt — asteroid belt -GJ-{n}-oort-S1 — horizon station in oort cloud +GJ {n} — system_id (space, e.g. "GJ 105A"; "A"/"B" suffix for binary) +GJ{n}A — primary star's body prefix (no dash after GJ) +GJ{n}Ab/c/d... — planets orbiting primary, innermost first (no dash) +GJ{n}Bb/c/d... — planets orbiting secondary only +GJ{n}Ae-m1 — first moon of body "e" (both forms seen — see below) +GJ{n}Ae-1 — first moon of body "e" (bare-number form, also seen) +GJ{n}Ac-S1 — first station orbiting body "c" +GJ{n}-belt — asteroid belt +GJ{n}-oort — oort cloud region +GJ{n}-oort-S1 — horizon station in oort cloud ``` +**Moon suffixes are not standardized** — both `-m{n}` (e.g. `GJ105Ae-m1`) and +bare `-{n}` (e.g. `GJ0d-1`) are in live use across proposals in roughly equal +numbers (~208 vs ~228 of 436 moon entries as of this check). Match whichever +form the system's existing proposal already uses; don't silently convert one +to the other. + +**Wiki slugs use the dashed form** (`GJ-105A`, spaces→dashes) for the system +directory under `wiki/star-systems/`, but body/station IDs inside that +directory keep the undashed body-ID form (e.g. +`wiki/star-systems/GJ-105A/bodies/GJ105Ac`) — the dash only applies to the +system-level slug, never to body IDs. + ## Entity Hierarchy ``` @@ -200,19 +211,32 @@ See `references/author-system.md` for the full workflow. - `references/review-ozzie.md` — Ozzie's review checklist (includes astrophysical sanity) - `references/integrity-check.md` — automated verification rules -## Scope — A True Atlas +## Asset Pipeline Relationship -This skill will grow to cover the full geographic hierarchy of the Reach: +See `.claude/rules/asset-pipeline.md` (the "Golden Rule": edit sources, not +`systems.db`) and D-223. The bodies/stations catalog this skill authors is +**atlas-CLI-owned** and survives `make regen-db`: `import_economics` only +*enriches* existing body rows (radius, axial tilt, biosphere class — see +`tooling/economy-db/economy_import/bodies.py`), it never deletes or +regenerates the catalog. What `import_economics` *does* clear on every regen +is the separate `atlas_*` geometry index tables (`atlas_cities`, +`atlas_roads`, etc., D-223) — those hold cascade-computed geometry, not the +hand-authored body/station identity data. So: **atlas write commands +(`add-body`, `add-station`, `author`/`commit-system`) are the sanctioned path +for the bodies/stations catalog**; they don't need a regen-db round-trip and +aren't at risk from one. -- **Galactic:** 301 systems, gate topology, sector/corridor data -- **System:** orbital bodies, stations, oort cloud -- **Planetary surface:** continents, oceans, mountain ranges, rivers, biome regions -- **Settlement:** cities, towns, villages, outposts, rail lines, road hierarchy -- **District:** neighborhoods, zones, named areas/provinces -- **Local:** named locations, landmarks, facilities +## Scope — Atlas Data Ownership -As the development cascade progresses through Phase 1 (wiki content) → Phase 3 -(planetary maps / Atlas of the Reach), the atlas CLI and its schema will extend -to cover each level. The goal is a single queryable geographic database from -galaxy scale to street level — the in-game implant's atlas app reads from this -same data. +The atlas CLI owns the **galactic** and **system** levels — star systems, +orbital bodies, stations — as hand-authored, reviewed data via this skill's +`author-system` pipeline. That catalog is durable (see "Asset Pipeline +Relationship" above). + +Everything below system scale — planetary surface, settlements, districts, +local detail — is **not** grown by extending this CLI or its schema. D-223 +retired the Python atlas geometry generator; the deterministic server-side +generation cascade (Phase 4, the active phase — see CLAUDE.md's Development +Cascade) is the sole producer of that geometry going forward. The `atlas_*` +index tables exist so the implant Atlas viewer can display cascade output, +not as a target for this skill to populate by hand. diff --git a/.claude/skills/atlas/references/integrity-check.md b/.claude/skills/atlas/references/integrity-check.md index fc736fa76..4f90b0c37 100644 --- a/.claude/skills/atlas/references/integrity-check.md +++ b/.claude/skills/atlas/references/integrity-check.md @@ -2,48 +2,64 @@ Run these checks on the proposal JSON before commit. All must pass. +`tooling/atlas-verify` only ever opens the proposal JSON file — it never opens +`systems.db`. Checks that require comparing against the live database (2, 4) +are **not enforced by the script** and must be performed manually or by the +authoring agent before commit. + ## Checks -1. **Inhabited bodies have names:** +1. **Inhabited bodies have names** — *(script-checked)* Every body with `inhabited: true` must have `proper_name` set (not null). -2. **Uninhabited bodies have no names:** - Every body with `inhabited: false` must have `proper_name: null`. +2. **Uninhabited bodies have no names** — *(manual/agent-performed — the + script explicitly skips this check)* + Every body with `inhabited: false` should have `proper_name: null`. Exception: bodies that serve as named research outposts (e.g., "Corrosion Bench") where the name IS the body's identity. -3. **All stations have names:** +3. **All stations have names** — *(script-checked)* Every station must have `proper_name` set. Use the station_id as the name if the station has no cultural name (e.g., "GJ406g-S1" for an anonymous fuel depot). -4. **No name collisions:** +4. **No name collisions** — *(manual/agent-performed — requires querying + `systems.db`, which the script never opens)* ```sql SELECT proper_name FROM bodies WHERE proper_name IS NOT NULL UNION SELECT proper_name FROM stations WHERE proper_name IS NOT NULL ``` No proposed name may duplicate an existing name in the database. -5. **Body count minimum:** +5. **Body count minimum** — *(script-checked)* - G/F stars: ≥ 8 planets (excluding moons, belts, oort) - K stars: ≥ 7 planets - - M dwarfs: ≥ 6 planets - - A/B stars: ≥ 6 planets + - M dwarfs and A/B stars: ≥ 6 planets (same threshold, script does not + distinguish these two cases) -6. **Required structures:** +6. **Required structures** — *(script-checked)* - Oort cloud body present - Horizon station present with `has_gate_infrastructure: true` - At least 1 asteroid belt (unless wiki explicitly contradicts) -7. **Orbit consistency:** +7. **Orbit consistency** — *(script-checked)* - `orbit_index` increases monotonically for top-level bodies - No duplicate `orbit_index` at the same parent level - - Moon `orbit_index` starts at 1 within parent + - Same monotonic + no-duplicate check applied per-parent to moon + `orbit_index` values (the script does not separately verify that moon + numbering starts at 1) -8. **Parent references valid:** +8. **Parent references valid** — *(script-checked)* - Every moon's `parent_body_id` references a body in the same proposal - Every station's `orbits_body_id` references a body in the same proposal +9. **star_type vs spectral_class consistency** — *(script-checked, not + previously documented here)* + The leading letter of `star_type` (e.g. "K") must match the leading class + letter of `spectral_class` after stripping dwarf/subdwarf prefixes (e.g. + "K3V" → "K"). Skipped when `star_type` is `"binary"` or `"unusual"`, or + when either field is missing. + ## Running the Check ```bash @@ -57,3 +73,12 @@ make atlas-verify The script reports all failures per file, not just the first one. Exit code 1 if any file has errors. + +**Exclusions:** `GJ0.json` (Sol) and `GJ1221.json` are permanently excluded +from the script's checks. Sol is hand-authored with different rules (named +uninhabited bodies, real Earth/Mars/Luna geography) and is exempt from the +normal generator/naming pipeline (D-223). GJ1221 is a dead-star remnant system +("stellar consumption remnant" per its proposal notes) with zero planets — +just a belt, an oort cloud, and a research horizon station — which would +otherwise fail the body-count-minimum check (5) that assumes every system has +planets. diff --git a/.claude/skills/atlas/references/naming-brief.md b/.claude/skills/atlas/references/naming-brief.md index 069cc91df..013f07c75 100644 --- a/.claude/skills/atlas/references/naming-brief.md +++ b/.claude/skills/atlas/references/naming-brief.md @@ -23,7 +23,8 @@ Collision avoidance — these names are already in use: UNION SELECT proper_name FROM stations WHERE proper_name IS NOT NULL} Name ALL inhabited bodies AND ALL stations in one pass. -Uninhabited bodies do NOT get names (proper_name stays null). +Uninhabited bodies do NOT get names (proper_name stays null) — unless the +name IS the body's identity (a named lore outpost, e.g. "Corrosion Bench"). ONE name + one-line rationale per location. Message only, no file. ``` @@ -38,4 +39,6 @@ ONE name + one-line rationale per location. Message only, no file. "The Exchange"). They should feel like real place names that happened to be assigned to transit/exchange facilities. - Every inhabited body and every station MUST get a proper_name. -- Uninhabited bodies MUST have proper_name = null (body_id is the UI label). +- Uninhabited bodies default to proper_name = null (body_id is the UI label) — + unless the name IS the body's identity (named lore outposts, e.g. "Corrosion + Bench"), matching the exception in `references/integrity-check.md` check 2. diff --git a/.claude/skills/audio-gen/SKILL.md b/.claude/skills/audio-gen/SKILL.md index 4c820e289..8d80072c7 100644 --- a/.claude/skills/audio-gen/SKILL.md +++ b/.claude/skills/audio-gen/SKILL.md @@ -19,6 +19,12 @@ Asset descriptions, filenames, bus routing, and design intent are documented in `docs/assets/audio/`. This skill provides the prompt system, generation workflow, and quality validation. +`audio-health`, `audio-generate`, and `audio-batch` re-exec into the project +`.venv` on startup (`audio-post` doesn't need to — it only shells out to +ffmpeg). On a fresh clone with no `.venv` yet, they fail fast with `error: +.venv not found — run make setup-venv first.` — run that once before using +this skill. + ## API Access **Never call the API directly.** Use the wrapper scripts: @@ -39,7 +45,7 @@ tooling/db/audio-generate "prompt text" \ --output-ogg client/assets/audio/final.ogg # Batch-generate from a manifest (preferred for multiple assets) -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json +tooling/db/audio-batch docs/assets/audio/.json ``` ### Parameters @@ -87,11 +93,12 @@ approvals to 2: one Write (manifest) + one Bash (batch run). ### 1. Create the manifest -Write a JSON manifest to `docs/assets/audio/batch-{sprint}-{ticket}.json`: +Write a JSON manifest to `docs/assets/audio/batch-T-.json` (current +ticket-ID scheme, e.g. `batch-T-327.json` — not the retired sprint scheme): ```json { - "description": "Sprint 10 ambient + world SFX batch", + "description": "T-327 ambient + world SFX batch", "output_dir": "client/assets/audio", "gen_dir": "client/assets/audio/gen", "defaults": { @@ -134,20 +141,27 @@ Write a JSON manifest to `docs/assets/audio/batch-{sprint}-{ticket}.json`: Asset `id` values must match IDs in `docs/assets/audio/{category}.md` (e.g., AMB-001, SFX-002, UI-005). This couples the manifest to the asset inventory. +**`lufs`/`quality` defaults apply to `synth` assets only.** `audio_batch.py`'s +SAO path (`run_sao_generate`) only forwards `steps`/`cfg`/`timeout` to the +connector — `--lufs`/`--quality` aren't even exposed as CLI flags on +`audio_connector.py generate`, so a manifest's `defaults.lufs`/`defaults.quality` +are silently ignored for `method: "sao"` assets. SAO post-processing is fixed +at -16 LUFS / quality 6 regardless of what the manifest says. + ### 2. Run the batch ```bash # Full run -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json +tooling/db/audio-batch docs/assets/audio/.json # Dry run — preview what would be generated -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --dry-run +tooling/db/audio-batch docs/assets/audio/.json --dry-run # Generate only specific assets -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --only AMB-001,AMB-002 +tooling/db/audio-batch docs/assets/audio/.json --only AMB-001,AMB-002 # Skip assets that already have OGG files -tooling/db/audio-batch docs/assets/audio/batch-s10-327.json --skip-existing +tooling/db/audio-batch docs/assets/audio/.json --skip-existing ``` ### 3. Update asset docs with prompts @@ -250,6 +264,9 @@ After generating, verify: ## File Placement Generated assets go to `client/assets/audio/` with exact filenames from the -asset docs. Intermediates go to `client/assets/audio/gen/` (gitignored). +asset docs. Intermediates go to `client/assets/audio/gen/` — untracked (the +directory doesn't exist until first use and isn't in `.gitignore`); delete +intermediates yourself after selecting the final candidate rather than +relying on git to ignore them. AudioManager discovers assets by directory scan — filenames must match exactly. diff --git a/.claude/skills/audio-gen/references/sonic-palette.md b/.claude/skills/audio-gen/references/sonic-palette.md index d9d2751d8..829aee682 100644 --- a/.claude/skills/audio-gen/references/sonic-palette.md +++ b/.claude/skills/audio-gen/references/sonic-palette.md @@ -32,7 +32,10 @@ For environmental sounds, human cognition, physical world. **Character:** Human warmth. Natural attack and decay. Reverberant — exists in physical space. The sound of a real place with real materials. -**Frequency range:** 60Hz-3kHz primary. Soft or natural attack, room tail. +**Frequency range:** 300Hz-1.5kHz primary. Soft or natural attack, room tail. +(The wider 60Hz-3kHz span belongs to the Station Baseline layered bed below — +low-frequency foundation through high-frequency detail — not to a single +Organic source sound.) **Used for:** fog_recognition, all amb_* ambient loops, all sfx_footstep_* diff --git a/.claude/skills/bug-report/SKILL.md b/.claude/skills/bug-report/SKILL.md index c176bc354..f14d0749b 100644 --- a/.claude/skills/bug-report/SKILL.md +++ b/.claude/skills/bug-report/SKILL.md @@ -6,7 +6,7 @@ description: > or invokes /bug-report. Scans the user:// bug-reports directory, summarizes each report, and offers investigation, ticket creation, or dismissal. user-invocable: true -allowed-tools: Bash, Read, Grep, Glob, Write +allowed-tools: Bash, Read, Grep, Glob --- # Bug Report Skill @@ -26,12 +26,13 @@ contains these files: | File | Purpose | |------|---------| -| `description.txt` | Tester notes + metadata (tick, room, stance, facing, position) | +| `description.txt` | Tester notes + metadata (tick, room, stance, facing, position, timestamp, ring-buffer depth) | | `render.txt` | Simplified text render of the game snapshot | +| `screenshot.png` | Screenshot of the game view at the moment the report was filed | | `snapshot.json` | Full JSON snapshot (entities, dialogue state, etc.) | | `inputs.jsonl` | Last 60 ticks of player input (replay format) | | `snapshots.jsonl` | Last 60 ticks of observer snapshots | -| `seed.txt` | RNG seed for deterministic replay | +| `seed.txt` | RNG seed for deterministic replay, or the literal string `unavailable` (see below) | ## Invocation @@ -42,10 +43,13 @@ contains these files: ### 1. Scan for unprocessed reports -List all report directories in the bug reports directory: +List all report directories in the bug reports directory. Match the +`gauntlet-*` naming pattern specifically, not a bare `*/` glob — this +excludes the `archive/` subdirectory (see "Create ticket" below) from being +mistaken for a report: ```bash -ls -1d "/var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/"*/ +ls -1d "/var/home/jeroenschweitzer/.local/share/godot/app_userdata/The Settled Reach/bug-reports/"gauntlet-*/ ``` If no directories are found, report "No bug reports found." and stop. @@ -62,10 +66,17 @@ For each report directory, read the following files using the Read tool: - Tick number - Room name - Player stance, facing, position + - `Timestamp` (wall-clock time the report was filed) + - `RingBufferTicks` (depth of the `inputs.jsonl`/`snapshots.jsonl` history — + 60 today, but read it rather than assume) 2. **`render.txt`** — extract: - A brief description of what the text render shows (room layout, visible entities, player position marker) -3. **`snapshot.json`** — extract: +3. **`screenshot.png`** — view with the Read tool (it renders the image) and + note what it shows. Compare against `render.txt`/`snapshot.json` — a + mismatch between what the screenshot shows and what the snapshot data says + is itself a bug signal (see Tips: rendering vs simulation desync). +4. **`snapshot.json`** — extract: - Total entity count - Whether dialogue is active (look for `dialogue` or `conversation` keys with non-null/non-empty values) @@ -73,7 +84,11 @@ For each report directory, read the following files using the Read tool: non-empty values) - NPC names and positions if present - Any error or anomaly fields -4. **`seed.txt`** — note the seed value for reference +5. **`seed.txt`** — note the seed value for reference. If the file contains + the literal string `unavailable`, deterministic replay is not currently + possible (the server doesn't yet emit `rng_seed` on `ObserverSnapshot`) — + report "seed unavailable — replay not possible", don't paste the sentinel + into a ticket as if it were a usable seed. Do NOT read `inputs.jsonl` or `snapshots.jsonl` during the summary phase. These are large files reserved for the investigation step. @@ -121,9 +136,13 @@ Dig deeper into the report for root cause analysis: - State machine transitions that look wrong - Component values going out of expected range - Entities appearing or disappearing unexpectedly -4. Cross-reference with `render.txt` to confirm visual manifestation -5. Read `seed.txt` and note it — the seed plus `inputs.jsonl` should allow - deterministic replay of the scenario +4. Cross-reference with `render.txt` and `screenshot.png` to confirm visual + manifestation — a mismatch between the screenshot and the snapshot/render + data is itself the rendering-desync signal described in Tips +5. Read `seed.txt` and note it — if it holds a real seed, it plus + `inputs.jsonl` should allow deterministic replay of the scenario; if it + holds the `unavailable` sentinel, say so explicitly and skip the replay + step rather than treating the sentinel as a seed Present findings as a root cause analysis: @@ -143,8 +162,8 @@ Present findings as a root cause analysis: - {system 2}: {how it's involved} ### Reproduction -Seed: {seed} -Replay inputs.jsonl from tick {start} to reproduce. +Seed: {seed, or "unavailable — replay not possible" if seed.txt held the sentinel} +Replay inputs.jsonl from tick {start} to reproduce (seed permitting). ### Suggested fix {If identifiable from the snapshot data} @@ -166,20 +185,42 @@ nature of the bug: issues) Construct the ticket title and description from the report summary and any -investigation findings. Use the pql CLI: +investigation findings. Parent the ticket per the cascade model (CLAUDE.md +Development Cascade / `.claude/rules/ticket-cli.md`): +- **`--parent T-1037`** for maintenance/test-debt bugs (bugs in test + infrastructure, tooling, or process hygiene rather than game systems) +- **the active phase epic** for phase-scoped bugs (bugs in game systems + currently under construction) — don't hardcode the id; verify first with + `pql ticket show T-745 --tree` and use whichever phase epic has + `"status": "in_progress"` (currently `T-750`, Phase 4, but the cascade + advances) + +Use the pql CLI: ```bash -pql ticket new bug "{title}" --team {team} --description "{description}" +pql ticket new bug "{title}" --team {team} --description "{description}" --parent {T-1037 or the active phase epic} ``` The description should include: - Bug summary (from tester notes) - Tick, room, position - Key observations from snapshot analysis -- Seed for reproduction +- Seed for reproduction, or "seed unavailable — replay not possible" if + `seed.txt` held the sentinel - Report directory name for reference -After creating the ticket, report the ticket ID to the user. +After creating the ticket, **verify it** with `pql ticket show ` (per +`.claude/rules/ticket-cli.md`'s verify-after-create step) to confirm the +title and parenting landed clean, then report the ticket ID to the user. + +A report is "processed" once it has a resulting ticket (or was dismissed) — +after reporting the ticket ID, offer to either delete the report directory +(same confirmation rule as Dismiss below) or move it to +`{bug-reports-dir}/archive/{report-dir}/`, for cases where the tester or a +reviewer might want to re-open it later. **Always confirm with the user +first**, same as Dismiss. This is what makes "Remaining unprocessed" in the +final summary computable — without it, a ticketed report is indistinguishable +from a fresh one on the next run. #### Dismiss @@ -226,6 +267,9 @@ After all reports have been processed, present a summary: positions in the render should match positions in the snapshot. Mismatches are themselves a bug signal (rendering vs simulation desync). - The seed in `seed.txt` combined with `inputs.jsonl` enables deterministic - replay on the server. Note this in any ticket you create. + replay on the server — when a real seed is present. Today `seed.txt` holds + the literal string `unavailable` in every report (the server doesn't yet + emit `rng_seed` on `ObserverSnapshot`); note whichever is true in any + ticket you create, don't assume replay is possible. - If the bug-reports directory does not exist, the tester has not yet run any gauntlet sessions or has not filed any reports. This is not an error. diff --git a/.claude/skills/clide/SKILL.md b/.claude/skills/clide/SKILL.md index 8907e6a0d..8de4b6fe4 100644 --- a/.claude/skills/clide/SKILL.md +++ b/.claude/skills/clide/SKILL.md @@ -4,10 +4,8 @@ description: > Use when you are running inside the clide IDE and want to observe or drive its live UI — panes, editor, files, git, readers, toasts, layout — through the `clide` CLI, or to find out what commands clide exposes. clide is the - IDE hosting this session; it puts `clide` on your PATH and a per-workspace - socket in `CLIDE_SOCK`. Start with `clide capabilities` to enumerate the - live tool surface. Triggers: "what can clide do", "drive the clide UI", - "open this in clide", "show the user", "toast", or invoking /clide. + IDE hosting this session. Triggers: "what can clide do", "drive the clide + UI", "open this in clide", "show the user", "toast", or invoking /clide. user-invocable: true allowed-tools: Bash --- @@ -15,9 +13,10 @@ allowed-tools: Bash # Driving clide from the CLI You are (often) running **inside clide** — a Flutter IDE that hosts this -Claude session. It exposes its whole UI surface as a `clide ` -CLI on your PATH, talking to the running app over a per-workspace socket -(`CLIDE_SOCK`). Every UI action the user can take has a CLI verb, and every +Claude session. It puts `clide` on your PATH and exposes its whole UI surface +as a `clide ` CLI, talking to the running app over a +per-workspace socket (`CLIDE_SOCK`). Every UI action the user can take has a +CLI verb, and every verb's effect is observable — that is the parity contract (D-6). So you can *see what the user sees* and *show the user what you mean*. @@ -53,9 +52,11 @@ one). `clide ` with no verb, or an unknown command, prints usage. ## Conventions -- **Slots:** the layout has three content slots — `sidebar` (left), `workspace` - (center, where Claude lives), `context` (right) — plus the bottom `statusbar`. - Many verbs take a slot. +- **Slots:** the layout has four content slots — `sidebar` (left), `workspace` + (center, where Claude lives), `context` (right), and `dock` (bottom — + Output/Problems panes, hidden by default; D-87) — plus the bottom + `statusbar`. Many verbs take a slot. The live list is whatever + `clide capabilities` reports. - **Honest failures:** a drive verb with no live GUI returns a `toolError` ("no live UI to drive"), not a hang. JSON on stdout; exit code conveys ok/usage/tool error. diff --git a/.claude/skills/d2-diagram/SKILL.md b/.claude/skills/d2-diagram/SKILL.md deleted file mode 100644 index c39ef4c98..000000000 --- a/.claude/skills/d2-diagram/SKILL.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -name: d2-diagram -description: > - Generate technical diagrams using d2 (text-to-diagram CLI). Use when the - user says "create a diagram", "draw architecture", "make a flowchart", - "diagram this", "render d2", "d2", "data flow diagram", "entity relationship", - "state machine", "sequence diagram", "UI flow", or invokes /d2-diagram. - Produces .d2 source files and renders them to PNG. Also use when asked - to update, re-render, or batch render existing diagrams. ---- - -# d2 Diagram Generation - -Generate technical diagrams from text using d2 (v0.7.1). Pure CLI, no -external dependencies beyond the d2 binary. - -**Binary:** `/home/linuxbrew/.linuxbrew/bin/d2` - -## Project Defaults - -| Setting | Value | Override | -|---------|-------|----------| -| Theme | 200 (Dark Mauve) | `--theme N` | -| Layout | dagre | `--layout elk` | -| Padding | 100px | — | -| Format | PNG | `--svg` | - -## Output Convention - -``` -docs/diagrams/ - architecture/ # System architecture, IPC, component layout - data-flow/ # Sequence diagrams, data pipelines - entity/ # ER diagrams, ECS component schemas - state/ # State machines, behavior trees - ui/ # UI navigation flow, screen transitions -``` - -Both `.d2` source and `.png` output are tracked in git. - -## Single Diagram Workflow - -1. **Determine category** — architecture, data-flow, entity, state, or ui -2. **Read template** — `references/diagram-templates.md` for the matching category -3. **Read syntax** — `references/d2-syntax-guide.md` if unfamiliar with d2 syntax -4. **Write .d2 source** — to `docs/diagrams/{category}/{name}.d2` -5. **Validate** — `.claude/skills/d2-diagram/scripts/d2-render.sh validate {file}` -6. **Render** — `.claude/skills/d2-diagram/scripts/d2-render.sh {file}` -7. **Read SVG** — verify the output, present to user - -### Script Usage - -```bash -# Render with project defaults -.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/ipc-bridge.d2 - -# Validate syntax only -.claude/skills/d2-diagram/scripts/d2-render.sh validate docs/diagrams/architecture/ipc-bridge.d2 - -# Auto-format source -.claude/skills/d2-diagram/scripts/d2-render.sh fmt docs/diagrams/architecture/ipc-bridge.d2 - -# Sketch mode (hand-drawn look for drafts) -.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/ui/flow.d2 --sketch - -# Light theme (for printable docs) -.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/entity/schema.d2 --theme 0 - -# SVG output (if specifically needed) -.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/overview.d2 --svg -``` - -## Batch Render - -Re-render all diagrams after theme or style changes: - -```bash -# All diagrams -.claude/skills/d2-diagram/scripts/d2-batch.sh - -# One category -.claude/skills/d2-diagram/scripts/d2-batch.sh docs/diagrams/architecture/ - -# Preview what would render -.claude/skills/d2-diagram/scripts/d2-batch.sh --dry-run - -# Force re-render everything -.claude/skills/d2-diagram/scripts/d2-batch.sh --force -``` - -Batch skips files whose PNG is newer than the `.d2` source unless `--force`. - -## Advanced Patterns - -### Variables for consistent styling - -```d2 -vars: { - color-bg: "#2a3040" - color-stroke: "#333340" - color-text: "#c8d0e0" - color-accent: "#c8d8f0" -} -``` - -### Multi-board (layers) - -```d2 -# Base diagram here - -layers: { - detailed: { - # More detailed view - } -} -``` - -### Sequence diagrams - -```d2 -shape: sequence_diagram -client: Godot Client -server: Rust Server - -client -> server: TickRequest(delta) -server -> client: WorldState(entities) -``` - -### Imports - -Split shared definitions into a separate file and import: - -```d2 -...@shared-defs.d2 -``` - -## Agent Guidance - -- **Qatux** — Architecture decision records, system overview diagrams, data - schemas. Prefer architecture and entity templates. -- **Tyre** — IPC bridge, ECS system flow, chunk loading pipeline, perception - system data flow. Prefer architecture and data-flow templates. -- **Araminta** — UI navigation flow, screen transitions, component hierarchy. - Prefer UI flow template. - -## References - -- `references/d2-syntax-guide.md` — Language quick reference (shapes, edges, - containers, styling, variables). Read when unfamiliar with d2 syntax. -- `references/diagram-templates.md` — Five category templates with complete - d2 source examples. Read when starting a new diagram. diff --git a/.claude/skills/d2-diagram/references/d2-syntax-guide.md b/.claude/skills/d2-diagram/references/d2-syntax-guide.md deleted file mode 100644 index e1c05de50..000000000 --- a/.claude/skills/d2-diagram/references/d2-syntax-guide.md +++ /dev/null @@ -1,212 +0,0 @@ -# D2 Language Quick Reference - -## Nodes - -```d2 -server # Implicit label from key -server: Simulation Server # Explicit label -server: Simulation Server { # With properties - shape: hexagon - style.fill: "#2d3436" -} -``` - -## Edges - -```d2 -a -> b # Directed -a <- b # Reverse directed -a <-> b # Bidirectional -a -- b # Undirected -a -> b: "label" # Labeled edge -a -> b -> c # Chained -``` - -## Containers (nesting) - -```d2 -infrastructure: { - server: Simulation Server - database: State Store { - shape: cylinder - } -} -``` - -## Shapes - -| Shape | Use for | -|-------|---------| -| `rectangle` | Default. Components, modules, generic. | -| `hexagon` | Systems, services, major components. | -| `cylinder` | Databases, storage, persistent state. | -| `diamond` | Decisions, conditions, branch points. | -| `oval` / `circle` | Start/end states, events. | -| `cloud` | External systems, networks. | -| `person` | Actors, users, NPCs. | -| `queue` | Message queues, buffers. | -| `page` | Documents, files. | -| `package` | Packages, modules, crates. | -| `sql_table` | Database tables, ECS component schemas. | -| `class` | Class diagrams, ECS system definitions. | -| `code` | Code blocks (set `language` property). | -| `markdown` | Rich text blocks. | - -## SQL Tables - -```d2 -entity: { - shape: sql_table - id: u64 {constraint: primary_key} - name: String - position: Vec2 - faction_id: u64 {constraint: foreign_key} -} -``` - -## Class Diagrams - -```d2 -perception_system: { - shape: class - +run(world: &mut World) - -calculate_los(entity: Entity): HashSet - #update_knowledge(entity: Entity, seen: HashSet) -} -``` - -## Sequence Diagrams - -```d2 -shape: sequence_diagram -client: Godot Client -server: Rust Server - -client -> server: TickRequest(delta) -server -> server: run ECS systems -server -> client: WorldState(entities) -``` - -## Styling - -```d2 -node: Label { - style: { - fill: "#2d3436" - stroke: "#333340" - stroke-width: 2 - stroke-dash: 5 # Dashed line - opacity: 0.8 - font-size: 14 - font-color: "#c8d0e0" - bold: true - italic: false - border-radius: 4 - shadow: true - 3d: true # Rectangles only - multiple: true # Stacked appearance - double-border: true # Rectangles/ovals only - } -} -``` - -### Edge styling - -```d2 -a -> b: { - style: { - stroke: "#c8d8f0" - stroke-width: 2 - stroke-dash: 5 - opacity: 0.8 - animated: true # Animated flow - } -} -``` - -## Variables - -```d2 -vars: { - color-bg: "#1a1e24" - color-stroke: "#333340" - color-text: "#c8d0e0" - color-accent: "#c8d8f0" -} - -node: { - style.fill: ${color-bg} - style.stroke: ${color-stroke} - style.font-color: ${color-text} -} -``` - -## Direction - -```d2 -direction: right # left-to-right (default for dagre) -direction: down # top-to-bottom -direction: up -direction: left -``` - -## Imports - -```d2 -...@shared-defs.d2 # Spread import (inline all definitions) -``` - -## Icons - -```d2 -node: Label { - icon: https://icons.terrastruct.com/essentials/time.svg -} -``` - -## Layers (multi-board) - -```d2 -# Base diagram content here - -layers: { - detailed: { - # More detailed view - } - simplified: { - # Simplified overview - } -} -``` - -## Scenarios (animated transitions) - -```d2 -# Base state - -scenarios: { - alert: { - # Changes from base for alert state - } - combat: { - # Changes from base for combat state - } -} -``` - -## Comments - -```d2 -# This is a comment -node: Label # Inline comment -``` - -## Project Colors (from visual-grammar-v01.md) - -| Constant | Hex | Usage | -|----------|-----|-------| -| Zone 1 floor | `#1a1e24` | Dark backgrounds | -| Zone 1 wall | `#2a3040` | Component fill | -| Outline standard | `#333340` | Borders, strokes | -| Insert chrome | `#c8d0e0` | Text, labels | -| Zone 1 fixture | `#c8d8f0` | Accents, highlights | diff --git a/.claude/skills/d2-diagram/references/diagram-templates.md b/.claude/skills/d2-diagram/references/diagram-templates.md deleted file mode 100644 index d134c9733..000000000 --- a/.claude/skills/d2-diagram/references/diagram-templates.md +++ /dev/null @@ -1,251 +0,0 @@ -# Diagram Templates - -Copy, adapt, and render. Each template uses project colors from visual-grammar-v01.md. - ---- - -## 1. Architecture Diagram - -System components, relationships, communication channels. - -**When to use:** IPC bridge, perception pipeline, chunk loading, ECS system layout, client-server architecture. - -**Agents:** Tyre (system architecture), Qatux (architecture decision records). - -```d2 -vars: { - color-bg: "#2a3040" - color-stroke: "#333340" - color-text: "#c8d0e0" - color-accent: "#c8d8f0" -} - -direction: right - -client: Godot Client { - shape: hexagon - style.fill: ${color-bg} - style.font-color: ${color-text} - - rendering: Rendering { - style.fill: ${color-bg} - style.font-color: ${color-text} - } - ui: UI Layer { - style.fill: ${color-bg} - style.font-color: ${color-text} - } - bridge: IPC Bridge { - style.fill: ${color-bg} - style.font-color: ${color-text} - style.stroke: ${color-accent} - } -} - -server: Rust Server { - shape: hexagon - style.fill: ${color-bg} - style.font-color: ${color-text} - - ecs: bevy_ecs { - style.fill: ${color-bg} - style.font-color: ${color-text} - } - perception: Perception { - style.fill: ${color-bg} - style.font-color: ${color-text} - } - bridge: IPC Bridge { - style.fill: ${color-bg} - style.font-color: ${color-text} - style.stroke: ${color-accent} - } -} - -client.bridge -> server.bridge: "stdin/stdout" { - style.stroke: ${color-accent} - style.stroke-dash: 5 -} -``` - ---- - -## 2. Entity Relationship - -Data schemas, ECS components, knowledge graph structure. - -**When to use:** Database tables, component definitions, entity relationships, knowledge store schema. - -**Agents:** Tyre (ECS component design), Qatux (schema documentation). - -```d2 -entity: Entity { - shape: sql_table - id: u64 {constraint: primary_key} - name: String - faction_id: u64 {constraint: foreign_key} -} - -position: Position { - shape: sql_table - entity_id: u64 {constraint: foreign_key} - x: f32 - y: f32 - chunk_id: u32 -} - -knowledge: KnowledgeEntry { - shape: sql_table - observer_id: u64 {constraint: foreign_key} - subject_id: u64 {constraint: foreign_key} - fact_type: FactType - confidence: f32 - last_seen_tick: u64 -} - -entity.id -> position.entity_id -entity.id -> knowledge.observer_id -entity.id -> knowledge.subject_id -``` - ---- - -## 3. Sequence / Data Flow - -Ordered interactions between systems over time. - -**When to use:** IPC message flow, tick processing, perception update cycle, dialogue system exchanges. - -**Agents:** Tyre (system interaction design), Qatux (protocol documentation). - -```d2 -shape: sequence_diagram - -client: Godot Client -bridge: IPC Bridge -server: Rust Server -ecs: ECS Systems - -client -> bridge: TickRequest(delta, input) -bridge -> server: deserialize + dispatch -server -> ecs: run_systems(delta) -ecs -> ecs: perception, AI, physics -ecs -> server: collect WorldState -server -> bridge: serialize WorldState -bridge -> client: WorldState(entities, events) -client -> client: update rendering -``` - ---- - -## 4. State Machine - -Entity states, transitions, conditions. - -**When to use:** NPC behavior states, game mode transitions, dialogue state, investigation phases. - -**Agents:** Tyre (behavior system design), Qatux (state documentation). - -```d2 -vars: { - color-state: "#2a3040" - color-text: "#c8d0e0" - color-edge: "#c8d8f0" - color-decision: "#333340" -} - -idle: Idle { - style.fill: ${color-state} - style.font-color: ${color-text} -} - -alert: Alert { - style.fill: ${color-state} - style.font-color: ${color-text} -} - -investigate: Investigate { - style.fill: ${color-state} - style.font-color: ${color-text} -} - -combat: Combat { - style.fill: ${color-state} - style.font-color: ${color-text} - style.stroke: "#f0b840" -} - -flee: Flee { - style.fill: ${color-state} - style.font-color: ${color-text} -} - -idle -> alert: "stimulus detected" { style.stroke: ${color-edge} } -alert -> investigate: "stimulus confirmed" { style.stroke: ${color-edge} } -alert -> idle: "timeout / stimulus lost" { style.stroke: ${color-edge}; style.stroke-dash: 5 } -investigate -> combat: "threat confirmed" { style.stroke: "#f0b840" } -investigate -> idle: "nothing found" { style.stroke: ${color-edge}; style.stroke-dash: 5 } -combat -> flee: "health < threshold" { style.stroke: "#f0b840" } -combat -> idle: "threat eliminated" { style.stroke: ${color-edge}; style.stroke-dash: 5 } -flee -> idle: "safe distance reached" { style.stroke: ${color-edge}; style.stroke-dash: 5 } -``` - ---- - -## 5. UI Flow - -Screen navigation, component hierarchy, interaction paths. - -**When to use:** HUD layout relationships, menu navigation, dialogue flow, insert mode transitions. - -**Agents:** Araminta (UI/visual design), Tyre (interface architecture), Qatux (UI documentation). - -```d2 -vars: { - color-screen: "#1a1e24" - color-panel: "#2a3040" - color-text: "#c8d0e0" - color-nav: "#c8d8f0" -} - -gameplay: Gameplay { - style.fill: ${color-screen} - style.font-color: ${color-text} - - hud: HUD { - style.fill: ${color-panel} - style.font-color: ${color-text} - - minimap: Minimap - monologue: Monologue Panel - insert_display: Insert Display - } - - world: World View { - style.fill: ${color-panel} - style.font-color: ${color-text} - } -} - -pause: Pause Menu { - style.fill: ${color-screen} - style.font-color: ${color-text} - - inventory: Inventory - journal: Journal - settings: Settings -} - -dialogue: Dialogue Mode { - style.fill: ${color-screen} - style.font-color: ${color-text} - - speaker: Speaker Panel - responses: Response List -} - -gameplay -> pause: "ESC" { style.stroke: ${color-nav} } -pause -> gameplay: "ESC / Resume" { style.stroke: ${color-nav}; style.stroke-dash: 5 } -gameplay -> dialogue: "interact with NPC" { style.stroke: ${color-nav} } -dialogue -> gameplay: "end conversation" { style.stroke: ${color-nav}; style.stroke-dash: 5 } -``` diff --git a/.claude/skills/d2-diagram/scripts/d2-batch.sh b/.claude/skills/d2-diagram/scripts/d2-batch.sh deleted file mode 100755 index fe456ad58..000000000 --- a/.claude/skills/d2-diagram/scripts/d2-batch.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)" -RENDER="$SCRIPT_DIR/d2-render.sh" - -usage() { - cat <&2; exit 1 - fi - shift - ;; - esac -done - -[[ ! -d "$DIR" ]] && { echo "ERROR: Directory not found: $DIR" >&2; exit 1; } - -RENDERED=0 -SKIPPED=0 -FAILED=0 - -while IFS= read -r -d '' d2_file; do - png_file="${d2_file%.d2}.png" - - # Skip if PNG is newer than source (unless --force) - if [[ "$FORCE" != true ]] && [[ -f "$png_file" ]] && [[ "$png_file" -nt "$d2_file" ]]; then - SKIPPED=$((SKIPPED + 1)) - continue - fi - - rel_path="${d2_file#"$REPO_ROOT/"}" - - if [[ "$DRY_RUN" == true ]]; then - echo "Would render: $rel_path" - RENDERED=$((RENDERED + 1)) - continue - fi - - if "$RENDER" "$d2_file" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}"; then - RENDERED=$((RENDERED + 1)) - else - echo "FAILED: $rel_path" >&2 - FAILED=$((FAILED + 1)) - fi -done < <(find "$DIR" -name '*.d2' -print0 | sort -z) - -echo "" -echo "Batch complete: $RENDERED rendered, $SKIPPED skipped (up to date), $FAILED failed" diff --git a/.claude/skills/d2-diagram/scripts/d2-render.sh b/.claude/skills/d2-diagram/scripts/d2-render.sh deleted file mode 100755 index fc561d928..000000000 --- a/.claude/skills/d2-diagram/scripts/d2-render.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/bash -set -euo pipefail - -D2="/home/linuxbrew/.linuxbrew/bin/d2" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)" - -DEFAULT_THEME=200 -DEFAULT_LAYOUT="dagre" -DEFAULT_PAD=100 - -usage() { - cat < [options] - -Render a .d2 file to PNG with project defaults (theme $DEFAULT_THEME, $DEFAULT_LAYOUT layout). - -Commands: - validate Check syntax without rendering - fmt Auto-format in place - -Options: - --theme N Override theme (default: $DEFAULT_THEME) - --layout NAME Override layout engine (default: $DEFAULT_LAYOUT) - --sketch Enable hand-drawn sketch mode - --output PATH Override output path (default: input with .png extension) - --svg Render to SVG instead of PNG - -Examples: - $(basename "$0") docs/diagrams/architecture/ipc-bridge.d2 - $(basename "$0") validate docs/diagrams/architecture/ipc-bridge.d2 - $(basename "$0") docs/diagrams/architecture/ipc-bridge.d2 --sketch --theme 0 -EOF - exit 1 -} - -[[ $# -lt 1 ]] && usage - -# Parse subcommand -SUBCMD="" -case "$1" in - validate|fmt) - SUBCMD="$1" - shift - ;; -esac - -[[ $# -lt 1 ]] && usage - -INPUT="$1" -shift - -# Resolve to absolute path -[[ "$INPUT" != /* ]] && INPUT="$REPO_ROOT/$INPUT" - -[[ ! -f "$INPUT" ]] && { echo "ERROR: File not found: $INPUT" >&2; exit 1; } - -# Handle subcommands -if [[ -n "$SUBCMD" ]]; then - "$D2" "$SUBCMD" "$INPUT" - echo "OK: $SUBCMD $INPUT" - exit 0 -fi - -# Parse render options -THEME="$DEFAULT_THEME" -LAYOUT="$DEFAULT_LAYOUT" -SKETCH="" -OUTPUT="" -FORMAT="png" - -while [[ $# -gt 0 ]]; do - case "$1" in - --theme) THEME="$2"; shift 2 ;; - --layout) LAYOUT="$2"; shift 2 ;; - --sketch) SKETCH="-s"; shift ;; - --output) OUTPUT="$2"; shift 2 ;; - --svg) FORMAT="svg"; shift ;; - *) echo "Unknown option: $1" >&2; exit 1 ;; - esac -done - -# Derive output path -if [[ -z "$OUTPUT" ]]; then - OUTPUT="${INPUT%.d2}.$FORMAT" -fi - -# Render -"$D2" -t "$THEME" -l "$LAYOUT" --pad "$DEFAULT_PAD" $SKETCH "$INPUT" "$OUTPUT" - -SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null) -echo "Rendered: $OUTPUT ($(( SIZE / 1024 ))KB)" diff --git a/.claude/skills/frame0-wireframe/SKILL.md b/.claude/skills/frame0-wireframe/SKILL.md deleted file mode 100644 index 6cc572f2e..000000000 --- a/.claude/skills/frame0-wireframe/SKILL.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -name: frame0-wireframe -description: > - Create and export UI wireframes using Frame0 (local desktop wireframing - app with HTTP API). Use when the user says "create wireframe", "wireframe - this", "mock up the UI", "draw a screen", "UI layout", "wireframe the HUD", - "Frame0", "frame0", "export wireframe", or invokes /frame0-wireframe. - Wireframes are authored as local JSON files (source of truth) and synced - to Frame0 for rendering and export. Requires Frame0 to be running locally. ---- - -# Frame0 Wireframe Generation - -Create UI wireframes as JSON files, sync them to Frame0 for rendering, and -export as PNG. Local JSON is the source of truth — Frame0 is the renderer. - -**Frame0 is a renderer, not a workspace.** Treat it as disposable output. -Push freely, delete test pages, keep it clean. Never pull from Frame0 unless -the user explicitly says they have made edits in Frame0 and want to import -them. The pull workflow exists for that case only — do not use it proactively. - -**Prerequisite:** Frame0 desktop app must be running. If not available, -stop and inform the user. Point to `references/setup-guide.md`. - -## Health Check - -Always check first: - -```bash -.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health -``` - -## Core Workflow - -1. **Health check** — verify Frame0 is running -2. **Write wireframe JSON** — to `docs/design/wireframes/{category}/{name}.json` -3. **Push to Frame0** — `frame0-sync.py push ` -4. **Export PNG** — `frame0-sync.py export ` -5. **Clean up** — delete test/scratch pages from Frame0 when done - -### Scripts - -| Script | Purpose | -|--------|---------| -| `scripts/frame0-sync.py` | **Primary.** Push/pull/export wireframes between JSON and Frame0 | -| `scripts/frame0-cmd.sh` | Low-level API wrapper for ad-hoc commands | - -## Wireframe JSON Format - -```json -{ - "name": "Dialogue Box", - "shapes": { - "panel": { - "type": "Rectangle", - "left": 170, "top": 500, "width": 800, "height": 260, - "fillColor": "#1a1e24", - "strokeColor": "#333340", - "corners": [8, 8, 8, 8] - }, - "speaker": { - "type": "Text", - "parent": "panel", - "left": 190, "top": 520, - "text": "LERA KONSTANTIN", - "fontColor": "#c8d0e0", - "fontSize": 16 - }, - "btn-ask": { - "type": "Rectangle", - "parent": "panel", - "left": 190, "top": 670, "width": 370, "height": 30, - "fillColor": "#2a3040", - "strokeColor": "#c8d8f0", - "corners": [4, 4, 4, 4] - } - }, - "connectors": { - "flow-1": { - "tailId": "panel", - "headId": "btn-ask", - "strokeColor": "#c8d8f0" - } - } -} -``` - -### Key rules - -- **Shape IDs are stable local IDs** you control (e.g. `"panel"`, `"btn-ask"`) -- **`parent`** references another local shape ID for nesting -- **`type`** uses create-API names: `Rectangle`, `Ellipse`, `Text`, `Line` -- **Colors** can be hex (`#2a3040`) or Frame0 theme tokens (`$slate6`) -- After a pull, Frame0 returns its native vocabulary (`Box` for Rectangle, - theme tokens for colors). The sync script handles the mapping transparently. -- The `.idmap.json` mapping file (gitignored) tracks local ID ↔ Frame0 ID - -### Sync commands - -```bash -SYNC=".claude/skills/frame0-wireframe/scripts/frame0-sync.py" - -# Push local JSON to Frame0 (clears page, recreates all shapes) -$SYNC push docs/design/wireframes/dialogue/dialogue-box.json - -# Pull Frame0 page back to local JSON (preserves local IDs via mapping) -$SYNC pull "Dialogue Box" docs/design/wireframes/dialogue/dialogue-box.json - -# Push + export as PNG in one step -$SYNC export docs/design/wireframes/dialogue/dialogue-box.json \ - docs/design/wireframes/dialogue/dialogue-box.png -``` - -### Batch export - -Use this when exporting multiple wireframes. It runs as a single Bash call, -avoiding repeated permission prompts. - -```bash -BATCH=".claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh" - -# Dry run first — shows full manifest, no Frame0 calls -$BATCH --dry-run - -# Export everything (skips PNGs already newer than their JSON) -$BATCH - -# Export one category only -$BATCH --category dialogue - -# Force re-export of everything -$BATCH --force -``` - -**Always dry-run first, then get approval before running the live export.** - -## Low-Level Commands - -For ad-hoc operations or exec calls not covered by sync: - -```bash -CMD=".claude/skills/frame0-wireframe/scripts/frame0-cmd.sh" -$CMD health -$CMD list-pages -$CMD current-page -$CMD get-page -$CMD create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}' -$CMD create-connector -$CMD move -$CMD export --format image/png -$CMD exec "view:fit-to-screen" '{}' -``` - -If you find yourself using `exec` for the same command repeatedly, flag it as -a candidate for a proper wrapper in `frame0-cmd.sh`. - -## Project Styling Defaults - -Colors from `docs/design/visual-grammar-v01.md`: - -| Role | Hex | Frame0 token | -|------|-----|-------------| -| Background | `#1a1e24` | `$sage3` | -| Stroke | `#333340` | `$slate6` | -| Fill | `#2a3040` | `$slate5` | -| Text | `#c8d0e0` | `$mint12` | -| Accent | `#c8d8f0` | `$blue12` | - -Use hex when authoring new wireframes. Frame0 maps them to theme tokens on push. - -## Output Convention - -``` -docs/design/wireframes/ - hud/ # HUD layout wireframes - menus/ # Menu screen wireframes - dialogue/ # Dialogue box wireframes - insert/ # Neural insert wireframes -``` - -Both `.json` source and `.png` exports are tracked in git. -`.idmap.json` mapping files are gitignored. - -## Agent Guidance - -- **Araminta** — Primary user. Full wireframe creation, layout iteration, - visual consistency. Uses all component library patterns. -- **Tyre** — Interface architecture wireframes. System boundary diagrams. -- **Qatux** — Export wireframes for UI decision records and documentation. - -## References - -- `references/component-library.md` — Pre-built JSON wireframe templates - (HUD, dialogue, menus, modals, lists, inventory). Copy and adapt. -- `references/api-reference.md` — Frame0 HTTP API command reference and - type mappings. Read for low-level control. -- `references/setup-guide.md` — Frame0 installation and startup for Fedora. diff --git a/.claude/skills/frame0-wireframe/references/api-reference.md b/.claude/skills/frame0-wireframe/references/api-reference.md deleted file mode 100644 index 616c6690e..000000000 --- a/.claude/skills/frame0-wireframe/references/api-reference.md +++ /dev/null @@ -1,241 +0,0 @@ -# Frame0 HTTP API Reference - -Frame0 exposes a local HTTP API when the desktop app is running. - -## Endpoint - -``` -POST http://localhost:{port}/execute_command -Content-Type: application/json -``` - -Default port: **58320** (override via `FRAME0_PORT` env var or `--port` flag). - -## Request / Response - -```json -{"command": "namespace:action", "args": { ... }} -``` - -```json -{"success": true, "data": { ... }} -{"success": false, "error": "description"} -``` - ---- - -## Type Mapping - -Frame0 uses different type names for create vs get: - -| Create API (`type`) | Get API (internal) | Description | -|--------------------|--------------------|-------------| -| `Rectangle` | `Box` | Rectangle with optional corners | -| `Ellipse` | `Ellipse` | Circle/ellipse | -| `Text` | `Text` | Text label | -| `Line` | `Line` | Line/polyline | -| `Frame` | `Frame` | Container from library | -| `Freehand` | `Freehand` | Freehand drawing | -| `Highlighter` | `Highlighter` | Highlighter stroke | - -The sync script handles this mapping transparently. - -## Color Tokens - -Frame0 maps hex colors to theme tokens on creation (`convertColors: true`): - -| Hex | Token | Role | -|-----|-------|------| -| `#1a1e24` | `$sage3` | Background | -| `#2a3040` | `$slate5` | Fill | -| `#333340` | `$slate6` | Stroke | -| `#c8d0e0` | `$mint12` | Text | -| `#c8d8f0` | `$blue12` | Accent | - -Both hex and token strings work in the API. Tokens are preserved on round-trip. - ---- - -## Commands - -### shape:create-shape - -```json -{ - "command": "shape:create-shape", - "args": { - "type": "Rectangle", - "shapeProps": { - "name": "my-button", - "left": 100, "top": 200, "width": 120, "height": 36, - "fillColor": "#2a3040", - "strokeColor": "#c8d8f0", - "corners": [4, 4, 4, 4] - }, - "parentId": "optional-parent-shape-id", - "convertColors": true - } -} -``` - -Returns: shape ID (string). - -### shape:get-shape - -```json -{"command": "shape:get-shape", "args": {"shapeId": "id"}} -``` - -### shape:update-shape - -```json -{ - "command": "shape:update-shape", - "args": { - "shapeId": "id", - "shapeProps": {"fillColor": "#1a1e24", "text": "Updated"}, - "convertColors": true - } -} -``` - -### shape:move - -```json -{"command": "shape:move", "args": {"shapeId": "id", "dx": 50, "dy": -20}} -``` - -### shape:create-connector - -```json -{ - "command": "shape:create-connector", - "args": { - "tailId": "source-id", - "headId": "target-id", - "shapeProps": {"strokeColor": "#c8d8f0"}, - "convertColors": true - } -} -``` - -### shape:create-icon - -```json -{ - "command": "shape:create-icon", - "args": { - "iconName": "search", - "shapeProps": {"left": 100, "top": 100, "width": 24, "height": 24} - } -} -``` - -### shape:get-available-icons - -```json -{"command": "shape:get-available-icons", "args": {}} -``` - -### shape:group / shape:ungroup - -```json -{"command": "shape:group", "args": {"shapeIdArray": ["id1", "id2"]}} -{"command": "shape:ungroup", "args": {"shapeIdArray": ["group-id"]}} -``` - -### edit:delete / edit:duplicate - -```json -{"command": "edit:delete", "args": {"shapeIdArray": ["id1", "id2"]}} -{"command": "edit:duplicate", "args": {"shapeIdArray": ["id"], "dx": 20, "dy": 0}} -``` - -### page:add - -```json -{"command": "page:add", "args": {"pageProps": {"name": "Page Name"}}} -``` - -Returns: `{id, type, name}`. - -### page:get - -```json -{"command": "page:get", "args": {"pageId": "id", "exportShapes": true}} -``` - -### page:get-current-page - -```json -{"command": "page:get-current-page", "args": {}} -``` - -Returns: page ID string. - -### page:set-current-page - -```json -{"command": "page:set-current-page", "args": {"pageId": "id"}} -``` - -### doc:get (list all pages) - -```json -{"command": "doc:get", "args": {"exportPages": true, "exportShapes": false}} -``` - -### page:delete - -```json -{"command": "page:delete", "args": {"pageId": "id"}} -``` - -### file:export-image - -```json -{ - "command": "file:export-image", - "args": { - "pageId": "optional-page-id", - "format": "image/png", - "fillBackground": true - } -} -``` - -Formats: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`. -Returns: base64-encoded image data. - -### view:fit-to-screen - -```json -{"command": "view:fit-to-screen", "args": {}} -``` - ---- - -## Shape Properties - -| Property | Type | Notes | -|----------|------|-------| -| `name` | string | Shape identifier/label | -| `left` | number | X position (origin: top-left) | -| `top` | number | Y position | -| `width` | number | Width in pixels | -| `height` | number | Height in pixels | -| `fillColor` | string | Hex or `$token` | -| `strokeColor` | string | Hex or `$token` | -| `strokeWidth` | number | Border width | -| `fontColor` | string | Text color (hex or `$token`) | -| `fontSize` | number | Font size in pixels | -| `fontFamily` | string | Font name (Frame0 default: `Loranthus`) | -| `text` | string | Text content | -| `wordWrap` | boolean | Enable word wrapping | -| `corners` | number[4] | Border radius [TL, TR, BR, BL] | -| `roughness` | number | Sketch roughness (Frame0 default: 1) | -| `constraints` | array | Auto-sizing constraints | -| `horzAlign` | string | Horizontal text alignment | -| `vertAlign` | string | Vertical text alignment | -| `fillStyle` | string | Fill style (`none` for transparent) | -| `path` | array | Coordinate pairs for lines | diff --git a/.claude/skills/frame0-wireframe/references/component-library.md b/.claude/skills/frame0-wireframe/references/component-library.md deleted file mode 100644 index 3ae364dda..000000000 --- a/.claude/skills/frame0-wireframe/references/component-library.md +++ /dev/null @@ -1,467 +0,0 @@ -# Component Library - -Pre-built wireframe JSON templates for The Settled Reach UI. Copy the JSON, -adapt positions/sizes, save to `docs/design/wireframes/{category}/`, and push. - -**Viewport:** 1140x780 (Godot project settings) -**Grid unit:** 8px -**Min touch target:** 36px height -**Font sizes:** 12 (label), 14 (body), 16 (subtitle), 18 (heading), 24 (title) - ---- - -## 1. HUD Layout - -Main gameplay overlay. Minimap top-right, monologue bottom-center, -insert display bottom-left, action hints bottom-right. - -```json -{ - "name": "HUD Layout", - "shapes": { - "minimap": { - "type": "Rectangle", - "left": 880, "top": 20, "width": 240, "height": 240, - "fillColor": "#1a1e24", "strokeColor": "#333340", - "corners": [4, 4, 4, 4] - }, - "minimap-label": { - "type": "Text", - "parent": "minimap", - "left": 890, "top": 30, - "text": "Minimap", - "fontColor": "#c8d0e0", "fontSize": 14 - }, - "monologue": { - "type": "Rectangle", - "left": 300, "top": 680, "width": 520, "height": 80, - "fillColor": "#1a1e24", "strokeColor": "#333340", - "corners": [4, 4, 4, 4] - }, - "monologue-text": { - "type": "Text", - "parent": "monologue", - "left": 310, "top": 700, "width": 500, - "text": "Internal monologue text appears here...", - "fontColor": "#c8d0e0", "fontSize": 13, "wordWrap": true - }, - "insert": { - "type": "Rectangle", - "left": 20, "top": 600, "width": 260, "height": 160, - "fillColor": "#1a1e24", "strokeColor": "#333340", - "corners": [4, 4, 4, 4] - }, - "insert-label": { - "type": "Text", - "parent": "insert", - "left": 30, "top": 620, - "text": "Neural Insert Data", - "fontColor": "#c8d0e0", "fontSize": 14 - }, - "hints": { - "type": "Rectangle", - "left": 880, "top": 700, "width": 240, "height": 60, - "fillColor": "#1a1e24", "strokeColor": "#333340", - "corners": [4, 4, 4, 4] - }, - "hints-label": { - "type": "Text", - "parent": "hints", - "left": 890, "top": 720, - "text": "[E] Interact [TAB] Insert", - "fontColor": "#c8d0e0", "fontSize": 12 - } - } -} -``` - ---- - -## 2. Dialogue Box - -Speaker panel with response options. Anchored bottom-center during dialogue mode. - -```json -{ - "name": "Dialogue Box", - "shapes": { - "panel": { - "type": "Rectangle", - "left": 170, "top": 500, "width": 800, "height": 260, - "fillColor": "#1a1e24", "strokeColor": "#333340", - "corners": [8, 8, 8, 8] - }, - "speaker": { - "type": "Text", - "parent": "panel", - "left": 190, "top": 520, - "text": "LERA KONSTANTIN", - "fontColor": "#c8d0e0", "fontSize": 16 - }, - "text-area": { - "type": "Rectangle", - "parent": "panel", - "left": 190, "top": 550, "width": 760, "height": 100, - "fillColor": "#2a3040", "strokeColor": "#333340", - "corners": [4, 4, 4, 4] - }, - "dialogue-text": { - "type": "Text", - "parent": "text-area", - "left": 200, "top": 560, "width": 740, - "text": "You look like you could use a drink. First time on the station?", - "fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true - }, - "btn-option1": { - "type": "Rectangle", - "parent": "panel", - "left": 190, "top": 670, "width": 370, "height": 30, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", - "corners": [4, 4, 4, 4] - }, - "btn-option1-label": { - "type": "Text", - "parent": "btn-option1", - "left": 200, "top": 674, - "text": "[1] Ask about the station", - "fontColor": "#c8d8f0", "fontSize": 12 - }, - "btn-option2": { - "type": "Rectangle", - "parent": "panel", - "left": 190, "top": 710, "width": 370, "height": 30, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", - "corners": [4, 4, 4, 4] - }, - "btn-option2-label": { - "type": "Text", - "parent": "btn-option2", - "left": 200, "top": 714, - "text": "[2] Ask about recent events", - "fontColor": "#c8d8f0", "fontSize": 12 - }, - "btn-leave": { - "type": "Rectangle", - "parent": "panel", - "left": 580, "top": 670, "width": 180, "height": 30, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", - "corners": [4, 4, 4, 4] - }, - "btn-leave-label": { - "type": "Text", - "parent": "btn-leave", - "left": 590, "top": 674, - "text": "[3] Leave", - "fontColor": "#c8d8f0", "fontSize": 12 - } - } -} -``` - ---- - -## 3. Menu Screen - -Full-screen menu with sidebar navigation and content area. - -```json -{ - "name": "Pause Menu", - "shapes": { - "bg": { - "type": "Rectangle", - "left": 0, "top": 0, "width": 1140, "height": 780, - "fillColor": "#1a1e24" - }, - "nav": { - "type": "Rectangle", - "parent": "bg", - "left": 20, "top": 20, "width": 200, "height": 740, - "fillColor": "#2a3040", "strokeColor": "#333340", - "corners": [4, 4, 4, 4] - }, - "btn-inventory": { - "type": "Rectangle", "parent": "nav", - "left": 30, "top": 40, "width": 180, "height": 36, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - }, - "btn-inventory-label": { - "type": "Text", "parent": "btn-inventory", - "left": 40, "top": 48, "text": "Inventory", - "fontColor": "#c8d8f0", "fontSize": 14 - }, - "btn-journal": { - "type": "Rectangle", "parent": "nav", - "left": 30, "top": 86, "width": 180, "height": 36, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - }, - "btn-journal-label": { - "type": "Text", "parent": "btn-journal", - "left": 40, "top": 94, "text": "Journal", - "fontColor": "#c8d8f0", "fontSize": 14 - }, - "btn-map": { - "type": "Rectangle", "parent": "nav", - "left": 30, "top": 132, "width": 180, "height": 36, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - }, - "btn-map-label": { - "type": "Text", "parent": "btn-map", - "left": 40, "top": 140, "text": "Map", - "fontColor": "#c8d8f0", "fontSize": 14 - }, - "btn-settings": { - "type": "Rectangle", "parent": "nav", - "left": 30, "top": 178, "width": 180, "height": 36, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - }, - "btn-settings-label": { - "type": "Text", "parent": "btn-settings", - "left": 40, "top": 186, "text": "Settings", - "fontColor": "#c8d8f0", "fontSize": 14 - }, - "btn-resume": { - "type": "Rectangle", "parent": "nav", - "left": 30, "top": 720, "width": 180, "height": 36, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - }, - "btn-resume-label": { - "type": "Text", "parent": "btn-resume", - "left": 40, "top": 728, "text": "Resume", - "fontColor": "#c8d8f0", "fontSize": 14 - }, - "content": { - "type": "Rectangle", - "parent": "bg", - "left": 240, "top": 20, "width": 880, "height": 740, - "fillColor": "#2a3040", "strokeColor": "#333340", - "corners": [4, 4, 4, 4] - }, - "content-label": { - "type": "Text", "parent": "content", - "left": 260, "top": 40, - "text": "Content area", - "fontColor": "#c8d0e0", "fontSize": 14 - } - } -} -``` - ---- - -## 4. Modal Dialog - -Centered overlay for confirmations, alerts, choices. - -```json -{ - "name": "Modal Dialog", - "shapes": { - "overlay": { - "type": "Rectangle", - "left": 0, "top": 0, "width": 1140, "height": 780, - "fillColor": "#0a0c10" - }, - "modal": { - "type": "Rectangle", - "parent": "overlay", - "left": 320, "top": 240, "width": 500, "height": 300, - "fillColor": "#1a1e24", "strokeColor": "#333340", - "corners": [8, 8, 8, 8] - }, - "title": { - "type": "Text", "parent": "modal", - "left": 340, "top": 260, - "text": "Confirm Action", - "fontColor": "#c8d0e0", "fontSize": 18 - }, - "divider": { - "type": "Line", "parent": "modal", - "left": 340, "top": 290, "width": 460, "height": 0, - "strokeColor": "#333340" - }, - "body-1": { - "type": "Text", "parent": "modal", - "left": 340, "top": 310, - "text": "Are you sure you want to proceed?", - "fontColor": "#c8d0e0", "fontSize": 14 - }, - "body-2": { - "type": "Text", "parent": "modal", - "left": 340, "top": 340, - "text": "This action cannot be undone.", - "fontColor": "#c8d0e0", "fontSize": 14 - }, - "btn-cancel": { - "type": "Rectangle", "parent": "modal", - "left": 480, "top": 480, "width": 120, "height": 36, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - }, - "btn-cancel-label": { - "type": "Text", "parent": "btn-cancel", - "left": 510, "top": 488, - "text": "Cancel", - "fontColor": "#c8d8f0", "fontSize": 14 - }, - "btn-confirm": { - "type": "Rectangle", "parent": "modal", - "left": 620, "top": 480, "width": 120, "height": 36, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - }, - "btn-confirm-label": { - "type": "Text", "parent": "btn-confirm", - "left": 645, "top": 488, - "text": "Confirm", - "fontColor": "#c8d8f0", "fontSize": 14 - } - } -} -``` - ---- - -## 5. List View - -Scrollable list with item selection and detail panel. - -```json -{ - "name": "List View", - "shapes": { - "list-panel": { - "type": "Rectangle", - "left": 20, "top": 20, "width": 400, "height": 740, - "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "item-1": { - "type": "Rectangle", "parent": "list-panel", - "left": 30, "top": 30, "width": 380, "height": 40, - "fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4] - }, - "item-1-label": { - "type": "Text", "parent": "item-1", - "left": 40, "top": 38, "text": "Item Alpha", - "fontColor": "#c8d8f0", "fontSize": 14 - }, - "item-2": { - "type": "Rectangle", "parent": "list-panel", - "left": 30, "top": 80, "width": 380, "height": 40, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "item-2-label": { - "type": "Text", "parent": "item-2", - "left": 40, "top": 88, "text": "Item Beta", - "fontColor": "#c8d0e0", "fontSize": 14 - }, - "item-3": { - "type": "Rectangle", "parent": "list-panel", - "left": 30, "top": 130, "width": 380, "height": 40, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "item-3-label": { - "type": "Text", "parent": "item-3", - "left": 40, "top": 138, "text": "Item Gamma", - "fontColor": "#c8d0e0", "fontSize": 14 - }, - "detail-panel": { - "type": "Rectangle", - "left": 440, "top": 20, "width": 680, "height": 740, - "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "detail-title": { - "type": "Text", "parent": "detail-panel", - "left": 460, "top": 40, - "text": "Item Alpha", - "fontColor": "#c8d0e0", "fontSize": 18 - }, - "detail-body": { - "type": "Text", "parent": "detail-panel", - "left": 460, "top": 80, "width": 640, - "text": "Description and properties appear here.", - "fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true - } - } -} -``` - ---- - -## 6. Inventory Grid - -Grid of cells for item management. - -```json -{ - "name": "Inventory Grid", - "shapes": { - "panel": { - "type": "Rectangle", - "left": 240, "top": 100, "width": 660, "height": 580, - "fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8] - }, - "title": { - "type": "Text", "parent": "panel", - "left": 260, "top": 120, - "text": "INVENTORY", - "fontColor": "#c8d0e0", "fontSize": 18 - }, - "cell-1-1": { - "type": "Rectangle", "parent": "panel", - "left": 260, "top": 160, "width": 64, "height": 64, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "cell-1-2": { - "type": "Rectangle", "parent": "panel", - "left": 332, "top": 160, "width": 64, "height": 64, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "cell-1-3": { - "type": "Rectangle", "parent": "panel", - "left": 404, "top": 160, "width": 64, "height": 64, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "cell-1-4": { - "type": "Rectangle", "parent": "panel", - "left": 476, "top": 160, "width": 64, "height": 64, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "cell-2-1": { - "type": "Rectangle", "parent": "panel", - "left": 260, "top": 232, "width": 64, "height": 64, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "cell-2-2": { - "type": "Rectangle", "parent": "panel", - "left": 332, "top": 232, "width": 64, "height": 64, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "cell-2-3": { - "type": "Rectangle", "parent": "panel", - "left": 404, "top": 232, "width": 64, "height": 64, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "cell-2-4": { - "type": "Rectangle", "parent": "panel", - "left": 476, "top": 232, "width": 64, "height": 64, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "detail": { - "type": "Rectangle", "parent": "panel", - "left": 580, "top": 160, "width": 300, "height": 400, - "fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4] - }, - "detail-title": { - "type": "Text", "parent": "detail", - "left": 600, "top": 180, - "text": "Selected Item Name", - "fontColor": "#c8d0e0", "fontSize": 16 - }, - "detail-body": { - "type": "Text", "parent": "detail", - "left": 600, "top": 210, "width": 260, - "text": "Item description and stats", - "fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true - } - } -} -``` diff --git a/.claude/skills/frame0-wireframe/references/setup-guide.md b/.claude/skills/frame0-wireframe/references/setup-guide.md deleted file mode 100644 index c0f56abd7..000000000 --- a/.claude/skills/frame0-wireframe/references/setup-guide.md +++ /dev/null @@ -1,53 +0,0 @@ -# Frame0 Setup Guide - -## Installation (Fedora) - -Download from https://frame0.app/download and install the RPM: - -```bash -sudo dnf install ./frame0-*.x86_64.rpm -``` - -Requires: Fedora 40 or later (x86_64). - -## Starting Frame0 - -Launch the desktop application: - -```bash -frame0 & -``` - -Frame0 exposes an HTTP API at `localhost:58320` when running. - -## Verify API Access - -```bash -.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health -``` - -Expected output: `Frame0 is running on port 58320` - -## Port Configuration - -Default port: **58320** - -To use a different port, set the environment variable: - -```bash -export FRAME0_PORT=58321 -``` - -Or pass `--port` to any script: - -```bash -.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh --port 58321 health -``` - -## Troubleshooting - -| Symptom | Cause | Fix | -|---------|-------|-----| -| "Connection refused" | Frame0 not running | Start the desktop app | -| "Port in use" | Another instance running | Close duplicate or use different port | -| Script hangs | API unresponsive | Restart Frame0 | diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh b/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh deleted file mode 100755 index 6fc23750d..000000000 --- a/.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh +++ /dev/null @@ -1,253 +0,0 @@ -#!/bin/bash -set -euo pipefail - -PORT="${FRAME0_PORT:-58320}" -ENDPOINT="http://localhost:${PORT}/execute_command" - -usage() { - cat < [args...] [--port N] - -Low-level Frame0 HTTP API wrapper. Replaces the MCP server with direct -curl calls. Requires Frame0 desktop app to be running. - -Commands: - health Check if Frame0 is running - exec Execute a raw API command - create-shape Create a shape (Rectangle, Ellipse, Text, Line) - get-shape Get shape details - update-shape Update shape properties - delete [id...] Delete shapes by ID - move Move a shape by pixel offset - duplicate Duplicate a shape - group [id...] Group shapes - ungroup Ungroup a group - create-connector [json-props] Connect two shapes - create-icon Create an icon shape - add-page Add a new page (becomes current) - get-page [page-id] Get current or specific page data - list-pages [--shapes] List all pages (--shapes for shape data) - current-page Get current page ID - set-page Set current page - export [page-id] [--format mime] Export page as image (default: image/png) - fit Fit view to screen - -Options: - --port N Frame0 API port (default: $PORT, env: FRAME0_PORT) - -Examples: - $(basename "$0") health - $(basename "$0") add-page "HUD Layout" - $(basename "$0") create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}' - $(basename "$0") list-pages - $(basename "$0") export --format image/png -EOF - exit 1 -} - -# Parse --port from anywhere in args -ARGS=() -while [[ $# -gt 0 ]]; do - case "$1" in - --port) PORT="$2"; ENDPOINT="http://localhost:${PORT}/execute_command"; shift 2 ;; - *) ARGS+=("$1"); shift ;; - esac -done -set -- "${ARGS[@]+"${ARGS[@]}"}" - -[[ $# -lt 1 ]] && usage - -# Execute a Frame0 API command, return data or error -frame0_exec() { - local command="$1" - local args - args="${2:-"{}"}" - - local response - response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \ - -H "Content-Type: application/json" \ - -d "{\"command\": \"$command\", \"args\": $args}" 2>&1) || { - echo "ERROR: Cannot connect to Frame0 at localhost:$PORT" >&2 - echo "Is Frame0 running? See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2 - return 1 - } - - local http_code body - http_code=$(echo "$response" | tail -1) - body=$(echo "$response" | sed '$d') - - if [[ "$http_code" != 2* ]]; then - echo "ERROR: HTTP $http_code from Frame0" >&2 - echo "$body" >&2 - return 1 - fi - - # Parse success/error from response - python3 -c " -import sys, json -try: - r = json.loads(sys.stdin.read()) - if r.get('success'): - d = r.get('data') - if d is not None: - print(json.dumps(d, indent=2)) - else: - print('ERROR: ' + str(r.get('error', 'Unknown error')), file=sys.stderr) - sys.exit(1) -except json.JSONDecodeError as e: - print(f'ERROR: Invalid JSON response: {e}', file=sys.stderr) - sys.exit(1) -" <<< "$body" -} - -# Build JSON array from remaining args -ids_to_json_array() { - local arr="[" - local first=true - for id in "$@"; do - [[ "$first" == true ]] && first=false || arr+="," - arr+="\"$id\"" - done - arr+="]" - echo "$arr" -} - -CMD="${1:-}" -shift || true - -case "$CMD" in - health) - if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null | grep -q "^[23]"; then - echo "Frame0 is running on port $PORT" - else - echo "Frame0 is NOT running on port $PORT" >&2 - echo "Start Frame0 desktop app, then retry." >&2 - echo "See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2 - exit 1 - fi - ;; - - exec) - [[ $# -lt 2 ]] && { echo "Usage: exec " >&2; exit 1; } - frame0_exec "$1" "$2" - ;; - - create-shape) - [[ $# -lt 2 ]] && { echo "Usage: create-shape " >&2; exit 1; } - local_type="$1" - local_props="$2" - local_parent="${3:-}" - local_parent_arg="" - [[ -n "$local_parent" ]] && local_parent_arg=", \"parentId\": \"$local_parent\"" - frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props$local_parent_arg, \"convertColors\": true}" - ;; - - get-shape) - [[ $# -lt 1 ]] && { echo "Usage: get-shape " >&2; exit 1; } - frame0_exec "shape:get-shape" "{\"shapeId\": \"$1\"}" - ;; - - update-shape) - [[ $# -lt 2 ]] && { echo "Usage: update-shape " >&2; exit 1; } - frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}" - ;; - - delete) - [[ $# -lt 1 ]] && { echo "Usage: delete [id...]" >&2; exit 1; } - local_arr=$(ids_to_json_array "$@") - frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}" - ;; - - move) - [[ $# -lt 3 ]] && { echo "Usage: move " >&2; exit 1; } - frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}" - ;; - - duplicate) - [[ $# -lt 1 ]] && { echo "Usage: duplicate [dx] [dy]" >&2; exit 1; } - local_dx="${2:-0}" - local_dy="${3:-0}" - frame0_exec "edit:duplicate" "{\"shapeIdArray\": [\"$1\"], \"dx\": $local_dx, \"dy\": $local_dy}" - ;; - - group) - [[ $# -lt 2 ]] && { echo "Usage: group [id...]" >&2; exit 1; } - local_arr=$(ids_to_json_array "$@") - frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}" - ;; - - ungroup) - [[ $# -lt 1 ]] && { echo "Usage: ungroup " >&2; exit 1; } - frame0_exec "shape:ungroup" "{\"shapeIdArray\": [\"$1\"]}" - ;; - - create-connector) - [[ $# -lt 2 ]] && { echo "Usage: create-connector [json-props]" >&2; exit 1; } - local_props="${3:-{}}" - frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props, \"convertColors\": true}" - ;; - - create-icon) - [[ $# -lt 2 ]] && { echo "Usage: create-icon " >&2; exit 1; } - frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}" - ;; - - add-page) - [[ $# -lt 1 ]] && { echo "Usage: add-page " >&2; exit 1; } - frame0_exec "page:add" "{\"pageProps\": {\"name\": \"$1\"}}" - ;; - - get-page) - if [[ $# -ge 1 ]]; then - frame0_exec "page:get" "{\"pageId\": \"$1\", \"exportShapes\": true}" - else - local_id - local_id=$(frame0_exec "page:get-current-page") - # Strip quotes from returned ID - local_id=$(echo "$local_id" | tr -d '"') - frame0_exec "page:get" "{\"pageId\": \"$local_id\", \"exportShapes\": true}" - fi - ;; - - list-pages) - local_shapes="false" - [[ "${1:-}" == "--shapes" ]] && local_shapes="true" - frame0_exec "doc:get" "{\"exportPages\": true, \"exportShapes\": $local_shapes}" - ;; - - current-page) - frame0_exec "page:get-current-page" - ;; - - set-page) - [[ $# -lt 1 ]] && { echo "Usage: set-page " >&2; exit 1; } - frame0_exec "page:set-current-page" "{\"pageId\": \"$1\"}" - ;; - - export) - local_page_id="" - local_format="image/png" - while [[ $# -gt 0 ]]; do - case "$1" in - --format) local_format="$2"; shift 2 ;; - *) local_page_id="$1"; shift ;; - esac - done - local_page_arg="" - [[ -n "$local_page_id" ]] && local_page_arg="\"pageId\": \"$local_page_id\", " - frame0_exec "file:export-image" "{${local_page_arg}\"format\": \"$local_format\", \"fillBackground\": true}" - ;; - - fit) - frame0_exec "view:fit-to-screen" - ;; - - --help|-h|help) - usage - ;; - - *) - echo "Unknown command: $CMD" >&2 - usage - ;; -esac diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh b/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh deleted file mode 100755 index d506858f0..000000000 --- a/.claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash -# Batch export wireframes from JSON to PNG via Frame0. -# -# Finds all .json wireframe files under docs/design/wireframes/ and exports -# each to a matching .png. Skips files whose PNG is already newer than the -# JSON, unless --force is passed. -# -# Usage: -# frame0-export-batch.sh [--dry-run] [--force] [--category CAT] [--root DIR] -# -# Options: -# --dry-run Print manifest only, don't touch Frame0. -# --force Re-export even if PNG already exists and is up to date. -# --category CAT Limit to one subdirectory (e.g. --category dialogue) -# --root DIR Wireframes root dir (default: docs/design/wireframes) -# -# Exit codes: -# 0 All exports succeeded (or nothing to do) -# 1 One or more exports failed - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" -SYNC="$SCRIPT_DIR/frame0-sync.py" -DEFAULT_ROOT="$REPO_ROOT/docs/design/wireframes" - -DRY_RUN=false -FORCE=false -CATEGORY="" -WF_ROOT="$DEFAULT_ROOT" - -# Parse arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run) DRY_RUN=true; shift ;; - --force) FORCE=true; shift ;; - --category) CATEGORY="$2"; shift 2 ;; - --root) WF_ROOT="$2"; shift 2 ;; - -h|--help) - sed -n '/^# /p' "$0" | sed 's/^# //' - exit 0 - ;; - *) echo "Unknown option: $1" >&2; exit 1 ;; - esac -done - -if [[ ! -d "$WF_ROOT" ]]; then - echo "ERROR: Wireframes directory not found: $WF_ROOT" >&2 - exit 1 -fi - -# Collect JSON files, optionally filtered by category subdirectory -mapfile -t JSON_FILES < <( - if [[ -n "$CATEGORY" ]]; then - find "$WF_ROOT/$CATEGORY" -name "*.json" ! -name ".*" | sort - else - find "$WF_ROOT" -name "*.json" ! -name ".*" | sort - fi -) - -if [[ ${#JSON_FILES[@]} -eq 0 ]]; then - echo "No wireframe JSON files found." - exit 0 -fi - -# Classify files into to-export and to-skip -TO_EXPORT=() -TO_SKIP=() - -for json in "${JSON_FILES[@]}"; do - png="${json%.json}.png" - if $FORCE || [[ ! -f "$png" ]] || [[ "$json" -nt "$png" ]]; then - TO_EXPORT+=("$json") - else - TO_SKIP+=("$json") - fi -done - -# Print manifest -if [[ ${#TO_EXPORT[@]} -gt 0 ]]; then - echo "" - echo "Will export (${#TO_EXPORT[@]} files):" - for json in "${TO_EXPORT[@]}"; do - png="${json%.json}.png" - rel="${json#$REPO_ROOT/}" - if [[ ! -f "$png" ]]; then - status="new" - else - status="updated" - fi - printf " [%-7s] %s\n" "$status" "$rel" - done -else - echo "" - echo "Nothing to export (all PNGs up to date)." -fi - -if [[ ${#TO_SKIP[@]} -gt 0 ]]; then - echo "" - echo "Will skip (${#TO_SKIP[@]} files already up to date):" - for json in "${TO_SKIP[@]}"; do - rel="${json#$REPO_ROOT/}" - printf " [skip ] %s\n" "$rel" - done -fi - -if $DRY_RUN; then - echo "" - echo "Dry run — no exports performed." - exit 0 -fi - -if [[ ${#TO_EXPORT[@]} -eq 0 ]]; then - exit 0 -fi - -echo "" -PASSED=0 -FAILED=0 -FAILED_FILES=() - -TOTAL=${#TO_EXPORT[@]} -IDX=0 - -for json in "${TO_EXPORT[@]}"; do - IDX=$((IDX + 1)) - png="${json%.json}.png" - rel="${json#$REPO_ROOT/}" - - printf "[%d/%d] %s ... " "$IDX" "$TOTAL" "$rel" - - output=$(python3 "$SYNC" export "$json" "$png" 2>/tmp/frame0-batch-err.txt) - rc=$? - if [[ $rc -eq 0 ]]; then - size=$(echo "$output" | tail -1 | grep -oP '\(\K[^)]+' || true) - echo "ok $size" - PASSED=$((PASSED + 1)) - else - echo "FAILED" - cat /tmp/frame0-batch-err.txt >&2 - FAILED=$((FAILED + 1)) - FAILED_FILES+=("$rel") - fi -done - -echo "" -echo "$PASSED exported, $FAILED failed." - -if [[ $FAILED -gt 0 ]]; then - echo "" - echo "Failed:" >&2 - for f in "${FAILED_FILES[@]}"; do - echo " $f" >&2 - done - exit 1 -fi - -exit 0 diff --git a/.claude/skills/frame0-wireframe/scripts/frame0-sync.py b/.claude/skills/frame0-wireframe/scripts/frame0-sync.py deleted file mode 100755 index df514c5a1..000000000 --- a/.claude/skills/frame0-wireframe/scripts/frame0-sync.py +++ /dev/null @@ -1,414 +0,0 @@ -#!/usr/bin/env python3 -"""Frame0 sync: push local JSON wireframes to Frame0, pull pages back. - -Local JSON is source of truth. Frame0 is a renderer. -A mapping file tracks local_id <-> frame0_id across push/pull cycles. - -Usage: - frame0-sync.py push [--port PORT] - frame0-sync.py pull [--port PORT] - frame0-sync.py export [--port PORT] [--format MIME] -""" - -import argparse -import json -import os -import sys -import urllib.request -import urllib.error - -DEFAULT_PORT = 58320 - - -def api(port, command, args=None): - """Execute a Frame0 API command. Returns the data field on success.""" - url = f"http://localhost:{port}/execute_command" - payload = json.dumps({"command": command, "args": args or {}}).encode() - req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}) - try: - with urllib.request.urlopen(req) as resp: - result = json.loads(resp.read()) - except urllib.error.HTTPError as e: - body = e.read().decode() - print(f"ERROR: {command}: HTTP {e.code}: {body[:500]}", file=sys.stderr) - sys.exit(1) - except urllib.error.URLError as e: - print(f"ERROR: Cannot connect to Frame0 on port {port}: {e}", file=sys.stderr) - sys.exit(1) - - if not result.get("success"): - print(f"ERROR: {command}: {result.get('error', 'unknown')}", file=sys.stderr) - sys.exit(1) - - return result.get("data") - - -# -- Mapping file management -------------------------------------------------- - -def mapping_path(wireframe_path): - """Derive the idmap path from the wireframe JSON path.""" - d = os.path.dirname(wireframe_path) - base = os.path.splitext(os.path.basename(wireframe_path))[0] - return os.path.join(d, f".{base}.idmap.json") - - -def load_mapping(wireframe_path): - p = mapping_path(wireframe_path) - if os.path.exists(p): - with open(p) as f: - return json.load(f) - return {"page_id": None, "shapes": {}, "connectors": {}} - - -def save_mapping(wireframe_path, mapping): - p = mapping_path(wireframe_path) - os.makedirs(os.path.dirname(p) or ".", exist_ok=True) - with open(p, "w") as f: - json.dump(mapping, f, indent=2) - f.write("\n") - - -# -- Push: local JSON -> Frame0 ----------------------------------------------- - -# Frame0 returns different type names from get vs what create accepts. -# Map internal types back to create-API types. -TYPE_TO_CREATE = { - "Box": "Rectangle", - "box": "Rectangle", -} - - -def topo_sort_shapes(shapes): - """Sort shape IDs so parents come before children.""" - order = [] - visited = set() - - def visit(sid): - if sid in visited: - return - visited.add(sid) - shape = shapes[sid] - parent = shape.get("parent") - if parent and parent in shapes: - visit(parent) - order.append(sid) - - for sid in shapes: - visit(sid) - return order - - -def find_or_create_page(port, name, mapping): - """Find existing page by mapping or name, or create a new one.""" - # Try mapped page_id first - if mapping.get("page_id"): - try: - page = api(port, "page:get", {"pageId": mapping["page_id"]}) - if page: - return mapping["page_id"] - except SystemExit: - pass # Page no longer exists, fall through - - # Search by name in existing pages - doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False}) - if doc and "children" in doc: - for page in doc["children"]: - if page.get("name") == name: - return page["id"] - - # Create new page - page = api(port, "page:add", {"pageProps": {"name": name}}) - return page["id"] - - -def clear_page(port, page_id): - """Delete all shapes on a page.""" - page = api(port, "page:get", {"pageId": page_id, "exportShapes": True}) - if not page or "children" not in page: - return - shape_ids = [s["id"] for s in page["children"]] - if shape_ids: - api(port, "edit:delete", {"shapeIdArray": shape_ids}) - - -def push(wireframe_path, port): - """Push local wireframe JSON to Frame0.""" - with open(wireframe_path) as f: - wireframe = json.load(f) - - name = wireframe.get("name", os.path.splitext(os.path.basename(wireframe_path))[0]) - shapes = wireframe.get("shapes", {}) - connectors = wireframe.get("connectors", {}) - - mapping = load_mapping(wireframe_path) - - # Find or create page - page_id = find_or_create_page(port, name, mapping) - mapping["page_id"] = page_id - - # Switch to page and clear it - api(port, "page:set-current-page", {"pageId": page_id}) - clear_page(port, page_id) - - # Reset ID mappings (shapes are recreated) - mapping["shapes"] = {} - mapping["connectors"] = {} - - # Create shapes in dependency order - order = topo_sort_shapes(shapes) - for local_id in order: - shape = shapes[local_id] - raw_type = shape.get("type", "Rectangle") - shape_type = TYPE_TO_CREATE.get(raw_type, raw_type) - parent_local = shape.get("parent") - - # Build shapeProps from everything except meta fields - meta_keys = {"type", "parent"} - props = {k: v for k, v in shape.items() if k not in meta_keys} - - # Set name to local_id if not explicitly set - if "name" not in props: - props["name"] = local_id - - create_args = { - "type": shape_type, - "shapeProps": props, - "convertColors": True, - } - - # Resolve parent ID - if parent_local and parent_local in mapping["shapes"]: - create_args["parentId"] = mapping["shapes"][parent_local] - - f0_id = api(port, "shape:create-shape", create_args) - mapping["shapes"][local_id] = f0_id - - # Create connectors - for local_id, conn in connectors.items(): - tail_local = conn.get("tailId") - head_local = conn.get("headId") - - if tail_local not in mapping["shapes"] or head_local not in mapping["shapes"]: - print(f"WARNING: connector '{local_id}' references unknown shape, skipping", file=sys.stderr) - continue - - meta_keys = {"tailId", "headId"} - props = {k: v for k, v in conn.items() if k not in meta_keys} - if "name" not in props: - props["name"] = local_id - - f0_id = api(port, "shape:create-connector", { - "tailId": mapping["shapes"][tail_local], - "headId": mapping["shapes"][head_local], - "shapeProps": props, - "convertColors": True, - }) - mapping["connectors"][local_id] = f0_id - - # Fit to screen - api(port, "view:fit-to-screen") - - save_mapping(wireframe_path, mapping) - total = len(mapping["shapes"]) + len(mapping["connectors"]) - print(f"Pushed '{name}' to Frame0: {len(mapping['shapes'])} shapes, {len(mapping['connectors'])} connectors") - - -# -- Pull: Frame0 -> local JSON ----------------------------------------------- - -def pull(page_ref, output_path, port): - """Pull a Frame0 page into local wireframe JSON.""" - # Resolve page_ref: could be an ID or a name - page_id = None - doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False}) - if doc and "children" in doc: - for page in doc["children"]: - if page["id"] == page_ref or page.get("name") == page_ref: - page_id = page["id"] - page_name = page.get("name", page_ref) - break - - if not page_id: - print(f"ERROR: Page not found: {page_ref}", file=sys.stderr) - sys.exit(1) - - # Load existing mapping for reverse lookup - mapping = load_mapping(output_path) - reverse_map = {v: k for k, v in mapping.get("shapes", {}).items()} - reverse_conn = {v: k for k, v in mapping.get("connectors", {}).items()} - - # Get full page with shapes - page = api(port, "page:get", {"pageId": page_id, "exportShapes": True}) - - shapes = {} - connectors = {} - new_mapping = {"page_id": page_id, "shapes": {}, "connectors": {}} - auto_id_counter = [0] - - def auto_id(f0_shape): - """Generate a stable local ID from shape name or auto-number.""" - # Prefer existing mapping - f0_id = f0_shape["id"] - if f0_id in reverse_map: - return reverse_map[f0_id] - # Use sanitized name - name = f0_shape.get("name", "") - if name: - sanitized = name.lower().replace(" ", "-").replace("_", "-") - if sanitized not in shapes: - return sanitized - # Fallback: auto-number - auto_id_counter[0] += 1 - return f"s{auto_id_counter[0]:03d}" - - def process_shape(f0_shape, parent_local_id=None): - f0_id = f0_shape["id"] - local_id = auto_id(f0_shape) - new_mapping["shapes"][local_id] = f0_id - - # Extract shape properties — only strip structural keys that our - # ID mapping replaces. Everything else passes through as-is so the - # local JSON speaks Frame0's native vocabulary. - shape_type = f0_shape.get("type", "Box") - skip_keys = {"id", "type", "children", "pageId", "parentId"} - props = {k: v for k, v in f0_shape.items() if k not in skip_keys and v is not None} - - entry = {"type": shape_type} - if parent_local_id: - entry["parent"] = parent_local_id - entry.update(props) - - # Remove name if it matches local_id (redundant) - if entry.get("name") == local_id: - del entry["name"] - - shapes[local_id] = entry - - # Process children recursively - for child in f0_shape.get("children", []): - child_type = child.get("type", "") - if child_type == "Connector": - process_connector(child) - else: - process_shape(child, local_id) - - def process_connector(f0_conn): - f0_id = f0_conn["id"] - local_id = reverse_conn.get(f0_id) - if not local_id: - auto_id_counter[0] += 1 - local_id = f"c{auto_id_counter[0]:03d}" - - new_mapping["connectors"][local_id] = f0_id - - tail_f0 = f0_conn.get("tail", {}).get("id") - head_f0 = f0_conn.get("head", {}).get("id") - - entry = {} - if tail_f0: - # Will be resolved after all shapes are processed - entry["_tailF0"] = tail_f0 - if head_f0: - entry["_headF0"] = head_f0 - - skip_keys = {"id", "type", "children", "pageId", "tail", "head"} - props = {k: v for k, v in f0_conn.items() if k not in skip_keys and v is not None} - entry.update(props) - - connectors[local_id] = entry - - # Process all top-level shapes - for child in page.get("children", []): - child_type = child.get("type", "") - if child_type == "Connector": - process_connector(child) - else: - process_shape(child) - - # Resolve connector references to local IDs - f0_to_local = {v: k for k, v in new_mapping["shapes"].items()} - for conn in connectors.values(): - tail_f0 = conn.pop("_tailF0", None) - head_f0 = conn.pop("_headF0", None) - if tail_f0 and tail_f0 in f0_to_local: - conn["tailId"] = f0_to_local[tail_f0] - if head_f0 and head_f0 in f0_to_local: - conn["headId"] = f0_to_local[head_f0] - - wireframe = {"name": page_name} - if shapes: - wireframe["shapes"] = shapes - if connectors: - wireframe["connectors"] = connectors - - os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) - with open(output_path, "w") as f: - json.dump(wireframe, f, indent=2) - f.write("\n") - - save_mapping(output_path, new_mapping) - print(f"Pulled '{page_name}' -> {output_path}: {len(shapes)} shapes, {len(connectors)} connectors") - - -# -- Export: push then export as image ----------------------------------------- - -def export_image(wireframe_path, output_path, port, fmt="image/png"): - """Push wireframe to Frame0 and export the page as an image.""" - import base64 - - # Push first to ensure Frame0 is up to date - push(wireframe_path, port) - - mapping = load_mapping(wireframe_path) - page_id = mapping.get("page_id") - if not page_id: - print("ERROR: No page_id in mapping after push", file=sys.stderr) - sys.exit(1) - - image_b64 = api(port, "file:export-image", { - "pageId": page_id, - "format": fmt, - "fillBackground": True, - }) - - image_bytes = base64.b64decode(image_b64) - with open(output_path, "wb") as f: - f.write(image_bytes) - - print(f"Exported: {output_path} ({len(image_bytes) // 1024}KB)") - - -# -- CLI ----------------------------------------------------------------------- - -def main(): - parser = argparse.ArgumentParser(description="Sync wireframe JSON with Frame0") - parser.add_argument("--port", type=int, default=int(os.environ.get("FRAME0_PORT", DEFAULT_PORT))) - sub = parser.add_subparsers(dest="command") - - p_push = sub.add_parser("push", help="Push local JSON to Frame0") - p_push.add_argument("wireframe", help="Path to wireframe .json file") - - p_pull = sub.add_parser("pull", help="Pull Frame0 page to local JSON") - p_pull.add_argument("page", help="Page ID or page name") - p_pull.add_argument("output", help="Output .json path") - - p_export = sub.add_parser("export", help="Push and export as image") - p_export.add_argument("wireframe", help="Path to wireframe .json file") - p_export.add_argument("output", help="Output image path (e.g. wireframe.png)") - p_export.add_argument("--format", default="image/png", - help="Export MIME type (default: image/png)") - - args = parser.parse_args() - - if args.command == "push": - push(args.wireframe, args.port) - elif args.command == "pull": - pull(args.page, args.output, args.port) - elif args.command == "export": - export_image(args.wireframe, args.output, args.port, args.format) - else: - parser.print_help() - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/.claude/skills/glb-gen/SKILL.md b/.claude/skills/glb-gen/SKILL.md index d971c6a45..cc41c47e2 100644 --- a/.claude/skills/glb-gen/SKILL.md +++ b/.claude/skills/glb-gen/SKILL.md @@ -18,7 +18,7 @@ Convert approved concept images to game-ready .glb models. | Service | Check | |---------|-------| | Trellis | `tooling/db/trellis_connector.py health` | -| Blender | `flatpak run org.blender.Blender --version` | +| Blender | `tooling/blender --version` | Trellis runs on tower-of-joy and may be switched off. Check before batching. @@ -41,54 +41,35 @@ python3 tooling/db/trellis_connector.py generate input.png \ ## Batch Usage -For multiple models, use the batch script: +`tooling/trellis-batch.sh` exists but is **hardcoded to one category**: it +takes no arguments and always processes the 16 character body types from +`.tmp/image-gen/characters/bodies` into `.tmp/glb-gen/characters/bodies`. +Running it for any other asset category (furniture, props, etc.) does +nothing useful — it will just re-run (or skip, if outputs already exist) the +same 16 character bodies regardless of what you intended. + +For any other category, loop `trellis_connector.py` calls yourself with a +cooldown between jobs: ```bash -bash tooling/trellis-batch.sh +for f in .tmp/image-gen/furniture/tables/*.png; do + name=$(basename "$f" .png) + python3 tooling/db/trellis_connector.py generate "$f" \ + --output ".tmp/glb-gen/furniture/tables/${name}.glb" \ + --simplify 0.95 --texture-size 1024 + sleep 15 +done ``` -The batch script runs one model at a time with 15s cooldown between jobs, -retries up to 3 times on failure, and skips already-generated GLBs. - **Never run Trellis jobs in parallel** — it uses the full GPU and concurrent jobs will OOM and corrupt the CUDA state. ## Trellis API Reference -The connector talks to a Gradio API. The parameter layout is fragile — document -changes here when the container is updated. - -### /image_to_3d — 9 inputs - -| Pos | Name | Type | Value | -|-----|------|------|-------| -| 0 | image | Image | preprocessed image from /preprocess_image_1 | -| 1 | multiimages | Gallery | `[]` for single-image mode | -| 2 | is_multiimage | State | `False` for single-image (**not a session — it's a boolean flag**) | -| 3 | seed | Slider | int, 0-2147483647 | -| 4 | ss_guidance | Slider | float, default 7.5 | -| 5 | ss_steps | Slider | int, default 12 | -| 6 | slat_guidance | Slider | float, default 3.0 | -| 7 | slat_steps | Slider | int, default 12 | -| 8 | multiimage_algo | Radio | `"stochastic"` or `"multidiffusion"` | - -### /extract_glb — 3 inputs - -| Pos | Name | Type | Value | -|-----|------|------|-------| -| 0 | output_buf | State | `None` — server uses internal state from image_to_3d | -| 1 | simplify | Slider | float, mesh simplification ratio (default 0.95) | -| 2 | texture_size | Slider | int, texture resolution (default 1024) | - -### Common Failure Modes - -| Error | Cause | Fix | -|-------|-------|-----| -| "needed 9, got 8" on image_to_3d | Missing `is_multiimage` at position 2 | Pass `False` | -| "needed 3, got 2" on extract_glb | Missing `output_buf` at position 0 | Pass `None` | -| `'float' cannot be interpreted as int` | numpy rejects float for linspace steps | Patch `flow_euler.py` on server: `int(steps)` | -| CUDA device mismatch / invalid argument | GPU state corrupted after crash | Restart the Trellis container | -| Repeated 500 after restart | Previous crash left tensors on wrong device | Full container restart (not just API restart) | +The connector talks to a fragile Gradio API (positional args, no field +names). Full parameter tables and common failure modes are in +`references/trellis-api.md` — read it when a Trellis call fails with a +position/count mismatch or a CUDA/device error. ## Intermediate and Output Directories @@ -112,7 +93,13 @@ Always mirror the category/subcategory path across stages so you can trace `image-gen/furniture/tables/foo_concept.png` → `glb-gen/furniture/tables/foo.glb`. Final game-ready assets are copied to `spikes/3dpipeline/models/` for spike -testing, or to `client/assets/models/` when ready for production. +testing. For production, character GLBs go to `client/assets/characters/` +(already in active use — see its `bodies/`, `clothing/`, `hair/`, +`skeleton/`, etc. subdirectories). `client/assets/models/` does not exist — +there is no established production path yet for non-character categories +(furniture, props); that's a Phase-5 concern (Player control & in-world +rendering, per CLAUDE.md's Development Cascade). Until then, keep +non-character output staged under `.tmp/glb-gen/postproc/`. ## Post-process in Blender @@ -125,7 +112,7 @@ Normalize scale, center, generate recolor mask, adjust materials: Or directly: ```bash -flatpak run org.blender.Blender --background \ +tooling/blender --background \ --python .claude/skills/glb-gen/scripts/postprocess_glb.py \ -- input.glb output.glb [--target-width N] [--color-threshold N] ``` @@ -139,7 +126,10 @@ For best Trellis results: - No text, labels, or watermarks - 3/4 front view at eye level gives Trellis the most information - **Do NOT force isometric angle** — Trellis reconstructs full 3D, the game camera handles the view -- Use a style anchor image (`--input`) to maintain consistency across batches +- To maintain consistency across a batch, generate the concept images with + `/image-gen` using its `--input` style anchor flag — `--input` is an + /image-gen flag, not a Trellis one; `trellis_connector.py` has no `--input` + argument and exits on unrecognized flags. These match `/image-gen` output with the Settled Reach style guide. @@ -159,6 +149,16 @@ Set in `project.godot` under `[gltf]` so new imports pick it up automatically. ## Material Convention -Post-processed .glb files use standard material slot names: -`mat_wood_primary`, `mat_metal_primary`, `mat_fabric_primary`, etc. -See `docs/design/character-visuals-spec.md` §5 for character materials. +`scripts/postprocess_glb.py` renames **every** material slot on the imported +mesh to a single slot, `mat_primary` by default (override with +`--material-name NAME`) — it does not produce per-material names like +`mat_wood_primary`/`mat_metal_primary`/`mat_fabric_primary`; those names are +not used anywhere in this pipeline. The script also keeps the original +Trellis-baked texture and sets roughness=1.0/specular=0.0 for toon +compatibility (see `setup_materials()`). + +Note `docs/design/character-visuals-spec.md` §5 ("Color Mesh Regions") is a +**different, unrelated system**: it defines 2D compositor color regions for +the character-visuals rendering pipeline (`skin_primary`, `hair_primary`, +`cloth_primary`/`cloth_secondary`/`cloth_accent`, `accessory_primary`, etc.), +not GLB material slot names — do not conflate the two when naming things. diff --git a/.claude/skills/glb-gen/references/trellis-api.md b/.claude/skills/glb-gen/references/trellis-api.md new file mode 100644 index 000000000..85830de50 --- /dev/null +++ b/.claude/skills/glb-gen/references/trellis-api.md @@ -0,0 +1,40 @@ +# Trellis API Reference + +The connector (`tooling/db/trellis_connector.py`) talks to a Gradio API. The +parameter layout is fragile — document changes here when the container is +updated. This is the canonical copy; the connector's module docstring carries +a duplicate for at-a-glance reference when reading the script directly — keep +both in sync if you change either, but treat this file as the source of truth +for the skill. + +## /image_to_3d — 9 inputs + +| Pos | Name | Type | Value | +|-----|------|------|-------| +| 0 | image | Image | preprocessed image from /preprocess_image_1 | +| 1 | multiimages | Gallery | `[]` for single-image mode | +| 2 | is_multiimage | State | `False` for single-image (**not a session — it's a boolean flag**) | +| 3 | seed | Slider | int, 0-2147483647 | +| 4 | ss_guidance | Slider | float, default 7.5 | +| 5 | ss_steps | Slider | int, default 12 | +| 6 | slat_guidance | Slider | float, default 3.0 | +| 7 | slat_steps | Slider | int, default 12 | +| 8 | multiimage_algo | Radio | `"stochastic"` or `"multidiffusion"` | + +## /extract_glb — 3 inputs + +| Pos | Name | Type | Value | +|-----|------|------|-------| +| 0 | output_buf | State | `None` — server uses internal state from image_to_3d | +| 1 | simplify | Slider | float, mesh simplification ratio (default 0.95) | +| 2 | texture_size | Slider | int, texture resolution (default 1024) | + +## Common Failure Modes + +| Error | Cause | Fix | +|-------|-------|-----| +| "needed 9, got 8" on image_to_3d | Missing `is_multiimage` at position 2 | Pass `False` | +| "needed 3, got 2" on extract_glb | Missing `output_buf` at position 0 | Pass `None` | +| `'float' cannot be interpreted as int` | numpy rejects float for linspace steps | Patch `flow_euler.py` on server: `int(steps)` | +| CUDA device mismatch / invalid argument | GPU state corrupted after crash | Restart the Trellis container | +| Repeated 500 after restart | Previous crash left tensors on wrong device | Full container restart (not just API restart) | diff --git a/.claude/skills/glb-gen/scripts/postprocess b/.claude/skills/glb-gen/scripts/postprocess index a6d48929a..235818d30 100755 --- a/.claude/skills/glb-gen/scripts/postprocess +++ b/.claude/skills/glb-gen/scripts/postprocess @@ -68,10 +68,28 @@ echo "Post-processing: $(basename "$INPUT_ABS")" echo " Input: $INPUT_REL" echo " Output: ${OUTPUT_ABS#$(cd "$PROJECT_ROOT" && pwd)/}" -flatpak run org.blender.Blender --background \ +LOG="${OUTPUT_ABS%.glb}_blender.log" + +# Blender does not reliably set a non-zero exit code on an uncaught Python +# exception (--python-exit-code is not set), so failures must be detected +# from the log content and from whether the output file actually appeared — +# not from Blender's own exit status alone. Capture the full log first +# (temporarily disabling -e so a non-zero Blender exit doesn't abort before +# we can inspect it), then filter for the console summary. +set +e +"$PROJECT_ROOT/tooling/blender" --background \ --python "$BLENDER_SCRIPT" \ - -- "$INPUT_ABS" "$OUTPUT_ABS" "$@" 2>&1 \ - | grep -E "^ |^=|WARNING|ERROR|Dominant|Mask" + -- "$INPUT_ABS" "$OUTPUT_ABS" "$@" > "$LOG" 2>&1 +BLENDER_EXIT=$? +set -e + +grep -iE "^ |^=|warning|error|traceback|dominant|mask" "$LOG" || true + +if [ "$BLENDER_EXIT" -ne 0 ] || [ ! -f "$OUTPUT_ABS" ]; then + echo "ERROR: Blender post-process failed (exit $BLENDER_EXIT, output present: $([ -f "$OUTPUT_ABS" ] && echo yes || echo no))" >&2 + echo " Full log: ${LOG#$(cd "$PROJECT_ROOT" && pwd)/}" >&2 + exit 1 +fi MASK="${OUTPUT_ABS%.glb}_mask.png" if [ -f "$MASK" ]; then diff --git a/.claude/skills/glb-gen/scripts/postprocess_glb.py b/.claude/skills/glb-gen/scripts/postprocess_glb.py index a238e930d..a0c6a5a7c 100644 --- a/.claude/skills/glb-gen/scripts/postprocess_glb.py +++ b/.claude/skills/glb-gen/scripts/postprocess_glb.py @@ -3,7 +3,7 @@ GLB post-processor for The Settled Reach asset pipeline. Run via Blender headless: - flatpak run org.blender.Blender --background --python postprocess_glb.py -- input.glb output.glb [options] + tooling/blender --background --python postprocess_glb.py -- input.glb output.glb [options] Operations: 1. Normalize scale to fit a target bounding box (default 1x1x1 world units) @@ -42,7 +42,7 @@ def get_script_args(): def parse_args(args): """Parse script arguments.""" if len(args) < 2: - print("Usage: postprocess_glb.py -- input.glb output.glb [--target-width N] [--color #hex] [--material-name name]") + print("Usage: postprocess_glb.py -- input.glb output.glb [--target-width N] [--target-height N] [--color-threshold N] [--material-name name]") sys.exit(1) result = { diff --git a/.claude/skills/image-gen/SKILL.md b/.claude/skills/image-gen/SKILL.md index 2f615dd80..5e61e6217 100644 --- a/.claude/skills/image-gen/SKILL.md +++ b/.claude/skills/image-gen/SKILL.md @@ -10,21 +10,27 @@ description: > # Image Generation — The Settled Reach -Generate concept images via the Gemini API. Standalone skill — does not chain -into other pipelines. Output is a PNG file. +Generate concept images via the Gemini API. Does not auto-invoke downstream +skills — /glb-gen consumes its output from `.tmp/image-gen/` when you choose +to feed a concept image into the 3D pipeline, but that's a manual next step, +not something this skill triggers itself. Output is a PNG file. ## Prerequisites Requires `GEMINI_API_KEY` in environment (set in `.claude/settings.local.json`). +Uses the canonical connector at `tooling/db/image_connector.py` (see +`.claude/rules/project-structure.md` — `tooling/db/` is the documented home +for asset/connector scripts; this skill has no local fork of it). + ```bash -python3 .claude/skills/image-gen/scripts/image_connector.py health +python3 tooling/db/image_connector.py health ``` ## Usage ```bash -python3 .claude/skills/image-gen/scripts/image_connector.py generate \ +python3 tooling/db/image_connector.py generate \ "prompt text" \ --output path/to/output.png \ --aspect 1:1 @@ -53,8 +59,8 @@ Always use `--output .tmp/image-gen/[category]/[subcategory]/[name]_concept.png` | Flag | Default | Description | |------|---------|-------------| | `--output` | auto-named in `~/Pictures/mcp-images/` | Output PNG path (use `.tmp/image-gen/...` instead) | -| `--aspect` | `1:1` | Aspect ratio (1:1, 16:9, 3:2, 4:3, etc.) | -| `--size` | standard | Resolution hint (1K, 2K, 4K) | +| `--aspect` | `1:1` | Real API parameter (`generationConfig.imageConfig.aspectRatio`). Valid: `1:1`, `3:2`, `2:3`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9` | +| `--size` | standard | Resolution **hint** only — appended to the prompt text, not a real API parameter; Gemini may not honor it | | `--input` | none | Input image for image-to-image variation | ## Art Style Guide — The Settled Reach @@ -81,7 +87,8 @@ For concept images intended as Trellis input (3D model generation): ## Batch Usage Generate multiple variants by running the script multiple times with different -prompts or seeds. Each call is independent — no state between calls. +prompts. Each call is independent — no state between calls, and there is no +`--seed` flag (the connector rejects unknown arguments). Show generated images to the user for review before proceeding to any downstream pipeline (e.g. /glb-gen). diff --git a/.claude/skills/image-gen/scripts/image_connector.py b/.claude/skills/image-gen/scripts/image_connector.py deleted file mode 100755 index 6c0884991..000000000 --- a/.claude/skills/image-gen/scripts/image_connector.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python3 -""" -Gemini image generator connector — direct API wrapper. - -Generates images via Google's Gemini 2.0 Flash image generation API. -API key from GEMINI_API_KEY env var or config.json. - -Usage: - python3 image_connector.py health - python3 image_connector.py generate "prompt" [--output file.png] [--aspect 1:1] [--size 1K] [--input image.png] -""" - -import base64 -import json -import os -import sys -import urllib.error -import urllib.request - -CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json") -DEFAULT_OUTPUT_DIR = os.path.expanduser("~/Pictures/mcp-images") - - -def get_api_key(): - """Get Gemini API key from env or config.""" - key = os.environ.get("GEMINI_API_KEY") - if key: - return key - try: - with open(CONFIG_PATH) as f: - config = json.load(f) - return config.get("gemini_api_key", "") - except Exception: - pass - print(json.dumps({ - "ok": False, - "error": "No GEMINI_API_KEY found in environment or config.json" - }, indent=2)) - sys.exit(1) - - -def health(): - """Check if the Gemini API is reachable with the configured key.""" - key = get_api_key() - url = f"https://generativelanguage.googleapis.com/v1beta/models?key={key}" - try: - req = urllib.request.Request(url, method="GET") - with urllib.request.urlopen(req, timeout=10) as resp: - data = json.loads(resp.read()) - models = [m.get("name", "") for m in data.get("models", []) - if "imagen" in m.get("name", "").lower() - or "flash" in m.get("name", "").lower()] - print(json.dumps({ - "ok": True, - "api": "gemini", - "image_capable_models": models[:5], - }, indent=2)) - except Exception as e: - print(json.dumps({ - "ok": False, - "error": str(e) - }, indent=2)) - sys.exit(1) - - -def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, - input_image=None): - """ - Generate an image from a text prompt using Gemini. - - Args: - prompt: Text description of the image to generate - output: Output file path (default: auto-named in ~/Pictures/mcp-images/) - aspect_ratio: Aspect ratio (1:1, 16:9, 3:2, etc.) - image_size: Resolution hint (1K, 2K, 4K) - may not be honored - input_image: Optional input image path for image-to-image generation - """ - key = get_api_key() - - # Gemini image generation model - model = "gemini-2.5-flash-image" - url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}" - - if output is None: - safe = "".join(c if c.isalnum() or c in "-_ " else "" for c in prompt[:40]) - safe = safe.strip().replace(" ", "_").lower() - os.makedirs(DEFAULT_OUTPUT_DIR, exist_ok=True) - output = os.path.join(DEFAULT_OUTPUT_DIR, f"{safe}.png") - - # Build the request - parts = [] - - # Add input image if provided (image-to-image) - if input_image: - if not os.path.isfile(input_image): - print(json.dumps({"ok": False, "error": f"Input image not found: {input_image}"}), indent=2) - sys.exit(1) - with open(input_image, "rb") as f: - image_data = base64.b64encode(f.read()).decode("utf-8") - # Detect mime type - ext = os.path.splitext(input_image)[1].lower() - mime = {"png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", - ".webp": "image/webp"}.get(ext, "image/png") - parts.append({ - "inlineData": { - "mimeType": mime, - "data": image_data - } - }) - - # Build enhanced prompt with aspect ratio and size hints - enhanced_prompt = prompt - if aspect_ratio and aspect_ratio != "1:1": - enhanced_prompt += f" Aspect ratio: {aspect_ratio}." - if image_size: - enhanced_prompt += f" Resolution: {image_size}." - - parts.append({"text": enhanced_prompt}) - - payload = json.dumps({ - "contents": [{"parts": parts}], - "generationConfig": { - "responseModalities": ["TEXT", "IMAGE"], - } - }) - - req = urllib.request.Request( - url, - data=payload.encode(), - headers={"Content-Type": "application/json"}, - method="POST" - ) - - print(f"Generating image...", file=sys.stderr) - print(f" Prompt: {prompt}", file=sys.stderr) - if input_image: - print(f" Input image: {input_image}", file=sys.stderr) - - try: - with urllib.request.urlopen(req, timeout=120) as resp: - result = json.loads(resp.read()) - except urllib.error.HTTPError as e: - body = e.read().decode("utf-8", errors="replace") - print(json.dumps({ - "ok": False, - "error": f"API error {e.code}: {e.reason}", - "details": body[:500] - }, indent=2)) - sys.exit(1) - except Exception as e: - print(json.dumps({"ok": False, "error": str(e)}), indent=2) - sys.exit(1) - - # Extract image data from response - candidates = result.get("candidates", []) - if not candidates: - print(json.dumps({ - "ok": False, - "error": "No candidates in response", - "response": json.dumps(result)[:500] - }, indent=2)) - sys.exit(1) - - image_saved = False - text_response = "" - - for candidate in candidates: - content = candidate.get("content", {}) - for part in content.get("parts", []): - if "inlineData" in part: - # Image data - image_b64 = part["inlineData"]["data"] - image_bytes = base64.b64decode(image_b64) - os.makedirs(os.path.dirname(os.path.abspath(output)), exist_ok=True) - with open(output, "wb") as f: - f.write(image_bytes) - image_saved = True - elif "text" in part: - text_response += part["text"] - - if not image_saved: - print(json.dumps({ - "ok": False, - "error": "No image data in response", - "text_response": text_response[:500], - "response": json.dumps(result)[:500] - }, indent=2)) - sys.exit(1) - - file_size = os.path.getsize(output) - print(json.dumps({ - "ok": True, - "file": output, - "size_bytes": file_size, - "prompt": prompt, - "aspect_ratio": aspect_ratio, - }, indent=2)) - - -def main(): - if len(sys.argv) < 2: - print("Usage:") - print(" image_connector.py health") - print(" image_connector.py generate 'prompt' [--output file.png] [--aspect 1:1] [--size 1K] [--input image.png]") - sys.exit(1) - - cmd = sys.argv[1] - - if cmd == "health": - health() - elif cmd == "generate": - if len(sys.argv) < 3: - print("Error: prompt required", file=sys.stderr) - sys.exit(1) - - prompt = sys.argv[2] - output = None - aspect_ratio = "1:1" - image_size = None - input_image = None - - i = 3 - while i < len(sys.argv): - if sys.argv[i] == "--output" and i + 1 < len(sys.argv): - output = sys.argv[i + 1] - i += 2 - elif sys.argv[i] == "--aspect" and i + 1 < len(sys.argv): - aspect_ratio = sys.argv[i + 1] - i += 2 - elif sys.argv[i] == "--size" and i + 1 < len(sys.argv): - image_size = sys.argv[i + 1] - i += 2 - elif sys.argv[i] == "--input" and i + 1 < len(sys.argv): - input_image = sys.argv[i + 1] - i += 2 - else: - print(f"Unknown argument: {sys.argv[i]}", file=sys.stderr) - sys.exit(1) - - generate(prompt, output=output, aspect_ratio=aspect_ratio, - image_size=image_size, input_image=input_image) - else: - print(f"Unknown command: {cmd}", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/.claude/skills/pr-process/SKILL.md b/.claude/skills/pr-process/SKILL.md index 900534be9..ee79b3310 100644 --- a/.claude/skills/pr-process/SKILL.md +++ b/.claude/skills/pr-process/SKILL.md @@ -27,14 +27,14 @@ current branch — never touches main. ### 0. Dry-run mode check -If the user invokes `/pr-push --dry-run`: +If the user invokes `/pr-process --dry-run`: - Print: "Dry-run mode — inspecting state, nothing will be pushed or committed." - Run steps 1 through 4a in **inspect-only** mode: - Step 4: run `make check-systems-db` to check current stamp freshness (no merge) - Step 4a: report which watched files changed vs origin/main; show whether `make regen-db` would be triggered; do NOT run the regen, stage, or commit - Print a summary: watched files changed (list), regen needed (yes/no), DB stamp fresh (yes/no) -- Print "Dry run complete — use /pr-push to apply." +- Print "Dry run complete — use /pr-process to apply." - Stop. Do not push or create a PR. --- @@ -55,8 +55,8 @@ step (1b, 1c), check for long-lived Godot processes from prior stuck test runs: ```bash -# List any godot/gdunit processes running longer than 5 minutes -ps -eo pid,etimes,cmd | awk '$2 > 300 && /godot.*gdunit4-run/ {print $1, $2"s", substr($0, index($0,$3))}' +# List any godot/gdUnit4 processes running longer than 5 minutes +ps -eo pid,etimes,cmd | awk '$2 > 300 && /godot.*(GdUnitCmdTool|[Gg]d[Uu]nit)/ {print $1, $2"s", substr($0, index($0,$3))}' ``` If any are listed: they are almost certainly orphans from a prior test @@ -85,9 +85,12 @@ ignore` comment with a reason. **For server branches:** ```bash -cargo clippy -- -D warnings 2>&1 +cargo clippy --manifest-path server/Cargo.toml --all-targets -- -D warnings 2>&1 ``` +This must match the pre-push gate's own invocation exactly (`--all-targets` is +easy to drop locally and then miss warnings the gate still catches). + **For CI/tooling branches:** ```bash ruff check tooling/ 2>&1 @@ -105,62 +108,41 @@ bugs (parse errors, depth sorting, scene tree failures). **For client/visual branches:** -First, **wipe the script class cache before parsing**. Sprint 36 close -caught this: the team added a new `class_name MetaScreen` base class and -six scripts extending it. Warm cache on developer machines parsed fine, -but CI / fresh clones / post-merge parses hit `Could not find base class -"MetaScreen"` because the autoload-vs-class_name registration order only -resolves correctly once the class cache is seeded. Wiping the cache here -(client-side, before push) simulates the cold-start path and catches the -bug locally — keeping the pre-push hook fast. +`tooling/godot-cold-parse` wipes the cached script-class registry (matching +the cold-start ordering CI / fresh clones see — Sprint 36 close caught a +`class_name` base-class registration bug that warm caches masked) and runs a +headless parse, filtering known pre-existing noise. Pass `--run-menu` if the +branch has UI changes to also launch the main menu briefly: ```bash -# Cold-cache parse check. Deleting the cached class registry forces -# Godot to rebuild it from source on the next parse, matching the -# cold-start ordering CI and fresh clones see. -rm -f client/.godot/global_script_class_cache.cfg - -# Headless parse + scanner check. Godot's resource scanner emits -# category errors (e.g. "Export type can only be built-in, a resource, -# a node, or an enum" for @export on a RefCounted) that do NOT always -# prefix with SCRIPT ERROR — they appear as plain ERROR lines. Widen -# the grep to catch both, then filter known pre-existing noise from -# the autoload class_name parse-order trap (documented in CLAUDE.md). -godot --headless --path client --quit 2>&1 | \ - grep -iE "^(SCRIPT )?ERROR|Parse Error|Export type" | \ - grep -v "Failed loading resource: res://assets" | \ - grep -v "Cannot infer the type" | \ - grep -vE "(Messagepack|LocalBridge|ServerProcess|Constants)\" not declared" - -# If the branch has UI changes, also run the game briefly: -timeout 10 godot --path client res://scenes/main_menu.tscn 2>&1 | \ - grep -iE "^(SCRIPT )?ERROR|Parse Error|Export type" +tooling/godot-cold-parse --run-menu ``` -If the cold parse reports a "Could not find base class X" error, the fix -is almost always an autoload-order issue (see `CLAUDE.md` → GDScript -conventions → Autoload parse-order rule). Rebuilding the cache with -`godot --editor --headless --quit` will mask it locally but the same error -will re-surface post-merge — fix the actual ordering problem, don't paper -over it with a cache rebuild. +If it reports a "Could not find base class X" error, the fix is almost +always an autoload-order issue (see `CLAUDE.md` → GDScript conventions → +Autoload parse-order rule) — fix the ordering, don't just rebuild the cache +to mask it locally (the same error resurfaces post-merge). -Any lines that come through the filter represent new errors introduced -by this branch. Fix them before pushing — Sprint 36 shipped commit -`84105916` with an `@export var descriptor: CharacterVisualDescriptor` -scanner error that the old narrower grep missed; Tyre caught it five -commits later during W6 review. +Any lines it reports are new errors introduced by this branch. Fix them +before pushing. **For server branches:** ```bash -cd server && cargo test --lib 2>&1 +cargo test --manifest-path server/Cargo.toml 2>&1 ``` +Full suite, not `--lib` — `--lib` skips the believability/derivation golden +harnesses (separate test binaries) and would silently under-test cascade +changes. This must match the pre-push gate's own invocation (`team-patterns.md`) +exactly; a weaker local pass here just gives false confidence right before the +gate catches it anyway. + If any errors are found, **stop and fix them before pushing**. Do not push broken code for reviewers to find — that wastes everyone's time. If the branch includes visual changes (character creation, UI, rendering), the team should have manually launched the game and verified the change -works on screen before invoking `/pr-push`. If they haven't, ask: +works on screen before invoking `/pr-process`. If they haven't, ask: "Have you run `make game` and verified this works visually?" ### 2. Commit uncommitted changes @@ -171,9 +153,8 @@ git diff --stat ``` **Run both commands from the repo root** (`git rev-parse --show-toplevel`). -Running from a subdirectory can cause paths to not resolve, hiding real -changes — Sprint 30 proved this when `git diff HEAD -- server/src/bin/atlas.rs` -returned 0 lines from the wrong CWD, masking uncommitted agent work. +Running from a subdirectory can hide real changes (Sprint 30: a wrong-CWD +`git diff` returned 0 lines, masking uncommitted work). **CRITICAL: Do not trust "already done" claims without checking git state.** If agents report that work was "already implemented in a prior commit," verify @@ -197,7 +178,7 @@ git fetch --all git log --oneline origin/.. ``` -If no unpushed commits, skip to step 5 (PR check). +If no unpushed commits, skip to step 6 (PR check). ### 4. Check for conflicts with main @@ -222,24 +203,13 @@ versus `origin/main`. This list covers generator code AND the data files that fe The stamped generator sources come from the shared registry `tooling/generator_sources.py` (T-1067) — the same module the stamp writer and `tooling/check-systems-db-stamp` use, so the lists can no longer drift (PR #136 -review T7). The CLI call below expands to one repo-relative path per line; the -extra hardcoded entries are non-stamped watch items (retired/one-time planet-gen -importers, the schema DDL whose SHA is stamped separately, and the data -directories that feed the generators). +review T7). `tooling/pr-watchlist-diff` wraps the comparison — its header comment +documents the non-stamped watch items it also checks (the surviving one-time +planet-gen importers, the schema DDL whose SHA is stamped separately, and the +wiki data directories that feed the generators): ```bash -git diff --name-only origin/main...HEAD -- \ - $(python3 tooling/generator_sources.py --list) \ - tooling/planet-gen/generate_atlas.py \ - tooling/planet-gen/gemma_naming.py \ - tooling/planet-gen/naming_core.py \ - tooling/planet-gen/import_city_names.py \ - tooling/planet-gen/import_heightmaps.py \ - tooling/planet-gen/import_province_boundaries.py \ - server/data/systems-schema.sql \ - wiki/star-systems/ \ - wiki/economics/ \ - content/economics/ +tooling/pr-watchlist-diff origin/main HEAD ``` **If output is empty:** skip this step entirely. @@ -315,8 +285,14 @@ git diff --stat main... Draft title (`(): `, max 70 chars) and description. ```bash +# NB — both halves matter (.claude/rules/tea-cli.md): +# 1. omit --repo: an explicit slug makes tea skip local-repo setup and +# pr create dies with "local repository required"; +# 2. run THIS ONE COMMAND from the MAIN checkout, not this worktree — +# tea's go-git can't read a worktree's .git *file*. From the worktree: +# cd "$(git rev-parse --path-format=absolute --git-common-dir)/.." +# for this command, then return here for the remaining steps. tea pr create \ - --repo jpmschweitzer/settled-reach \ --login schweitz \ --title "" \ --description "## Summary ..." \ @@ -377,5 +353,5 @@ Suggest: "PR processed. Run `/pr-review` from main to review, or `/whats-next` f ## Arguments -If the user passes arguments (e.g., `/pr-push "my title"`), use them as the +If the user passes arguments (e.g., `/pr-process "my title"`), use them as the PR title instead of generating one. diff --git a/.claude/skills/pr-review/SKILL.md b/.claude/skills/pr-review/SKILL.md index 82d9eb1eb..b14663e79 100644 --- a/.claude/skills/pr-review/SKILL.md +++ b/.claude/skills/pr-review/SKILL.md @@ -1,12 +1,12 @@ --- name: pr-review description: > - Review a branch diff with team-appropriate agents before merge. Use when the - user says "review-pr", "review this PR", "review this branch", or invokes + Review a Gitea branch diff with team-appropriate agents before merge. Use when + the user says "review-pr", "review this PR", "review this branch", or invokes /pr-review. Spawns reviewers matched to the branch type (code, copy, visual, audio) in parallel. Reports approve/reject with inline comments. user-invocable: true -allowed-tools: Bash, Read, Grep, Glob, Task +allowed-tools: Bash, Read, Grep, Glob, Agent, Write --- # PR Review Skill @@ -30,8 +30,8 @@ Stop and wait for the user to invoke `/pr-review` from main. ### 0b. Verify runtime smoke test was performed Before spawning reviewers, check that the pushing team performed basic -runtime verification. This was the #1 process failure of Sprint 28 — -3 review rounds without anyone launching the game missed critical bugs. +runtime verification — the #1 process failure of Sprint 28 (3 review +rounds shipped without anyone launching the game). Ask: "Did the team run `make game` or a headless smoke test before pushing this PR?" @@ -40,13 +40,6 @@ If the PR description or commit messages don't mention runtime testing, note this in the review output as a process gap. Reviewers should still proceed (the PR exists and needs reviewing) but the gap should be visible. -For **client/visual branches**, run a quick headless parse check from main: -```bash -godot --headless --path client --quit 2>&1 | grep -i "SCRIPT ERROR" -``` -If script errors appear in the branch diff files, flag them immediately -before spawning reviewers — no point reviewing code that doesn't parse. - #### 0b-i. Merge-path smoke test gate When the branch diff touches any of: @@ -69,18 +62,17 @@ Unchecked merge-path smoke boxes downgrade the verdict from APPROVED to REQUEST_CHANGES even if reviewers have no code comments. The smoke is a deliverable, not a suggestion. -### 0c. Zero warnings check +### 0c. Parse/lint — deferred to the push gate -The project enforces a **zero warnings policy**. Before spawning reviewers, -check if the branch introduces lint warnings: - -- **client/visual:** `gdlint client/scripts/ client/ui/` should report 0 issues -- **server:** `cargo clippy -- -D warnings` should be clean -- **ci/tooling:** `ruff check tooling/` should be clean - -If warnings exist, note the count in the review output. Reviewers should -flag any **new** warnings introduced by the branch as `warning` severity. -Pre-existing warnings are not PR blockers but should be tracked for cleanup. +`/pr-review` runs on an already-pushed branch (step 0's branch guard means the +reviewer's only local tree is **main**). Re-running `gdlint`/`cargo clippy`/`ruff` +here would check main's code, not the branch's — and `cargo clippy` fails outright +at the repo root (no root `Cargo.toml`; the server crate is `server/`). None of +that is necessary anyway: the pre-push hook (`.config/hooks/pre-push`, declared +authoritative by `team-patterns.md`) already ran `cargo fmt --check` / `cargo +clippy --all-targets -- -D warnings` / `cargo test` (server changes) or the full +gdUnit4 suite via `tests/run-godot` (client changes) before this push succeeded. +Trust the gate; don't duplicate it here with a weaker, wrong-tree variant. ### 1. Determine the branch to review @@ -113,7 +105,9 @@ git diff --stat main...<branch> Examine the changed file paths to determine the dominant change type: -- Mostly `server/` changes → **code** reviewers +- Mostly `server/` changes → **code** reviewers — *except* `server/content/` + (game content data: campaigns, gauntlet, global, `content.yaml`), which routes + to **copy** reviewers despite living under `server/` - Mostly `client/` changes (excluding `client/ui/` art assets) → **code** reviewers - Mostly `wiki/`, `docs/atlas/`, `content/` changes → **copy** reviewers - Mostly `client/ui/` or asset changes (`.tres`, `.tscn`, textures) → **visual** reviewers @@ -140,59 +134,54 @@ If the diff is empty, report "No changes to review" and stop. - `Cargo.lock` (auto-generated) - `client/addons/gdUnit4/` (vendor test framework) - `*.uid` (Godot-generated) -- `docs/backups/settledreach.db.backup` (binary) +- `docs/backups/*` (binary DB backups, e.g. `settledreach.db.backup.pre35`) Three-dot diff with pathspec exclusions is unreliable. Instead, either: 1. Use `git diff main...<branch>` (full diff) and filter in the prompt, or -2. Read source files directly from the team directory (see below). +2. Read source files directly from the worktree (see below). For large diffs (>1000 lines of source), provide **source files** rather than raw diff to reviewers — cleaner context, better reviews. -**Reviewer agents read source files from the team worktree.** +**Reviewer agents read source files from the branch worktree — verified, not +assumed — or via `git show`.** -Sprint branches follow `sprint-{N}/{team}`. Mid-sprint, a worktree -of each branch exists at `$(dirname <repo_root>)/.sprint/sprint-{N}/{team}/` -— a *sibling* of the repo root, not a child. This worktree IS the -branch: Read/Grep on paths rooted there resolve against the branch's -checkout, not main's. - -**Why this matters:** Sprint 37 PR #138 review produced 6 false- -positive findings because the reviewer defaulted to Read/Grep on the -main repo path (`/var/mnt/data/projects/settled-reach/main/`) instead -of the branch worktree. Every finding was a verbatim match against -main's state but irrelevant to the branch — the branch had already -cleaned the residue the reviewer flagged as "still present." Sending -those findings to the team would have caused busywork on already-clean -code, and more dangerously, the same drift hides *false negatives* -(branch-introduced bugs the reviewer never saw because it never read -the branch). - -Fix: before spawning reviewers, resolve the worktree path and pass it -into every reviewer prompt with prominent language. The reviewer -reads from the worktree, not from main. +Topic branches from `/whats-next` may have a worktree at +`<repo_root>/.worktrees/<branch-name>` (the kanban convention, D-221 — see +`tooling/worktree-setup`): ```bash -# Find worktree for this branch (if one exists) -WORKTREE=$(git worktree list --porcelain | grep -B2 "branch refs/heads/<branch>" | grep "worktree " | sed 's/worktree //') - -# Verify it exists and matches the branch tip -git -C "$WORKTREE" rev-parse HEAD # should equal `git rev-parse origin/<branch>` +REPO_ROOT="$(git rev-parse --show-toplevel)" +WORKTREE="$REPO_ROOT/.worktrees/<branch-name>" ``` -If the worktree exists and its HEAD matches `origin/<branch>`, use it -as the reviewer's source of truth. If it doesn't exist (e.g. the -worktree has been torn down or you're reviewing a branch without a -worktree), fall back to `git show origin/<branch>:<path>` — explicitly -flag this fallback in the reviewer prompt so the reviewer knows -Read/Grep on any local path would be wrong. +If that path doesn't exist, fall back to the generic resolution (also used by +`/pr-review` §7a cleanup): + +```bash +WORKTREE=$(git worktree list --porcelain | grep -B2 "branch refs/heads/<branch>" | grep "worktree " | sed 's/worktree //') +``` + +Use the worktree as the reviewer's source of truth **only if both** hold — +otherwise fall back to `git show origin/<branch>:<path>` and flag the fallback +in the reviewer prompt: + +1. **HEAD matches the branch tip:** `git -C "$WORKTREE" rev-parse HEAD` equals `git rev-parse origin/<branch>`. +2. **Not a sparse checkout:** `git -C "$WORKTREE" sparse-checkout list` is empty or errors ("not a sparse checkout"). + +Two incidents motivate both checks — cited once, here, so they don't drift +back into two contradictory rules: Sprint 37 (a reviewer defaulted to reading +the main repo root instead of the branch worktree — 6 false positives) and +Sprint 33 (a sparse worktree silently excluded the wiki directory — a false +"missing prose" finding). One rule now covers both: verify HEAD *and* +sparse-checkout status, or use `git show`. In the reviewer prompt, state the rule non-negotiably: -> **Read source from `<WORKTREE_PATH>` only.** Do NOT Read or Grep -> paths under the main repo root (`/var/mnt/data/projects/settled-reach/main/`). -> Those resolve to main, not the branch. The worktree at `<WORKTREE_PATH>` -> IS the branch — point all file tools there. +> **Read source from `<WORKTREE_PATH>` only** (or `git show +> origin/<branch>:<path>` if no valid worktree exists). Do NOT Read or Grep +> paths under the main repo root (`$(git rev-parse --show-toplevel)`) — that +> resolves to main, not the branch. Also tell agents to read relevant `governance/**/*.md` files for context (these can be read from either path — they're usually identical — @@ -200,7 +189,7 @@ but for consistency, use the worktree path). ### 4. Spawn reviewers in parallel -Use the Task tool to spawn **all reviewers simultaneously** in a single message. +Use the Agent tool to spawn **all reviewers simultaneously** in a single message. Read `references/reviewer-profiles.md` for the full per-branch-type reviewer specifications (agent types, models, prompt focus areas). Match the branch type @@ -269,7 +258,7 @@ Verdict rules: something can be better, say so. ``` -## 6. Posting results to Gitea +### 6. Posting results to Gitea After presenting results to the user, post the review as a PR comment. @@ -283,8 +272,8 @@ Post using the `tea-comment` wrapper (handles temp files and cleanup). `tooling/tea-comment` in a separate short Bash call. 2. **Run `tooling/tea-comment` in the FOREGROUND, never with `run_in_background`.** The background execution path silently fails — - the comment never reaches Gitea and the team never sees the review. - Sprint 38 lost an entire review round this way. Always foreground. + the comment never reaches Gitea and the team never sees the review + (Sprint 38 lost an entire review round this way). ``` # Step 1: Use the Write tool to create the file @@ -297,7 +286,7 @@ tooling/tea-comment <PR_NUMBER> @/tmp/pr-review-<NUMBER>.md Do NOT use `cat << 'EOF'` heredocs for review content — they create massive permission prompts that are slow to render and often get stuck. -## 7. Merging approved PRs +### 7. Merging approved PRs `tea pr merge` fails (405) when branches have conflicts with main. Merge locally instead: diff --git a/.claude/skills/pr-review/references/reviewer-profiles.md b/.claude/skills/pr-review/references/reviewer-profiles.md index 5bf12b827..16ac25bf2 100644 --- a/.claude/skills/pr-review/references/reviewer-profiles.md +++ b/.claude/skills/pr-review/references/reviewer-profiles.md @@ -2,23 +2,18 @@ Use `model: sonnet` for all reviewers — sufficient for review, saves cost. -**Reviewer agents read source files via `git show` or from sprint -worktrees.** Sprint branches use `sprint-{N}/{team}` naming. Include -the branch name and a list of changed files in every prompt. The -default approach is `git show origin/<branch>:<path>`. If an active -worktree exists under `.sprint/`, agents can also use the Read tool -with the worktree path. **Always prefer `git show` over worktree -reads** — worktrees may use sparse checkouts that silently exclude -files, causing reviewers to miss content and produce false findings -(Sprint 33 lesson: Paula reported missing prose that was actually -present, because the worktree excluded the wiki directory). +**Reviewer agents read source files from the branch worktree or via `git +show`** — see SKILL.md §3 for the full resolution + validation rule (prefer +the worktree only when its HEAD matches the branch tip AND it isn't a sparse +checkout; otherwise `git show origin/<branch>:<path>`). Include the branch +name and a list of changed files in every prompt. ## Code reviews (`server`, `client`, `ci`) **Hoshe (Code Quality)** - `subagent_type`: `hoshe`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, and commit log. - Tell Hoshe to read source files from the team directory, then review for: +- Prompt: Provide the worktree path, list of changed files, and commit log. + Tell Hoshe to read source files from the worktree, then review for: - Correctness and bug risks - Error handling gaps - Test coverage (are new features tested?) @@ -28,8 +23,8 @@ present, because the worktree excluded the wiki directory). **Tyre (Architecture)** - `subagent_type`: `tyre`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, and commit log. - Tell Tyre to read the relevant `governance/**/*.md` files from the team directory +- Prompt: Provide the worktree path, list of changed files, and commit log. + Tell Tyre to read the relevant `governance/**/*.md` files from the worktree first, then review for: - Architectural consistency with project decisions - API/interface design quality @@ -41,8 +36,8 @@ present, because the worktree excluded the wiki directory). **Hoshe (QA)** - `subagent_type`: `hoshe`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, and commit log. - Tell Hoshe to read the changed files from the team directory, then review for: +- Prompt: Provide the worktree path, list of changed files, and commit log. + Tell Hoshe to read the changed files from the worktree, then review for: - Formatting consistency (markdown, file naming, frontmatter) - Broken references or links - Spelling and grammar @@ -51,9 +46,9 @@ present, because the worktree excluded the wiki directory). **Paula (Narrative Depth)** - `subagent_type`: `paula`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, commit log, and +- Prompt: Provide the worktree path, list of changed files, commit log, and list of relevant `governance/**/*.md` files to read. Tell Paula to read all - files from the team directory using the Read tool, then review for: + files from the worktree using the Read tool, then review for: - Narrative quality and character voice consistency - Whether dialogue and monologue feel authentic to the characters - Consequences and stakes — do choices carry weight? @@ -62,9 +57,9 @@ present, because the worktree excluded the wiki directory). **Miri (World Consistency)** - `subagent_type`: `miri`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, commit log, and +- Prompt: Provide the worktree path, list of changed files, commit log, and list of relevant `governance/**/*.md` files to read. Tell Miri to read all - files from the team directory using the Read tool, then review for: + files from the worktree using the Read tool, then review for: - Lore accuracy — do facts match established setting? - Internal consistency across files - IP originality — nothing should read as a copy from another franchise @@ -75,8 +70,8 @@ present, because the worktree excluded the wiki directory). **Hoshe (QA)** - `subagent_type`: `hoshe`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, and commit log. - Tell Hoshe to read the changed files from the team directory, then review for: +- Prompt: Provide the worktree path, list of changed files, and commit log. + Tell Hoshe to read the changed files from the worktree, then review for: - File format and naming conventions - Asset organization and directory structure - Missing or broken references in scene/resource files @@ -84,7 +79,7 @@ present, because the worktree excluded the wiki directory). **Araminta (Art Direction)** - `subagent_type`: `araminta`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, and commit log. +- Prompt: Provide the worktree path, list of changed files, and commit log. Tell Araminta to read the style guide and relevant design docs from the worktree first, then review for: - Visual consistency with the established style guide @@ -97,8 +92,8 @@ present, because the worktree excluded the wiki directory). **Hoshe (QA)** - `subagent_type`: `hoshe`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, and commit log. - Tell Hoshe to read the changed files from the team directory, then review for: +- Prompt: Provide the worktree path, list of changed files, and commit log. + Tell Hoshe to read the changed files from the worktree, then review for: - File format and naming conventions - Audio asset organization and directory structure - Missing or broken references @@ -106,8 +101,8 @@ present, because the worktree excluded the wiki directory). **Ozzie (Player Experience)** - `subagent_type`: `ozzie`, `model`: `sonnet` -- Prompt: Provide the team directory path, list of changed files, and commit log. - Tell Ozzie to read all files from the team directory using the Read tool, then +- Prompt: Provide the worktree path, list of changed files, and commit log. + Tell Ozzie to read all files from the worktree using the Read tool, then review for: - Emotional impact — does the audio enhance the moment? - Atmosphere and tone — does it feel like the Settled Reach? diff --git a/.claude/skills/skill-create/SKILL.md b/.claude/skills/skill-create/SKILL.md deleted file mode 100644 index 623dd0c72..000000000 --- a/.claude/skills/skill-create/SKILL.md +++ /dev/null @@ -1,203 +0,0 @@ ---- -name: skill-create -description: > - Guidance for creating effective Claude Code skills (.skill packages). - Use when the user wants to create, build, design, or iterate on a skill — - including writing SKILL.md files, bundling scripts/references/assets, - initializing new skills, packaging skills, or improving existing ones. - Triggers on requests like "create a skill", "make a new skill", - "build a skill for X", "package this skill", or "improve my skill". ---- - - -# Skill Creator - -## About Skills - -Skills are modular, self-contained packages that extend Claude's capabilities -by providing specialized knowledge, workflows, and tools. They transform Claude -from a general-purpose agent into a specialized agent equipped with procedural -knowledge that no model can fully possess. - -### What Skills Provide - -- **Specialized workflows** — Multi-step procedures for specific domains -- **Tool integrations** — Instructions for working with specific file formats or APIs -- **Domain expertise** — Company-specific knowledge, schemas, business logic -- **Bundled resources** — Scripts, references, and assets for complex and repetitive tasks - -## Core Principles - -### Concise is Key - -The context window is a public good. Skills share it with everything else Claude -needs: system prompt, conversation history, other skills' metadata, and the -actual user request. - -Default assumption: Claude is already very smart. Only add context Claude doesn't -already have. Challenge each piece of information: "Does Claude really need this -explanation?" and "Does this paragraph justify its token cost?" - -Prefer concise examples over verbose explanations. - -### Set Appropriate Degrees of Freedom - -Match specificity to the task's fragility and variability: - -- **High freedom** (text-based instructions): Multiple approaches valid, decisions - depend on context, heuristics guide the approach. -- **Medium freedom** (pseudocode or scripts with parameters): Preferred pattern - exists, some variation acceptable, configuration affects behavior. -- **Low freedom** (specific scripts, few parameters): Operations are fragile and - error-prone, consistency is critical, specific sequence must be followed. - -Think of Claude as exploring a path: a narrow bridge with cliffs needs specific -guardrails (low freedom), while an open field allows many routes (high freedom). - -## Anatomy of a Skill - -``` -skill-name/ -├── SKILL.md (required) -│ ├── YAML frontmatter metadata (required) -│ │ ├── name: (required) -│ │ ├── description: (required) -│ │ └── compatibility: (optional, rarely needed) -│ └── Markdown instructions (required) -└── Bundled Resources (optional) - ├── scripts/ - Executable code (Python/Bash/etc.) - ├── references/ - Documentation loaded into context as needed - └── assets/ - Files used in output (templates, icons, fonts, etc.) -``` - -### SKILL.md (required) - -- **Frontmatter (YAML)**: `name` and `description` fields (required). Only these - are read by Claude to determine when the skill triggers — be clear and - comprehensive. The `compatibility` field is for environment requirements but - most skills don't need it. -- **Body (Markdown)**: Instructions and guidance. Only loaded AFTER the skill - triggers. - -### Bundled Resources (optional) - -**Scripts (`scripts/`)** — Executable code for tasks requiring deterministic -reliability or that are repeatedly rewritten. - -**References (`references/`)** — Documentation loaded as needed into context. -Keep SKILL.md lean; move detailed reference material, schemas, and examples here. -If files are large (>10k words), include grep search patterns in SKILL.md. - -**Assets (`assets/`)** — Files used in output, not loaded into context (templates, -images, icons, boilerplate code, fonts). - -### What to NOT Include - -Do NOT create extraneous files like README.md, INSTALLATION_GUIDE.md, -QUICK_REFERENCE.md, CHANGELOG.md, etc. The skill should only contain information -needed for an AI agent to do the job. - -## Progressive Disclosure - -Skills use a three-level loading system: - -1. **Metadata** (name + description) — Always in context (~100 words) -2. **SKILL.md body** — When skill triggers (<5k words) -3. **Bundled resources** — As needed (unlimited; scripts can run without reading) - -Keep SKILL.md body under 500 lines. Split content into separate files when -approaching this limit. Reference split files from SKILL.md with clear -descriptions of when to read them. - -### Disclosure Patterns - -**Pattern 1: High-level guide with references** — Keep overview in SKILL.md, -link to detail files loaded only when needed. - -**Pattern 2: Domain-specific organization** — Organize content by domain -(e.g., `references/finance.md`, `references/sales.md`) so only relevant content -is loaded. - -**Pattern 3: Conditional details** — Show basic content, link to advanced -content loaded only when the user needs those features. - -Guidelines: -- Avoid deeply nested references — keep one level deep from SKILL.md -- Structure longer reference files with a table of contents at the top - -## Skill Creation Process - -Follow these steps in order, skipping only with clear reason: - -### Step 1: Understand the Skill with Concrete Examples - -Skip only when usage patterns are already clearly understood. - -Ask the user for concrete examples of how the skill will be used: -- "What functionality should the skill support?" -- "Can you give some examples of how this skill would be used?" -- "What would a user say that should trigger this skill?" - -Avoid overwhelming users — start with the most important questions. - -### Step 2: Plan the Reusable Skill Contents - -Analyze each example by considering how to execute from scratch and identifying -what scripts, references, and assets would help with repeated execution. - -Establish a list of reusable resources: scripts, references, and assets. - -### Step 3: Initialize the Skill - -Create the skill directory manually: - -``` -mkdir -p <output-directory>/<skill-name> -``` - -Then create `SKILL.md` with frontmatter and body. Add `scripts/`, `references/`, -and `assets/` subdirectories only as needed. - -Skip if iterating on an existing skill. - -### Step 4: Edit the Skill - -Remember the skill is for another Claude instance to use. Include beneficial, -non-obvious information. - -For design patterns, consult: -- `references/workflows.md` — Sequential workflows and conditional logic -- `references/output-patterns.md` — Template and example patterns - -**Implementation order:** -1. Start with reusable resources (`scripts/`, `references/`, `assets/`) -2. Test added scripts by running them -3. Delete unused example files from initialization -4. Update SKILL.md - -**Writing guidelines:** Always use imperative/infinitive form. - -**Frontmatter:** -- `name`: The skill name — use **domain-action** naming: `{domain}-{action}`. - The domain is the system/area the skill operates on, the action is what it does. - Examples: `pr-review`, `sprint-plan`, `docs-search`, `git-commit`, `debt-scan`. - Multi-action wrappers (like `ticket`) can use the domain name alone. - The directory name must match the `name` field. -- `description`: Primary triggering mechanism. Include what the skill does AND - specific triggers/contexts. All "when to use" info goes here (not in body). - -**Body:** Instructions for using the skill and its bundled resources. - -### Step 5: Validate the Skill - -Check the skill manually: -- Frontmatter has `name` and `description` -- SKILL.md body is under 500 lines -- No extraneous files (README.md, CHANGELOG.md, etc.) -- Scripts are executable and tested -- References are referenced from SKILL.md - -### Step 6: Iterate - -After real usage, notice struggles or inefficiencies, identify needed updates, -implement changes, and test again. diff --git a/.claude/skills/sprite-gen/SKILL.md b/.claude/skills/sprite-gen/SKILL.md index 63e5406f1..81653dae1 100644 --- a/.claude/skills/sprite-gen/SKILL.md +++ b/.claude/skills/sprite-gen/SKILL.md @@ -23,6 +23,12 @@ artwork via the Godot render pipeline at `renderer/`. models to 4-direction sprites for a 2D view — that role is **retired** (D-244). It now serves flat 2D artwork only. +## Authoring a New Scene + +To author a new scene, read `renderer/README.md` — "Adding a New Model" + +Material Guidelines (`MeshInstance3D` root centered at origin, flat +`StandardMaterial3D`, no emission/specularity). + ## Render Pipeline ```bash @@ -32,16 +38,22 @@ serves flat 2D artwork only. Scenes live at `renderer/models/<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`. diff --git a/.claude/skills/sprite-gen/scripts/render.sh b/.claude/skills/sprite-gen/scripts/render.sh index f015c90ec..6f8c74421 100755 --- a/.claude/skills/sprite-gen/scripts/render.sh +++ b/.claude/skills/sprite-gen/scripts/render.sh @@ -24,24 +24,48 @@ echo "Rendering: $MODEL_NAME" echo "Godot: $GODOT" echo "Project: $PROJECT" +# The render step (unlike --import) needs a display. Fail fast with a clear +# remedy instead of letting Godot die on a cryptic low-level GL/X error. +if [ -z "${DISPLAY:-}" ]; then + if command -v xvfb-run >/dev/null 2>&1; then + echo "No DISPLAY set — re-run under xvfb-run, e.g.:" + echo " xvfb-run -a $0 $MODEL_NAME${2:+ $2}" + exit 1 + else + echo "ERROR: No DISPLAY set and xvfb-run is not installed." + echo " Install it (e.g. 'xorg-x11-server-Xvfb' on Fedora/RHEL, 'xvfb' on" + echo " Debian/Ubuntu), then re-run under xvfb-run -a." + exit 1 + fi +fi + # Ensure resources are imported (headless, no display needed) echo "Importing resources..." "$GODOT" --path "$PROJECT" --headless --import 2>&1 || echo "Warning: import step had errors (may be non-fatal)" +# Clear stale output from any previous run of this model first, so a failed +# render can't be masked by leftover files from an earlier successful run. +rm -f "$OUTPUT/${MODEL_NAME}"_*.png + # Run Godot with the render scene, passing model name as user arg "$GODOT" --path "$PROJECT" "$SCENE" -- "$MODEL_NAME" 2>&1 # Check output -EXPECTED_FILES=0 +FOUND_FILES=0 for dir in north east south west; do for res in 1024 256 64; do f="$OUTPUT/${MODEL_NAME}_${dir}_${res}.png" if [ -f "$f" ]; then - EXPECTED_FILES=$((EXPECTED_FILES + 1)) + FOUND_FILES=$((FOUND_FILES + 1)) fi done done echo "" -echo "Generated $EXPECTED_FILES/12 files in $OUTPUT/" +echo "Generated $FOUND_FILES/12 files in $OUTPUT/" ls -la "$OUTPUT/${MODEL_NAME}"_*.png 2>/dev/null || echo "No output files found." + +if [ "$FOUND_FILES" -eq 0 ]; then + echo "ERROR: Godot produced no output files — render failed." >&2 + exit 1 +fi diff --git a/.claude/skills/ticket/SKILL.md b/.claude/skills/ticket/SKILL.md index ff2998fb6..4b8a4e44c 100644 --- a/.claude/skills/ticket/SKILL.md +++ b/.claude/skills/ticket/SKILL.md @@ -1,9 +1,12 @@ --- name: ticket description: > - Manage project tickets in the pql planning store. Use when the user says - "ticket", "create a ticket", "show tickets", "board", "phase", or invokes - /ticket. Wraps the pql CLI for structured project management. + Manage project tickets in the pql planning store — day-to-day ticket CLI + (list, create, update, board, phase epics). Use when the user says "ticket", + "create a ticket", "show tickets", "board", "phase epic", "what's in the + active phase", or invokes /ticket. Wraps the pql CLI for structured project + management. For changelog/planning-store internals (branch moves, recovery, + write-through mechanics), use the pql skill instead. user-invocable: true allowed-tools: Bash, Read, Grep, Glob --- @@ -16,58 +19,22 @@ Manage tickets via **pql** (`.pql/pql.db`, rebuilt from the git-tracked ## Commands -### List / browse -```bash -pql ticket list [--status S] [--team T] [--label L] [--assigned A] [--decision D-NNN] \ - [--unblocked] [--leaf] [--under T-N] -pql ticket board [--team T] # kanban view, grouped by status -pql ticket show T-440 [--with-context] [--with-children] [--with-blockers] [--tree] -pql ticket show T-1,T-2,T-3 # batch -``` -(pql ticket `list` has no `--type`/`--priority` filter — list and read the JSON, or use -`--under <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 diff --git a/.claude/skills/whats-next/SKILL.md b/.claude/skills/whats-next/SKILL.md index 47f2b8ecf..4795a7747 100644 --- a/.claude/skills/whats-next/SKILL.md +++ b/.claude/skills/whats-next/SKILL.md @@ -47,6 +47,11 @@ lowest-numbered non-`done` phase epic): pql ticket list --under T-745 --status in_progress --fields id,type,status,title ``` +> `--status` takes a SINGLE value (not a comma list — `backlog,in_progress` matches +> nothing and silently returns `[]`). Most tickets here sit in `backlog` or `done`; +> if you need multiple statuses at once (e.g. both `backlog` and `in_progress`), run +> the query once per status. + The phase epic is the `"type": "epic"` row. Use pql natively — no ad-hoc python/jq projections over its output: since pql 1.11.0, `ticket list` omits `description` by default and supports `--fields id,type,parent_id,priority,status,title` (the batch- @@ -179,6 +184,23 @@ For each ticket in the batch (batch with commas: `pql ticket status T-1,T-2 in_p pql does **not** enforce the WIP limit — check it yourself in step 1d and warn if the batch pushes in-progress past 5. +> **Then make sure it persists.** Ticket mutations (status here, and any +> `ticket append` in Step 2) land only in the gitignored `.pql/pql.db`. The +> pre-commit hook runs `pql plan export --stage` automatically — the changelog +> is exported and staged on every commit, so never hand-run the export or +> `git add .pql/changelog`. The rule is simpler: the turn must land at least +> one commit (through the git-commit skill). A ticket-only turn with no commit +> leaves the mutations in `pql.db` only, and the post-checkout/post-merge hooks +> rebuild that DB from the committed changelog on the next branch switch — +> silently dropping them. See the pql skill's "Versioning planning state" +> section for the full mechanics. +> +> - Don't hand-export or hand-stage `.pql/changelog/` — the pre-commit hook does +> both on every commit. The real footgun is a turn that mutates tickets but +> never commits: `pql.db` is gitignored and gets rebuilt from the committed +> changelog on branch switch, silently dropping un-committed mutations. Land +> at least one commit per ticket-mutating turn. + ### 3b. Create topic branch Name the branch after the epic or logical grouping. Examples: diff --git a/.claude/skills/workshop-start/IMPROVEMENTS.md b/.claude/skills/workshop-start/IMPROVEMENTS.md deleted file mode 100644 index 3ba9b1162..000000000 --- a/.claude/skills/workshop-start/IMPROVEMENTS.md +++ /dev/null @@ -1,87 +0,0 @@ -# workshop-start — Improvement Log - -Running notes on friction and fixes observed while running workshops. Fold the -confirmed ones into `SKILL.md` periodically; delete once applied. - -## From the `system-economic-specialization` relaunch (2026-05-31) - -- **Tool name drift.** SKILL.md §5 says "Use the **Task tool** to spawn each - agent." The actual spawn tool is **`Agent`** (with `team_name` + `name` + - `subagent_type`). "Task" reads as the TaskCreate/TaskUpdate family and is - misleading. Rename to "Agent tool" throughout §5. - -- **SI has no Round 1 work — say so.** The skill adds SI to every workshop but - SI's job is Round 2 ticket creation. Round 1 there is no SI task, so SI sits - with an empty TaskList and no instruction. Recommend the skill explicitly note: - "SI joins on standby; SI gets no task until Round 2 — spawn it in background - with a 'load context and wait for Round 2' prompt." Avoids an idle/confused - agent on spawn. - -- **Spawn order vs. task creation.** Following the skill literally (§4 create - tasks → §5 spawn) matters: if you spawn agents before the tasks exist, they - check TaskList, find nothing, and may idle. Worth a one-line warning in §5: - "Create and assign all Round 1 tasks BEFORE spawning, or the agents wake to an - empty list." (Recoverable by messaging them after, but cleaner to order it - right.) - -- **`TaskCreate` is one-task-per-call.** No batch/array form. A batch attempt is - rejected. The skill could state this so the lead emits N separate TaskCreate - calls (and assigns owners via TaskUpdate, since TaskCreate takes no `owner`). - -- **Read-only participants can't write their own files.** Several workshop agent - types (paula, gore, nigel, ozzie, and the read-only miri variant) have NO Write - tool — yet SKILL.md §4 makes "write your full output to disk" a hard - requirement for every participant. Paula hit this: she produced her full Round 1 - doc but had to send it to the lead to write `paula-round1.md`. The skill should - state up front: "Participants without a Write tool deliver their output via - SendMessage; the lead writes the file and marks the task complete on their - behalf." Or: assign a write-capable scribe. Either way, don't make disk-write a - per-agent requirement for read-only agent types — it guarantees a manual relay. - -- **Participants self-claim, which races owner assignment.** Paula picked up - task #3 on her own before the lead assigned owners. Harmless here, but if tasks - aren't clearly scoped per-agent an agent could grab the wrong one. The skill's - per-agent task titles ("Round 1 — Miri …") mitigate this; keep titles - agent-named. - -## Candidate feature — per-round "scout" critic (user idea, 2026-05-31) - -Add an optional independent **scout** agent that runs per round, AFTER the round's -output files land and BEFORE the user checkpoint, to surface missed options, -simpler designs, internal contradictions, and external design prior-art. - -- **Value is mostly the independent adversarial review**, not the web. Participants - build on each other and converge; a fresh agent with no stake catches blind - spots convergence hides. Web search is a bonus, useful for the *mechanism* (real - economic geography, how other games encode X, schema-granularity tradeoffs) — - NOT the lore. -- **Hard guardrails:** (1) lore is authored-canonical and fictional — the scout must - NOT "correct" the fiction with real-world/web facts; scope web to design/mechanism - prior-art only. (2) Only surface findings that would CHANGE a decision — a critic - that always finds 10 things gets ignored. -- **Shape:** cheap model (Haiku), `run_in_background: true`, reads the round files + - relevant D-records, writes `scout-round{N}.md`. Lead folds decision-changing items - into the checkpoint presented to the user. Escalate to Sonnet only to dig into a - real find. -- Pairs well with the quality-over-scope-creep principle: the scout is a cheap way - to pressure-test that "quality" claims actually hold and nothing simpler was missed. - -## Wrap-up gotchas (2026-05-31) - -- **`pql decisions claim` is side-effect-free** (it just prints the next available ID). - This fixes the old `tooling/db/decision claim` footgun, which burned an ID and appended - a placeholder on every call — retrying it created duplicate records. With pql, claim is - safe to re-run; the record only exists once you write the `### D-NNN: …` heading into the - governance file. Still claim each ID once and write it immediately so parallel branches - don't pick the same number. -- **Read-only documenter can still file** — the lead hands it the claimed ID(s); the - documenter writes the records into `governance/{decisions,questions,rejected}/<domain>.md`. -- **Background (`run_in_background`) teammates can be slow/unreliable to consume - `shutdown_request`.** At wrap-up, qatux + si (both spawned in background per the - skill's §5 guidance) stayed "active" through multiple shutdown sends, blocking - TeamDelete. Foreground participants shut down immediately. Options to fold into - the skill: (a) don't background the always-present agents, or (b) document that - wrap-up may need several shutdown retries / a longer settle, and that the lead - should not spin indefinitely — the work is already on disk, TeamDelete is just - resource cleanup. Consider whether the skill should note the team can be left to - expire rather than blocking the session on cleanup. diff --git a/.claude/skills/workshop-start/SKILL.md b/.claude/skills/workshop-start/SKILL.md index d2be90f84..cc54f5164 100644 --- a/.claude/skills/workshop-start/SKILL.md +++ b/.claude/skills/workshop-start/SKILL.md @@ -4,7 +4,7 @@ description: > Start a multi-agent design workshop from a workshop brief. Use when the user says "start workshop", "run workshop", "let's start the workshop", or invokes /workshop-start. Parses the workshop brief to extract participants, questions, and round format. - Creates a team, tasks, and spawns agents as teammates via the Task tool. + Creates a team, tasks, and spawns agents as teammates via the Agent tool. --- # Start Workshop @@ -24,7 +24,8 @@ Read the workshop brief. Extract: - Workshop name (from directory name) - Participant list (from `**Participants:**` line) - Per-participant questions (scan for `**{AgentName}**:` patterns in questions sections) -- Round count and round descriptions (from `**Workshop Format**` section) +- Round count and round descriptions (from the `## Workshop Format` heading — a real + heading, not a bold marker; every brief under `docs/workshops/` uses this form) ### 2. Create Team @@ -38,8 +39,8 @@ Two agents join every workshop regardless of the participant list: | Agent | Role | Task | Participates in discussion? | |-------|------|------|-----------------------------| -| **Qatux** | Documenter | Captures all decisions, questions, dissent, consensus. Writes `workshop-notes.md` per round, produces final `workshop-outcomes.md`. | No — observes and records only | -| **SI** | Sprint prep | Suggest adding when outputs include tickets. Creates tickets from decisions, links to sprint backlog. | No — execution prep only | +| **Qatux** | Documenter | Captures all decisions, questions, dissent, consensus. Writes `round-{N}-notes.md` per round, produces final `workshop-outcomes.md`. | No — observes and records only | +| **SI** | Refinement reviewer | Suggest adding when outputs include tickets. Reviews `workshop-outcomes.md` for ticket-worthy items and context completeness — does **not** create tickets (si.md's "What you do NOT do" list is explicit: no ticket creation or DB writes). The lead creates tickets via the `/ticket` skill; SI reviews them for gaps. | No — execution prep only | Qatux and SI are **never dismissed early.** If the user reduces the team mid-workshop, keep qatux and si (if added). Documenting everything prevents loss of valuable information. @@ -50,6 +51,7 @@ One task per participant containing: - The specific questions assigned to that participant (extracted from all layers) - The output format from the brief's round description - **IMPORTANT — file output requirement:** Instruct each agent to write their full output to `docs/workshops/{name}/{agent}-round{N}.md` (e.g., `docs/workshops/test-architecture/gestalt-round1.md`). Agents must write to disk, not just send messages. This ensures Qatux and other agents can read all outputs directly without relying on message forwarding. + **Several participant types have no Write tool** (ozzie, gore, nigel, paula, miri — verify current grants in `.claude/agents/*.md` frontmatter). Participants without a Write tool deliver their output via SendMessage instead; the lead writes `{agent}-round{N}.md` on their behalf and marks the task complete. One task for Qatux: "Document Round N — read all agent output files at `docs/workshops/{name}/*-round{N}.md` and capture decisions, questions, and dissent." @@ -57,16 +59,21 @@ Assign all tasks using TaskUpdate with `owner` = agent name. ### 5. Spawn Agents -Use the Task tool to spawn each agent as a teammate. Each call should: +**Create and assign all Round 1 tasks (§4) before spawning.** Spawning first means +agents wake to an empty TaskList and may idle. SI joins on standby with no Round 1 +task — SI's job starts at Round 2 ticket review — so give it a "load context and +wait" prompt rather than an empty task. + +Use the Agent tool to spawn each agent as a teammate. Each call should: - Set `team_name` to the workshop team name - Set `name` to the agent name (e.g., "gestalt") - Set `subagent_type` to the matching agent type (same as name — see reference table) - **Set `model: "sonnet"`** — team-mode participants pin to Sonnet 4.6 for literal-rule-following discipline. Opus 4.7 was observed ignoring scope rules and failing to report back in team mode (Sprint 37). The *team lead* (this session) stays on whatever model the user has selected. - Provide a prompt telling the agent to check TaskList for their assigned task -Spawn all agents in parallel (one Task call per agent in a single message). Agents will appear as teammates in the Claude Code UI and pick up their tasks from the shared task list. +Spawn all agents in parallel (one Agent call per agent in a single message). Agents will appear as teammates in the Claude Code UI and pick up their tasks from the shared task list. -For large workshops (>6 agents), spawn participants in batches to avoid overwhelming the system. Always-present agents (Qatux, SI) can run in background via `run_in_background: true`. +For large workshops (>6 agents), spawn participants in batches to avoid overwhelming the system. Always-present agents (Qatux, SI) can run in background via `run_in_background: true` — but background teammates can be slow or unreliable to consume `shutdown_request`, so wrap-up (§8) may need several shutdown retries. Don't spin indefinitely waiting on it: the work is already on disk, and the team can be left to expire rather than blocking the session on `TeamDelete`. ### 6. Monitor @@ -96,9 +103,9 @@ When all Round N tasks are complete: Before the user dismisses the team, the following are **hard requirements**: 1. **User reviews final outcomes** — Present `workshop-outcomes.md` content to the user via AskUserQuestion. Get explicit approval before proceeding to filing. -2. **D-records filed** — All new D-records, amendments, and supersessions are written to `governance/` domain files. This is non-negotiable — workshops that produce decisions MUST file them before shutdown. +2. **D-records filed** — **Claim each ID first** with `pql decisions claim D <domain> "title"` (side-effect-free — safe to re-run, just prints the next free id) and write the record immediately after claiming, so parallel branches don't collide on the same number (CLAUDE.md requirement). All new D-records, amendments, and supersessions are written to `governance/` domain files. This is non-negotiable — workshops that produce decisions MUST file them before shutdown. 3. **Discussion captured** — Qatux produces final `workshop-outcomes.md` from accumulated notes. Qatux creates or updates diagrams (via `/d2-diagram`) for any new D-records produced by the workshop. -4. **Tickets created** — If SI is present, SI creates tickets from decided items and the user reviews the ticket list. +4. **Tickets created** — The lead creates tickets from decided items via the `/ticket` skill, grounded in `workshop-outcomes.md`. If SI is present, SI reviews the drafted tickets for context completeness before the user reviews the ticket list. 5. **User gives explicit go-ahead to dismiss** — Only after steps 1-4 are complete AND the user confirms, send shutdown_request to all agents (qatux and si last). 6. TeamDelete to clean up. @@ -156,3 +163,5 @@ Agent name maps directly to subagent_type: | inigo | Sound design | Audio, soundscape, spatial audio | | troblum | Tech consultant | Second opinion on architecture/tech choices | | hoshe | QA/testing | Test plans, verification, quality | +| burnelli-sheldon | Economist | Market models, price formation, economic credibility | +| justine | Polish/deployment | Build, performance, release quality | diff --git a/.gitignore b/.gitignore index d8af5a631..617dc357f 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,8 @@ wiki/economics/corporations/generated_corporations.toml .claude/plans/ .claude/projects/ .claude/agent-memory/ +.claude/scheduled_tasks.lock +.claude/scheduled_tasks.json .tmp/ # Local agent tooling caches (per-machine, not shared) diff --git a/TEAM.md b/TEAM.md index bac6a879e..ee5703477 100644 --- a/TEAM.md +++ b/TEAM.md @@ -31,17 +31,21 @@ |-------|------|-------|-------------| | **SI** | Project Manager & Scrum Master | Sprint planning, ticket management, turning decisions into executable work. Manages `/ticket` skill. | Always (from now) | +## Implementation Team (active) + +| Agent | Role | Focus | When active | +|-------|------|-------|-------------| +| **DUDLEY** | Server Developer | Game server, ECS, simulation loop, world state management. | Active — server/ is the main Phase-4 cascade workstream | +| **STIG** | UI Developer | HUD, menus, insert/minimap, diegetic UI per D-013. | Active — client UI work as needed | +| **JUSTINE** | Polish & Deploy | Build pipelines, packaging, performance optimization, release prep. | Active — build/validation tooling | +| **MELLANIE** | Copywriter | In-game text, UI copy, tooltips, flavor text, lore entries. | Active — content batches | + ## Standby Team (activate when needed) | Agent | Role | Focus | When active | |-------|------|-------|-------------| -| **STIG** | UI Developer | HUD, menus, insert/minimap, diegetic UI per D-013. | UI implementation phase | -| **DUDLEY** | Server Developer | Game server, ECS, simulation loop, world state management. | Server implementation phase | | **OSCAR** | Networking Developer | Multiplayer networking, client-server protocol, sync. | Networking implementation phase | -| **JUSTINE** | Polish & Deploy | Build pipelines, packaging, performance optimization, release prep. | Pre-release phase | | **INIGO** | Sound Designer | Soundscape design, ambient layers, diegetic audio cues, audio propagation. | Audio implementation phase | -| **MELLANIE** | Copywriter | In-game text, UI copy, tooltips, flavor text, lore entries. | Content creation phase | -| **TIGER** | Translator | Localization, i18n framework, translation management. | Localization phase | ## Agent Briefings diff --git a/docs/briefings/araminta.md b/docs/briefings/araminta.md index 1e9009709..488346c66 100644 --- a/docs/briefings/araminta.md +++ b/docs/briefings/araminta.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned araminta` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned araminta` ## v0.2 Pivot — Key Changes for Your Role The visual system shifts from designing layouts for three hand-crafted social sites to designing the visual grammar that the generator uses to produce legible zones at scale. diff --git a/docs/briefings/burnelli-sheldon.md b/docs/briefings/burnelli-sheldon.md index 44c84297c..1689f0c9a 100644 --- a/docs/briefings/burnelli-sheldon.md +++ b/docs/briefings/burnelli-sheldon.md @@ -11,8 +11,8 @@ updated: 2026-04-05 Last updated: 2026-04-05 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned burnelli-sheldon` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned burnelli-sheldon` ## Your Domain diff --git a/docs/briefings/dudley.md b/docs/briefings/dudley.md index 6cbe6e7f3..9d2fb21df 100644 --- a/docs/briefings/dudley.md +++ b/docs/briefings/dudley.md @@ -1,6 +1,6 @@ --- title: "Dudley - Project Briefing" -description: "Backend/engine implementation agent on standby — covers Rust simulation server, ECS model, and bevy_ecs architecture decisions" +description: "Server implementation agent — Rust/bevy_ecs simulation server, ECS model, world-generation cascade; the project's most active workstream" type: briefing status: active agent: Dudley @@ -8,14 +8,19 @@ updated: 2026-03-13 --- # Dudley - Project Briefing -Last updated: 2026-03-13 +Last updated: 2026-07-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned dudley` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned dudley` ## Status -STANDBY. This briefing will be populated when backend/engine implementation begins. Dudley activates when simulation implementation work is assigned. +ACTIVE. The Rust/bevy_ecs simulation server (`server/`) is the project's most +active implementation workstream — the Phase-4 world-generation cascade (D-166, +D-243) runs through it. Dudley is the default implementer for server batches +selected by `/whats-next`; recent work includes the Layer-5 tile fill and the +D-253 region transient model (PR #174). Build/test via the top-level Makefile; +the pre-push gate runs fmt/clippy/full cargo test on every push. ## Key Decisions for Your Domain Read `decisions/architecture.md` (primary) and `decisions/content.md` (NPC model). Key decisions: diff --git a/docs/briefings/gestalt.md b/docs/briefings/gestalt.md index 9b148f76e..7dad4216b 100644 --- a/docs/briefings/gestalt.md +++ b/docs/briefings/gestalt.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned gestalt` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned gestalt` ## Key Design Pivots - **Tycoon bookmark (D-117):** Zero investigation content. Tycoon (small business owner) is the starting state. diff --git a/docs/briefings/gore.md b/docs/briefings/gore.md index 85f7b4c31..f1d2d62b7 100644 --- a/docs/briefings/gore.md +++ b/docs/briefings/gore.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned gore` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned gore` ## v0.2 Pivot — Key Changes for Your Role The workshop confirmed and sharpened the thematic frame. "Is this enough?" survives as the central question. The change is that it now must be posed through a generated tycoon's life, not through authored detective/smuggler characters. diff --git a/docs/briefings/hoshe.md b/docs/briefings/hoshe.md index c88611281..5bb0e4fe0 100644 --- a/docs/briefings/hoshe.md +++ b/docs/briefings/hoshe.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned hoshe` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned hoshe` ## Status **ACTIVE.** Test infrastructure from earlier sprints is operational. v0.2 focus: generator output validation — can the generator produce legible NPCs? Are relationships readable from generator output? diff --git a/docs/briefings/inigo.md b/docs/briefings/inigo.md new file mode 100644 index 000000000..59ee85566 --- /dev/null +++ b/docs/briefings/inigo.md @@ -0,0 +1,32 @@ +# Inigo Briefing — Sound Designer (STANDBY) + +**Status: STANDBY.** Audio implementation has not started. Player-facing sound +is a later-phase concern — the development cascade (D-166) builds the world +outside-in, and audio attaches to the walkable, rendered world (Phase 5+), not +before. Do not design detailed soundscapes ahead of the phase that needs them; +if pulled in early, stay at the level the current phase actually requires. + +## When you activate + +You own player-facing audio: soundscape layers per zone type, ambient beds, +diegetic sound cues, and the sound-propagation behaviour that makes hearing a +second channel of perception. + +## Load-bearing decisions + +- **D-018 — sound model.** Sound propagation (close/medium/long range + + occlusion) is a core perception mechanic, not decoration. What the player + hears through walls, around corners, and across z-levels carries gameplay + information. Design every sound with its falloff and occlusion in mind. +- **Information asymmetry is the core loop.** Audio reinforces it — what you + hear but cannot see is a deliberate tell. No non-diegetic cues for gameplay + events (music is the exception). + +## First moves when activated + +1. Read the active phase epic and D-018 in full (`pql decisions read D-018`). +2. Confirm with the team lead which zone types and spaces exist in the + generated world before authoring palettes — soundscapes attach to real + generated locations, not a hand-made test map. +3. Coordinate with Gestalt (sound cues ↔ mechanical significance) and the + perception-system owner (propagation ↔ `VisibilitySector` occlusion). diff --git a/docs/briefings/justine.md b/docs/briefings/justine.md index 27e7cbe98..54d68cc20 100644 --- a/docs/briefings/justine.md +++ b/docs/briefings/justine.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned justine` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned justine` ## Status **ACTIVE.** CI and build pipeline maintenance. Content validation infrastructure remains relevant as the AI-assisted content pipeline (D-123) produces NPC content at scale. diff --git a/docs/briefings/mellanie.md b/docs/briefings/mellanie.md index 653c7e38c..b1cba5124 100644 --- a/docs/briefings/mellanie.md +++ b/docs/briefings/mellanie.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned mellanie` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned mellanie` ## v0.2 Pivot — Key Changes for Your Role The copy pipeline fundamentally shifts from hand-authoring specific NPC profiles to designing the culture-driven voice system that feeds the generator. diff --git a/docs/briefings/miri.md b/docs/briefings/miri.md index f7dbfcdc1..21134992b 100644 --- a/docs/briefings/miri.md +++ b/docs/briefings/miri.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned miri` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned miri` ## v0.2 Pivot — Key Changes for Your Role The workshop confirmed Sova Transit District as the v0.2 setting (tycoon bookmark in Van Maanen's Star) and resolved the culture architecture gap in Miri's favour. diff --git a/docs/briefings/nigel.md b/docs/briefings/nigel.md index c56c5c434..3bb35014b 100644 --- a/docs/briefings/nigel.md +++ b/docs/briefings/nigel.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned nigel` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned nigel` ## v0.2 Pivot — Key Changes for Your Role The replayability frame shifts from "same conspiracy, different character lens" to "same generator, different seeded world." The anti-metagaming goal survives; the mechanism changes. diff --git a/docs/briefings/oscar.md b/docs/briefings/oscar.md index b244096e6..b8c348bd8 100644 --- a/docs/briefings/oscar.md +++ b/docs/briefings/oscar.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned oscar` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned oscar` ## Status STANDBY. This briefing will be populated when networking/multiplayer implementation begins. diff --git a/docs/briefings/ozzie.md b/docs/briefings/ozzie.md index 25e2c3041..f6630dbe9 100644 --- a/docs/briefings/ozzie.md +++ b/docs/briefings/ozzie.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned ozzie` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned ozzie` ## v0.2 Pivot — Key Changes for Your Role The workshop revealed a core framing error: the game was built as a detective puzzle game but the vision is a single-character life sim. The fix is not cosmetic. diff --git a/docs/briefings/paula.md b/docs/briefings/paula.md index da31c7e52..ab4310ad9 100644 --- a/docs/briefings/paula.md +++ b/docs/briefings/paula.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned paula` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned paula` ## v0.2 Pivot — Key Changes for Your Role diff --git a/docs/briefings/qatux.md b/docs/briefings/qatux.md index afb8f9df5..bfad9c214 100644 --- a/docs/briefings/qatux.md +++ b/docs/briefings/qatux.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned qatux` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned qatux` ## Decisions Relevant to Your Role All decisions (D-001 through D-137) — maintains decision domain files in `decisions/` as the canonical record. Indexes and retrieves decision context for the team. diff --git a/docs/briefings/si.md b/docs/briefings/si.md index 3f570d4f4..273fd59db 100644 --- a/docs/briefings/si.md +++ b/docs/briefings/si.md @@ -11,8 +11,8 @@ updated: 2026-05-03 Last updated: 2026-05-03 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned si` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned si` ## Role: Refinement Manager @@ -55,8 +55,8 @@ A ticket has enough context when an agent can pick it up cold and deliver withou ## Key Commands ```bash -tooling/db/ticket list --status backlog # Candidates for refinement -tooling/db/ticket show <id> # Full ticket details -tooling/db/decision show D-NNN # Check a referenced decision -tooling/db/sqlite-query "SELECT * FROM decisions WHERE status = 'open'" # Open decisions +pql ticket list --status backlog # Candidates for refinement +pql ticket show <id> # Full ticket details +pql decisions show D-NNN # Check a referenced decision +pql decisions list --type question # Open questions ``` diff --git a/docs/briefings/stig.md b/docs/briefings/stig.md index 360822b0d..c8295a5a7 100644 --- a/docs/briefings/stig.md +++ b/docs/briefings/stig.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned stig` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned stig` ## Status **ACTIVE.** v0.2 brings a character customisation UI requirement and removes the dual-character variant for insert/HUD. diff --git a/docs/briefings/tiger.md b/docs/briefings/tiger.md deleted file mode 100644 index f5dc264c7..000000000 --- a/docs/briefings/tiger.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Tiger - Project Briefing" -description: "Localization agent on standby — covers tagged line pool translation scope, culture voice card translation units, and Van Maanen's Star naming transliteration guides" -type: briefing -status: active -agent: Tiger -updated: 2026-03-13 ---- - -# Tiger - Project Briefing -Last updated: 2026-03-13 - -## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned tiger` - -## Status -STANDBY. This briefing will be populated when localization work begins. - -## Key Decisions for Your Domain -- D-016: Internal monologue system - localization of character voice and personality through text -- D-021: Official title "The Settled Reach" -- Q-008: Licensing (open question - affects localization scope and supported languages) -- **D-028: Tagged line pool dialogue system** — Localization advantages: each line is tagged with context metadata (access tier, relationship level, trust threshold, mood). Translators receive rich context. Lines are pooled, not tree-branched — translation scope is predictable. -- **D-035: Converged tag taxonomy (6+3)** — 9 tags per line provide rich context for translators. 13 situations, 9 topics, 8 moods. Translation scope predictable from pool size. -- **D-036: Sova Transit District / Van Maanen's Star** — Van Maanen's Star naming conventions (compact, consonant-heavy). Localization note: proper nouns and slang terms need transliteration guides per target language. -- **D-121: Voice is culture-driven, job as modifier** — localization implication: culture voice cards are the primary translation unit, not individual NPC profiles. Single tycoon character type in v0.2 (D-117) reduces monologue translation scope vs. prior smuggler/detective dual-pool model (D-032 superseded for v0.2). - -## Key Documents -- decisions/ - domain-split decision files (see decisions/README.md for index) -- docs/discussions/ - archived design rounds diff --git a/docs/briefings/troblum.md b/docs/briefings/troblum.md index 4c3187d45..78abba322 100644 --- a/docs/briefings/troblum.md +++ b/docs/briefings/troblum.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned troblum` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned troblum` ## Decisions Relevant to Your Role - D-010: Architectural baseline - four principles diff --git a/docs/briefings/tyre.md b/docs/briefings/tyre.md index fdc4595a5..6900b19bf 100644 --- a/docs/briefings/tyre.md +++ b/docs/briefings/tyre.md @@ -11,8 +11,8 @@ updated: 2026-03-13 Last updated: 2026-03-13 ## Current Project State -Check active milestones: `tooling/db/ticket milestone list` -Check your tickets: `tooling/db/ticket list --assigned tyre` +Check active work: `pql ticket list --status in_progress` +Check your tickets: `pql ticket list --assigned tyre` ## Decisions Relevant to Your Role Read `decisions/architecture.md` (primary) and `decisions/perception.md` (secondary). Key decisions: diff --git a/docs/design/diagram-wireframe-conventions.md b/docs/design/diagram-wireframe-conventions.md new file mode 100644 index 000000000..b8333acdf --- /dev/null +++ b/docs/design/diagram-wireframe-conventions.md @@ -0,0 +1,30 @@ +# Diagram & wireframe conventions (Settled Reach) + +The `/d2-diagram` and `/frame0-wireframe` skill *mechanics* now live at user scope +(`~/.claude/skills/`), de-contaminated of repo-specifics. This file preserves the +Settled-Reach tailoring the local skill copies carried, so it isn't lost when they're +deleted (CLAUDE-CLEANUP §2 / T-1101). + +## d2 diagrams (`/d2-diagram`) + +**Output taxonomy:** `.d2` source + `.png`/`.svg` renders under +`docs/diagrams/{category}/{name}.d2` — categories in use: `architecture/`, +`data-flow/`, `design/`, `entity/`, `state/`, `ui/`. Create/update a diagram +whenever a D-record is added or modified. + +**Agent routing** (who to task with a diagram, by domain): +- **Qatux** — architecture decision records, system-overview + data-flow diagrams. +- **Tyre** — IPC bridge, ECS system flow, chunk-loading pipeline, perception internals. +- **Araminta** — UI navigation flow, screen transitions, component hierarchy. + +## Frame0 wireframes (`/frame0-wireframe`) + +**Output taxonomy:** wireframe JSON (source of truth) + exported PNG under +`docs/design/wireframes/{category}/{name}.json` — categories: +`character-creation/`, `dialogue/`, `hud/` (incl. the neural-insert HUD +wireframes `v01-insert-basic.json`, `v10-insert-full.json`), `menus/`, +`monologue/`, `navigator/`, `popups/`. Palette/style source is +`docs/design/visual-grammar-v01.md`. + +**Agent routing:** Araminta (art direction / visual consistency) owns wireframe review; +Stig (UI dev) consumes them for implementation. diff --git a/governance/README.md b/governance/README.md index ab9603c2a..f5aec5f82 100644 --- a/governance/README.md +++ b/governance/README.md @@ -462,3 +462,4 @@ line in place — keep the Q-record for the audit trail rather than deleting it. - [R-010: protobuf for client-server serialization](rejected/architecture.md#r-010-protobuf-for-client-server-serialization) — _architecture_ - [R-011: Single currency for Phase 2 economics](rejected/economics.md#r-011-single-currency-for-phase-2-economics) — _economics_ - [R-012: Overheard NPC conversation system (D-078) — scrapped](rejected/perception.md#r-012-overheard-npc-conversation-system-d-078--scrapped) — _perception_ +- [R-013: Localization / i18n](rejected/scope.md#r-013-localization--i18n) — _scope_ diff --git a/governance/rejected/scope.md b/governance/rejected/scope.md new file mode 100644 index 000000000..923083aa4 --- /dev/null +++ b/governance/rejected/scope.md @@ -0,0 +1,8 @@ +# Rejected Alternatives — Scope + +Rejected proposals in the **scope** domain, rationale preserved for the audit trail. + +### R-013: Localization / i18n +- **Rejected:** 2026-07-13 +- **Cross-reference:** D-123 (content.md — Generative AI for NPC content), D-138 (content.md — LLM re-voicing pipeline); roster removal in `.claude/agents/README.md` + `TEAM.md` +- **Reason:** Full localization — an i18n framework, translation management, and the volume of template writing the Settled Reach's procedurally-composed text would demand — is prohibitively expensive for a solo developer, and compounds against the Gemma-based naming/text generation: every generated string would need a parallel translation pathway. The game ships English-only. The `tiger` (Translator) agent persona was removed with this decision (see `.claude/agents/README.md` and TEAM.md). Revisit only if project scope and resourcing change fundamentally. diff --git a/tooling/db/common.py b/tooling/db/common.py index 27223c48b..87bcfb463 100644 --- a/tooling/db/common.py +++ b/tooling/db/common.py @@ -75,24 +75,25 @@ def get_base_url(key: str, default: str) -> str: return load_config().get(key, default) -def get_api_key(env_var: str, config_key: str) -> str: - """Get an API key from the environment or config.json. +def get_api_key(env_var: str, config_key: str = "") -> str: + """Get an API key from the environment — environment-only, by design. - Checks the ``env_var`` environment variable first, then ``config_key`` in - config.json. Prints a JSON error and exits 1 if neither is set — connector - scripts emit machine-readable JSON on all paths. + ``tooling/db/config.json`` is a *tracked* file and holds endpoints only; + it must never carry secrets, so there is deliberately no config.json + fallback here (the old one steered users toward committing paid API keys). + ``config_key`` is retained in the signature for caller compatibility but + is ignored. Prints a JSON error and exits 1 if the variable is unset — + connector scripts emit machine-readable JSON on all paths. """ key = os.environ.get(env_var) if key: return key - try: - with open(CONFIG_PATH) as f: - config = json.load(f) - return config.get(config_key, "") - except Exception: - pass print(json.dumps({ "ok": False, - "error": f"No {env_var} found in environment or config.json" + "error": ( + f"{env_var} not set. Export it in your shell or add it to the " + "machine-local .claude/settings.local.json env block (untracked). " + "Never put keys in tooling/db/config.json — it is tracked." + ) }, indent=2)) sys.exit(1) diff --git a/tooling/db/image_connector.py b/tooling/db/image_connector.py index e6a8d7a81..83a58b41a 100755 --- a/tooling/db/image_connector.py +++ b/tooling/db/image_connector.py @@ -2,7 +2,7 @@ """ Gemini image generator connector — direct API wrapper. -Generates images via Google's Gemini 2.0 Flash image generation API. +Generates images via Google's gemini-2.5-flash-image generation API. API key from GEMINI_API_KEY env var or config.json. Usage: @@ -87,13 +87,13 @@ def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, # Add input image if provided (image-to-image) if input_image: if not os.path.isfile(input_image): - print(json.dumps({"ok": False, "error": f"Input image not found: {input_image}"}), indent=2) + print(json.dumps({"ok": False, "error": f"Input image not found: {input_image}"}, indent=2)) sys.exit(1) with open(input_image, "rb") as f: image_data = base64.b64encode(f.read()).decode("utf-8") # Detect mime type ext = os.path.splitext(input_image)[1].lower() - mime = {"png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", + mime = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp"}.get(ext, "image/png") parts.append({ "inlineData": { @@ -102,20 +102,25 @@ def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, } }) - # Build enhanced prompt with aspect ratio and size hints + # Build enhanced prompt with a size hint. Unlike aspect ratio below, Gemini + # has no dedicated resolution parameter for this model — this is a + # best-effort prompt hint only and may not be honored. enhanced_prompt = prompt - if aspect_ratio and aspect_ratio != "1:1": - enhanced_prompt += f" Aspect ratio: {aspect_ratio}." if image_size: enhanced_prompt += f" Resolution: {image_size}." parts.append({"text": enhanced_prompt}) + generation_config = {"responseModalities": ["TEXT", "IMAGE"]} + if aspect_ratio: + # Real API parameter (not a prompt hint). Valid values: 1:1, 3:2, + # 2:3, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9. + # https://ai.google.dev/gemini-api/docs/image-generation + generation_config["imageConfig"] = {"aspectRatio": aspect_ratio} + payload = json.dumps({ "contents": [{"parts": parts}], - "generationConfig": { - "responseModalities": ["TEXT", "IMAGE"], - } + "generationConfig": generation_config }) req = urllib.request.Request( @@ -142,7 +147,7 @@ def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, }, indent=2)) sys.exit(1) except Exception as e: - print(json.dumps({"ok": False, "error": str(e)}), indent=2) + print(json.dumps({"ok": False, "error": str(e)}, indent=2)) sys.exit(1) # Extract image data from response diff --git a/tooling/godot-cold-parse b/tooling/godot-cold-parse new file mode 100755 index 000000000..053069685 --- /dev/null +++ b/tooling/godot-cold-parse @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# tooling/godot-cold-parse [--run-menu] — cold-cache headless parse check. +# +# Used by /pr-process step 1c before push. Deletes the cached script-class +# registry so the parse simulates the cold-start ordering CI / fresh clones +# see: Sprint 36 close caught a new `class_name MetaScreen` base class and +# six extending scripts that parsed fine on warm developer caches but hit +# `Could not find base class "MetaScreen"` post-merge, because the +# autoload-vs-class_name registration order only resolves correctly once the +# class cache is seeded (see CLAUDE.md -> GDScript conventions -> Autoload +# parse-order rule). +# +# Godot's resource scanner emits category errors (e.g. "Export type can only +# be built-in, a resource, a node, or an enum") that do NOT always prefix +# with SCRIPT ERROR — they appear as plain ERROR lines. The filter below +# catches both, then drops known pre-existing noise from the autoload +# class_name parse-order trap. Sprint 36 shipped a scanner error the old +# narrower grep missed; this is why the filter stays wide. +# +# --run-menu: also launch main_menu.tscn briefly (for branches with UI changes). +# +# Exit 0 + "clean" if no matches. Exit 1 + the matched lines if any are found. +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +RUN_MENU=false +[ "${1:-}" = "--run-menu" ] && RUN_MENU=true + +rm -f "$REPO_ROOT/client/.godot/global_script_class_cache.cfg" + +# A truly cold checkout (fresh clone or worktree — .godot/ is gitignored) has +# no resource-import cache, and every imported asset (fonts, ogg) then "fails +# loading" during the parse run: a wall of false positives. Seed the cache +# with an import pass first; source assets are tracked, so this is always +# reconstructible. (Found live: first run in a fresh worktree, 2026-07-13.) +if [ ! -d "$REPO_ROOT/client/.godot/imported" ] || [ -z "$(ls -A "$REPO_ROOT/client/.godot/imported" 2>/dev/null)" ]; then + echo "godot-cold-parse: no import cache — running one-time import pass..." >&2 + set +e + IMPORT_OUT=$(godot --headless --path "$REPO_ROOT/client" --import 2>&1) + IMPORT_EXIT=$? + set -e + if [ "$IMPORT_EXIT" -ne 0 ]; then + echo "godot-cold-parse: import pass exited $IMPORT_EXIT" >&2 + printf '%s\n' "$IMPORT_OUT" | tail -20 >&2 + exit "$IMPORT_EXIT" + fi +fi + +FILTER='^(SCRIPT )?ERROR|Parse Error|Export type' + +# Capture the godot run separately from the filter pipeline: with the +# trailing `|| true` on the greps, a nonzero exit from godot itself (crash, +# missing binary, corrupted install) would otherwise report "clean". Nothing +# downstream re-reads the raw output now that this is scripted, so fail loud. +set +e +RAW=$(godot --headless --path "$REPO_ROOT/client" --quit 2>&1) +GODOT_EXIT=$? +set -e +if [ "$GODOT_EXIT" -ne 0 ]; then + echo "godot-cold-parse: godot itself exited $GODOT_EXIT — not a parse verdict" >&2 + printf '%s\n' "$RAW" | tail -20 >&2 + exit "$GODOT_EXIT" +fi + +MATCHES=$(printf '%s\n' "$RAW" \ + | grep -iE "$FILTER" \ + | grep -v "Failed loading resource: res://assets" \ + | grep -v "Cannot infer the type" \ + | grep -vE '(Messagepack|LocalBridge|ServerProcess|Constants)" not declared' || true) + +if [ "$RUN_MENU" = true ]; then + # Deliberately no exit-code check here: `timeout` kills the menu after + # 10s by design (exit 124 is the expected shutdown path); only the + # scraped error lines carry signal for this bounded run. + MENU_MATCHES=$(timeout 10 godot --path "$REPO_ROOT/client" res://scenes/main_menu.tscn 2>&1 \ + | grep -iE "$FILTER" || true) + if [ -n "$MENU_MATCHES" ]; then + MATCHES="$MATCHES +$MENU_MATCHES" + fi +fi + +if [ -n "$MATCHES" ]; then + echo "$MATCHES" + exit 1 +fi +echo "godot-cold-parse: clean" diff --git a/tooling/pr-watchlist-diff b/tooling/pr-watchlist-diff new file mode 100755 index 000000000..42ac88d56 --- /dev/null +++ b/tooling/pr-watchlist-diff @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# tooling/pr-watchlist-diff <base> <head> — list watch-list files changed +# between <base> and <head>. +# +# Used by /pr-process step 4a (T-858) to decide whether `make regen-db` must +# run before push. The stamped generator sources come from the shared +# registry tooling/generator_sources.py (T-1067) — imported live here so this +# list can't drift from the stamp writer / tooling/check-systems-db-stamp. +# +# The extra hardcoded paths below are non-stamped watch items: the surviving +# one-time planet-gen importers (import_heightmaps.py, import_province_ +# boundaries.py — not part of `make regen-db`, but their data feeds the +# committed DB), the schema DDL (stamped separately via schema_sha), and the +# wiki data directories that feed the generators. +set -euo pipefail + +BASE="${1:?usage: tooling/pr-watchlist-diff <base> <head>}" +HEAD="${2:?usage: tooling/pr-watchlist-diff <base> <head>}" + +# Load the registry via plain assignment (set -e sees its failure), not +# `mapfile < <(...)` — a failed process substitution is invisible to set -e +# and would silently yield an empty watch list, disabling the DB-staleness +# net exactly when the shared registry breaks. Guard the empty case too. +SOURCES_RAW="$(python3 tooling/generator_sources.py --list)" +if [ -z "$SOURCES_RAW" ]; then + echo "pr-watchlist-diff: generator_sources.py --list returned nothing" >&2 + exit 1 +fi +mapfile -t GENERATOR_SOURCES <<< "$SOURCES_RAW" + +git diff --name-only "$BASE...$HEAD" -- \ + "${GENERATOR_SOURCES[@]}" \ + tooling/planet-gen/import_heightmaps.py \ + tooling/planet-gen/import_province_boundaries.py \ + server/data/systems-schema.sql \ + wiki/star-systems/ \ + wiki/economics/