From claude-commands
Defines architecture boundaries for level-up, rewards, and XP logic — file ownership, API contracts, and correction guard rules. Consult before any rewards/XP code change.
How this skill is triggered — by the user, by Claude, or both
Slash command
/claude-commands:zfc-leveling-roadmapThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill is the level-up/rewards specialization of the general ZFC contract.
This skill is the level-up/rewards specialization of the general ZFC contract.
Before applying these file boundaries, read
.claude/skills/zero-framework-cognition/SKILL.md for the repository-wide
rules: models make semantic decisions, new regex/keyword/heuristic semantic
routing is banned, and model contracts should request minimal semantic fields
instead of redundant aliases.
Before accepting a backend correction guard, also apply
.claude/skills/root-cause-first/SKILL.md: prove the selected model path
received the right prompt/schema, fix missing or ambiguous prompt/schema
instructions first, and treat backend correction as a last resort.
"Model Computes, Backend Formats" — the LLM decides level-up;
rewards_engine.pyformats it;world_logic.pywraps the formatted result with modal semantics.
"LLM Calculates, Backend Corrects" — the LLM owns XP arithmetic, target-level math, level-up availability, and choice/benefit selection. Backend code must not independently calculate those facts from thresholds as a primary path. It may only correct or suppress mechanically contradictory explicit model/state payloads.
Correction Exception Boundary: Correction guards that prevent degraded UX (stale-flag time-freeze, SSE soft-lock, already-applied level-up residue) are permitted only within
rewards_engine.py. These guards must be correction-only: compare explicit model/state fields for contradictions, suppress or normalize invalid payloads, and avoid creating a competing backend level-up decision. Scattered heuristics across multiple files are strictly forbidden.
Opaque Choice Boundary: Planning choice IDs are opaque handles, not semantic signals.
level_up_now,finish_level_up_return_to_game, andcustom_actionare migration/compatibility handles only. Level-up choice meaning must come from explicit model/schema fields on the resolved choice object, not from ID text, prefixes, labels, or backend allowlists.
Before modifying ANY file for level-up/rewards/XP work, verify against this table. If your change puts logic in a "MUST NOT DO" column → STOP and redesign.
| File | OWNS | MUST NOT DO |
|---|---|---|
rewards_engine.py | Format explicit model signals, correction-only contradiction guards, canonicalize_level_up_signal(), XP/reward field normalization from explicit values, sync_level_up_signals(), SSE atomicity enforcement (_enforce_atomicity()) | Model calls, Firestore writes, text interpretation, primary XP/level calculation, threshold-derived level-up decisions |
world_logic.py | Modal locks, finish choice injection, wrap precomputed payloads only, thin delegation to rewards_engine.project_level_up_ui() | Recompute XP, synthesize choices, call canonicalize_level_up_signal() directly, build level_up_signal dicts, call resolve_level_progression() |
game_state.py | State storage, deterministic mechanical primitives (xp_needed_for_level, level_from_xp, coerce_int) | Decide level-up from state, replace model choices, own level-up decisions, contain level-up dataclasses or resolvers |
agents.py | Route to correct agent using rewards_engine.is_level_up_active() | Inline flag interpretation, XP extraction, stale-flag recovery logic |
llm_parser.py | Parse model output, call the pipeline once via canonicalize_rewards(), persist/deliver | Duplicate rewards formatting, inline false-positive mutations, call canonicalization separately |
narrative_response_schema.py | Preserve model output losslessly, contradiction detection on model output boundary (e.g., level_up=true but current_turn_exp < total_exp_for_next_level) | Class feature synthesis, state mutation, building UI payloads |
llm_service.py | LLM request/response pipeline, retry on SchemaRejectionError | Rewards formatting, level-up flag interpretation, modal state mutation |
structured_fields_utils.py | Extract non-empty structured fields | Semantic validation, alias normalization, UI formatting |
These are the only rewards_engine functions that external files may call:
| Function | Callers | Purpose |
|---|---|---|
canonicalize_rewards(structured_fields, game_state, original_state) | llm_parser.py (1 call), world_logic.py (polling paths) | Single convergence point for all rewards/level-up UI |
is_level_up_active(game_state) | agents.py, world_logic.py | Correction-guarded modal activity check over explicit state/model fields |
project_level_up_ui(game_state, original_state) | world_logic.py (via _project_level_up_ui_locally()) | Polling/projection path (transitional — delete when callers migrate) |
sync_level_up_signals(game_state, original_state) | world_logic.py (via ensure_level_up_rewards_pending() shim) | Idempotent rewards_pending sync |
format_model_level_up_signal(signal) | llm_parser.py (fallback path) | Explicit signal formatter (should become private _format_...) |
All other functions are private implementation details. Do not call _canonicalize_core(), _enforce_atomicity(), _apply_contradiction_guard(), resolve_level_up_signal(), or ensure_rewards_box() from outside rewards_engine.py.
canonicalize_rewards() returns (rewards_box, planning_block) — a 2-tuple. Do NOT change this return signature. Use out_meta dict parameter to pass additional data out (e.g., canonical signal, false-positive flags).canonicalize_level_up_signal() is called inside rewards_engine.py only. External files consume precomputed results.is_level_up_active() returns bool. It performs correction-only stale-flag/contradiction suppression internally. It must not become a primary XP-threshold calculator. Callers must not second-guess its result.ensure_rewards_box() returns tuple[dict | None, dict | None] (rewards_box, signal_metadata). Internal only.These are the most frequently observed agent drift patterns. If you catch yourself doing any of these, STOP:
| If you're about to... | WRONG file | RIGHT approach |
|---|---|---|
Call canonicalize_level_up_signal() | world_logic.py, llm_parser.py | Only inside rewards_engine.py (via _canonicalize_core) |
Build a level_up_signal dict | world_logic.py | Build in rewards_engine.py, pass via return or out_meta |
| Compute XP progress, thresholds, target level, or level-up availability as the primary path | any backend file | Ask the LLM for explicit calculated fields; rewards_engine.py may only correct/suppress contradictions in those explicit fields |
Check level_up_pending / level_up_in_progress flags directly | world_logic.py, agents.py | Call rewards_engine.is_level_up_active() |
Call resolve_level_progression() | anywhere | Deleted. Use rewards_engine.is_level_up_active() or rewards_engine.project_level_up_ui() |
Use build_level_up_rewards_box() | anywhere | Deleted. Use rewards_engine.ensure_rewards_box() (internal) or project_level_up_ui() |
Change canonicalize_rewards() return type | anywhere | Don't. Use out_meta dict instead |
| Add signal normalization aliases | structured_fields_utils.py | Add in rewards_engine.canonicalize_level_up_signal() |
| Classify or scrub choices by ID text/prefix, label, enum name, or CSS class | any backend file | Resolve the submitted handle against persisted planning_block.choices, then read explicit structured fields; keep level_up_now, finish_level_up_return_to_game, and custom_action exact checks migration-only |
| Add a new feature to a level-up PR | the same PR | Open a separate PR — scope creep is the #1 cause of spin |
Access rewards_engine._is_state_flag_true() from outside | agents.py, world_logic.py | Use the public helpers or import from game_state.py |
Run this before writing any code for a PR that touches level-up, rewards, XP, or canonicalization logic.
For each file you plan to modify, check the table above:
canonicalize_rewards()? → Don't. Use out_meta.rewards_engine.* from world_logic.py outside the authorized API surface table? → Check boundary table.level_up_pending / level_up_in_progress flags instead of calling is_level_up_active()? → Use the centralized function.level_up_now, finish_level_up_return_to_game, custom_action) require a migration note and must not become the pattern for new behavior.game_state.resolve_level_progression_state()game_state.resolve_level_progression()game_state.ensure_level_up_rewards_pending()game_state.LevelProgressionStategame_state.PendingRewardsStateworld_logic.build_level_up_rewards_box()agents._is_stale_level_up_pending()~/roadmap/zfc-pr-task-specs-2026-04-22.md?Is my change about level-up/XP/rewards logic?
├── YES → Put the logic in rewards_engine.py
│ ├── Does world_logic.py need the result?
│ │ └── Pass via return value or out_meta dict
│ ├── Does agents.py need to know if level-up is active?
│ │ └── Call rewards_engine.is_level_up_active() — never inline flag checks
│ └── Does llm_parser.py need the result?
│ └── Call canonicalize_rewards() once; don't call internal functions directly
├── Is my change about validating LLM output?
│ └── Contradiction detection → narrative_response_schema.py
│ └── Schema retry → llm_service.py SchemaRejectionError
└── NO → Normal file protocol applies
| Milestone | Status | Description |
|---|---|---|
| M0 | ✅ Complete | Delete legacy paths, net LOC ≤ 0 |
| M1 | ✅ Complete | Model compliance probe — prompt contract + streaming evidence |
| M1b | ✅ Complete | Repeated-run reliability harness (Wilson score intervals) |
| M2 | 🟡 Near Complete | Legacy resolver deletion done; formatter narrowing partially done |
| M3 | 🟢 Partially Started | CI grep gates active via design-doc-gate.yml (8 gates enforced) |
game_state.py — all legacy resolvers and dataclasses deleted (422 LOC)world_logic.py — build_level_up_rewards_box() deleted, all resolve_level_progression() calls removedagents.py — _is_stale_level_up_pending() deleted, all routing uses is_level_up_active()rewards_engine.sync_level_up_signals() replaces 200-line ensure_level_up_rewards_pending()_enforce_atomicity() rewritten for semantic SSE validation (fail-closed)current_turn_exp, total_exp_for_next_levelensure_rewards_box() returns tuple[dict|None, dict|None], field resolved_target_level → new_levelformat_model_level_up_signal() → make private (_format_model_level_up_signal)project_level_up_ui() → delete when no remaining callers (currently used by _project_level_up_ui_locally())The design-doc-gate.yml workflow enforces 8 grep-based boundary checks on every PR:
| Gate | Rule | Status |
|---|---|---|
world_logic 0 rewards_engine public API imports | No direct from rewards_engine import in world_logic | ✅ Enforced |
world_logic 0 resolve_level_up_signal calls | No direct resolver calls | ✅ Enforced |
constants get_xp_for_level | No duplicate XP helpers in constants | ✅ Enforced |
constants get_level_from_xp | No duplicate level helpers in constants | ✅ Enforced |
_is_state_flag_true 2 files | Helper exists in exactly 2 files | ✅ Enforced |
world_logic ≤1 project_level_up_ui | At most 1 non-wrapper call | ✅ Enforced |
non-rewards 0 direct level-up pair builders | No external pair builders | ✅ Enforced |
llm_parser canonicalize_rewards=1 | Exactly 1 canonicalize call | ✅ Enforced |
def in world_logic.py whose name exists in rewards_engine.pylevel_up_signal formatting stays in rewards_engine.pyEvery shared dict field must have exactly ONE authoritative writer.
| Dict / Field | Writer | Semantic |
|---|---|---|
level_up_signal.current_level | LLM | Int: level before applying the level-up |
level_up_signal.target_level | LLM | Int: target level now available; target_level > current_level is the model-owned level-up signal |
rewards_pending.level_up_available | Backend-derived compatibility/display field from accepted level_up_signal | Bool: deterministic UI alias; do not ask the model to emit it |
rewards_pending.xp_gained | LLM via state_updates; rewards_engine.sync_level_up_signals() only for transitional correction | Int: model-calculated XP delta for rewards box |
rewards_pending.new_level | Backend-derived compatibility/display field from level_up_signal.target_level | Int: deterministic UI alias; do not ask the model to emit it |
player_character_data.experience.current | LLM via state_updates | Int: cumulative XP (backend NEVER writes) |
custom_campaign_state.level_up_in_progress | Server-owned modal/session control state | Bool: level-up modal active; model must not write this on story/rewards turns |
custom_campaign_state.level_up_pending | Backend-derived compatibility/control field from accepted level-up signal or modal state | Bool: modal routing compatibility; model must not write this on story/rewards turns |
custom_campaign_state.level_up_complete | Server-owned level-up flow state | Bool: level-up flow finished (suppresses is_level_up_active()) |
custom_campaign_state.level_up_cancelled | Server-owned level-up flow state | Bool: level-up deferred (suppresses is_level_up_active()) |
For the full field registry, see .claude/skills/field-ownership-contracts.md.
Correction-only validation: is_level_up_active() validates explicit flags against explicit model/state fields only to suppress contradictions. A stale level_up_pending=True can be rejected when the persisted state proves the model-owned level-up signal is already applied or mechanically impossible, but this must not become a backend threshold-derived level-up decision.
SSE atomicity: _enforce_atomicity() enforces that level-up rewards_box and planning_block always ship together. If either is missing, both are dropped (fail-closed). This prevents UI soft-locks.
Completion/cancellation precedence: level_up_complete=True or level_up_cancelled=True always returns False from is_level_up_active(), regardless of other flags.
Level cap: Level 20 characters always return False from is_level_up_active() for pending flags, preventing stale signals at max level.
Single delegation: world_logic._project_level_up_ui_locally() is a 1-line call to rewards_engine.project_level_up_ui(). No independent computation.
roadmap/zfc-level-up-model-computes-2026-04-19.md (repo) / ~/roadmap/zfc-level-up-model-computes-2026-04-19.md (local) (1,725 lines — read lines 219-241 for the file-responsibility table)roadmap/zfc-pr-task-specs-2026-04-22.md (repo) / ~/roadmap/zfc-pr-task-specs-2026-04-22.md (local) (per-PR scope locks)roadmap/2026-04-21-level-up-zfc-loop-postmortem.md (repo) / ~/roadmap/2026-04-21-level-up-zfc-loop-postmortem.md (local)When delegating PR refactors (such as PRs #6718, #6725, #6736, #6756, #6758) to autonomous workers, provide them with this exact prompt to prevent common architectural drift:
# ZFC Leveling Compliance Redesign Prompt
**Objective**: Refactor and redesign existing level-up, streaming orchestration, and infinite progression logic to strictly comply with the new Zero-Framework Cognition (ZFC) mandate: **"LLM Calculates, Backend Corrects."**
---
## 🛑 Core Architectural Mandate
The backend is no longer the source of truth for gameplay math. You must enforce the following invariant across your PR:
**The LLM owns all XP arithmetic, target-level math, and level-up availability.**
The backend code must **never** independently calculate these facts from thresholds as a primary path.
### 1. The "Backend Just Fixes" Rule (Correction Exception Boundary)
The backend is ONLY permitted to apply *correction guards* to prevent degraded UX (e.g., stale-flag time-freezes, SSE soft-locks, or already-applied level-up residues).
* **Where**: These guards are permitted **ONLY** within `$PROJECT_ROOT/rewards_engine.py`.
* **How**: Guards must be "correction-only." They may compare explicit model/state fields for contradictions and suppress or normalize invalid payloads.
* **Forbidden**: Guards must *never* synthesize a competing backend level-up decision or trigger a level-up prompt because backend XP math crossed a threshold. Scattered heuristics across multiple files are strictly forbidden.
### 2. Strict File Boundaries
Before modifying any file, verify your changes against these boundaries. If your logic violates these rules, **STOP and redesign.**
* **`rewards_engine.py`**
* **Owns**: Formatting explicit model signals, correction-only contradiction guards, `canonicalize_level_up_signal()`, and SSE atomicity enforcement.
* **Must NOT Do**: Primary XP/level calculations, threshold-derived level-up decisions, or model calls.
* **`world_logic.py`**
* **Owns**: Modal locks, injecting finish choices, wrapping precomputed payloads.
* **Must NOT Do**: Recomputing XP, synthesizing choices, directly calling `canonicalize_level_up_signal()`, or making threshold-based level-up decisions.
* **`game_state.py`**
* **Owns**: State storage and deterministic mechanical primitives (`xp_needed_for_level`, `level_from_xp`).
* **Must NOT Do**: Deciding level-ups from state, replacing model choices, or containing level-up resolvers.
* **`agents.py`**
* **Owns**: Routing to the correct agent using `rewards_engine.is_level_up_active()`.
* **Must NOT Do**: Inline flag interpretation, XP extraction, or stale-flag recovery logic.
### 3. Infinite Leveling & Streaming Parity Specifics
* If you are removing the Level 20 hard-cap or fixing streaming parity, you must ensure that the progression relies on the LLM's explicit `level_up_signal` (including `previous_turn_exp`, `current_turn_exp`, `new_level`, etc.).
* Do not fallback to having the backend detect `current_turn_exp > threshold` to trigger the level-up modal. The LLM must emit `level_up: true`. If the LLM misses it, the backend fails closed (suppresses the level-up), it does *not* fix it by synthesizing the level-up itself.
### 4. Required Action Items for Your PR
1. **Audit your diff**: Search your PR for any instances where the backend calculates XP progress, checks thresholds to trigger events, or builds level-up choices independently.
2. **Remove backend inference**: Delete any code that tries to act as a "competing source of truth" against the LLM's `level_up_signal`.
3. **Refactor to correction-only**: If your PR fixes a stale state bug, ensure the fix is a contradiction guard placed *only* in `rewards_engine.py` that suppresses the stale payload, rather than recalculating the correct state.
4. **Grep for rogue flag checks**: You must not check `level_up_pending` or `level_up_in_progress` directly in `world_logic.py` or `agents.py`. You must replace these with calls to `rewards_engine.is_level_up_active()`, which contains the centralized correction guards.
5. **Do NOT change the API contract**: `rewards_engine.canonicalize_rewards()` returns a 2-tuple `(rewards_box, planning_block)`. Do not change this signature. Use the `out_meta` dict parameter to pass out additional flags (like false-positives).
> **Failure to adhere to the "LLM Calculates, Backend Corrects" mandate will result in immediate rejection of the PR.** Consult `roadmap/zfc-level-up-model-computes-2026-04-19.md` and `.claude/skills/zfc-leveling-roadmap/SKILL.md` if you are unsure about a specific file boundary.
.claude/skills/field-ownership-contracts.md — full field writer/reader registry~/.claude/skills/evidence-standards.md — evidence requirements for production PRs~/.claude/skills/pr-green-definition.md — 7-green merge-readiness criterianpx claudepluginhub jleechanorg/claude-commands --plugin claude-commandsDeprecated skill that redirects to the consolidated ZFC leveling roadmap skill at .claude/skills/zfc-leveling-roadmap/SKILL.md.
Autonomous multi-agent system that drives projects from PRD to production with zero human intervention. Active via /loki-mode.
Set a 1.0-10.0 engineering working-mode dial - tunes agency, collaboration, assumption budget, decision ownership, questioning, verification, and security posture from fast prototype to maintainer-grade. Use when the user mentions "Linus Level", "LL <n>", asks to set rigor/strictness/maintainer mode, calibrate agent autonomy or coworker mode, or distinguishes prototype vs production / established-codebase / mission-critical work.