Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/pixellab-cli-assets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ recovering a stopped run, and the free local tooling every category uses.
- Free or cheap, reach for them freely: `balance`, `ledger`, `character list|show|sheet`,
`object list`, `recipe list`, every `image` command, every `export` command,
`config show|path`.
- **Pro Tools, twenty to forty generations a call:** `object new`, `ui`, `inpaint`,
- **Pro Tools, twenty to forty generations a call:** `object new`, `ui new`, `inpaint`,
`tiles variants`, `character state`, `outfit`, `portrait`, `sprite` with more than one
`--style`, and `edit` with more than one image. They say so before calling. Never run
one without agreement.
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/pixellab-cli-assets/references/costs.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public pricing; the tool records what each call actually reported.
| Base | ~1 | `sprite`, `animate`, `character animate` (per direction), `edit` with one image, `tiles isometric`, `tiles prop` |
| Character | ~3–4 | `character new`, `rotate` |
| Tilesets | ~3 | `tiles terrain`, `tiles platform` |
| Pro Tools | 20–40 | `object new`, `ui`, `inpaint`, `tiles variants`, `character state`, `outfit`, `portrait`, `interpolate`, `sprite` with several `--style`, `edit` with several images or `--match` |
| Pro Tools | 20–40 | `object new`, `ui new`, `inpaint`, `tiles variants`, `character state`, `outfit`, `portrait`, `interpolate`, `sprite` with several `--style`, `edit` with several images or `--match` |
| Fonts | 25 fixed | `font` |
| By generation time | 1–6 | `animate` above sixteen frames, which is `animate-pixminimax`: beta, tier 1 and above, and the one route whose estimate here is rough |
| fal | unpriced | `art concept`, `art anchor`, `art boxart`, `art edit` — fal reports no usage and publishes no price, so the ledger records these as unknown rather than as a number nobody checked |
Expand Down Expand Up @@ -70,7 +70,7 @@ Interpolate knows both ends and fills the middle, on a Pro route, at most 128 pe
and picks the count itself. If they described where the motion ends as concretely as where
it starts — shut to open, car to robot — it is interpolate.

**`ui` against `sprite --style`.** A panel is one picture holding a whole layout, for Pro
**`ui new` against `sprite --style`.** A panel is one picture holding a whole layout, for Pro
pricing. Several small repeated pieces — inventory slots, icon buttons — come back as
separate sprites from a style reference, which is usually what was actually wanted.

Expand Down Expand Up @@ -103,5 +103,5 @@ conversion. `pixellab-cli-images` carries the full decision.
- `animate` and `character animate`: at most 256 per side, and width times height times
frames may not exceed 524,288 — so 256x256 stops at eight frames.
- `inpaint`: 32 to 512 per side.
- `ui`: 192 to 688 per axis, the maximum depending on the aspect.
- `ui new`: 192 to 688 per axis, the maximum depending on the aspect.
- `sprite` with several `--style`: no size at all — it is read off the style images.
2 changes: 1 addition & 1 deletion .claude/skills/pixellab-cli-interface/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ With neither `--element` nor `--piece` the result is one full-canvas rounded pan

## A panel, or one element

`ui` makes a **panel** by default, and moves to the single-element route
`ui new` makes a **panel** by default, and moves to the single-element route
(`generate-ui-v2`) on either of the two signals that mean a panel was never possible or
never intended:

Expand Down
51 changes: 51 additions & 0 deletions plans/the-cost-tables-name-ui-new.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
autonomy: auto
ci: wait
---

# The cost tables name ui new

`ui` became a group in PR #79 — `ui new` generates, `ui list` and `ui show` read the
account back — and the rename reached the command help, the README and the wiki but not
the places that name `ui` as a price. The harness block and the cost reference still
list `ui` among the Pro Tools routes, which now reads as though the group costs twenty
to forty generations a call when two of its three commands are free.

## Why

The harness block is read every session by an agent that may never load a skill, and
`references/costs.md` is what an agent consults before quoting a price. Both now name a
command that does not exist as a paid route. An agent that believes `ui list` costs
thirty generations will not run it, and reading the account back for nothing is the
whole point of the command it will avoid.

## Paths

- `src/pixellab_cli/harness.py`
- `src/pixellab_cli/skill/pixellab-cli-assets/SKILL.md`
- `src/pixellab_cli/skill/pixellab-cli-assets/references/costs.md`
- `src/pixellab_cli/skill/pixellab-cli-interface/SKILL.md`
- `.claude/skills/` — the installed copies of the same files

- `src/pixellab_cli/commands/interface.py` — one docstring naming the command rather than the group

## References

- `specs/interface-portrait-and-font/` — R1.5 and R1.6, which made `ui` a group
- `specs/agent-skill/` — R5.2, the entry skill carrying the rules that cost money if missed

## Out of scope

- Any change to what the commands do. This is what they are called and what they cost.
- The generating form's own price, which is unchanged at Pro Tools.

## Tasks

- [x] 1.1 (Unit) Name `ui new` wherever a price is quoted, and sync the installed copies
- [x] 1.2 (Unit) Refuse a price list that names a group rather than a command
_Reason nothing would catch a third rename leaving a price behind_

## Done when

- No file under `src/` or `.claude/skills/` quotes a price for `ui` rather than `ui new`
- `pixellab-cli setup` writes a harness block naming `ui new`
2 changes: 1 addition & 1 deletion src/pixellab_cli/commands/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def _choose_ui_route(
) -> str:
"""A panel unless a panel was never possible or never intended (R1.3).

The default is unchanged, because a panel is what `ui` has always made. The two
The default is unchanged, because a panel is what this command has always made. The two
signals that move it are the ones a panel route cannot answer: a size beneath its
floor, and a concept image it has no slot for.
"""
Expand Down
2 changes: 1 addition & 1 deletion src/pixellab_cli/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def block_body(references: Path | None) -> str:
started from, and what the subject has cost. A `--start-pose` from the wrong character
is accepted by the route, charged per frame per direction, and comes back wrong.

- **Pro Tools cost twenty to forty generations a call:** `object new`, `ui`, `inpaint`,
- **Pro Tools cost twenty to forty generations a call:** `object new`, `ui new`, `inpaint`,
`tiles variants`, `character state`, `outfit`, `sprite` with more than one `--style`,
and `edit` with more than one image.
- **A failed generation is charged.** `pixellab-cli ledger` lists what was spent, and
Expand Down
2 changes: 1 addition & 1 deletion src/pixellab_cli/skill/pixellab-cli-assets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ recovering a stopped run, and the free local tooling every category uses.
- Free or cheap, reach for them freely: `balance`, `ledger`, `character list|show|sheet`,
`object list`, `recipe list`, every `image` command, every `export` command,
`config show|path`.
- **Pro Tools, twenty to forty generations a call:** `object new`, `ui`, `inpaint`,
- **Pro Tools, twenty to forty generations a call:** `object new`, `ui new`, `inpaint`,
`tiles variants`, `character state`, `outfit`, `portrait`, `sprite` with more than one
`--style`, and `edit` with more than one image. They say so before calling. Never run
one without agreement.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public pricing; the tool records what each call actually reported.
| Base | ~1 | `sprite`, `animate`, `character animate` (per direction), `edit` with one image, `tiles isometric`, `tiles prop` |
| Character | ~3–4 | `character new`, `rotate` |
| Tilesets | ~3 | `tiles terrain`, `tiles platform` |
| Pro Tools | 20–40 | `object new`, `ui`, `inpaint`, `tiles variants`, `character state`, `outfit`, `portrait`, `interpolate`, `sprite` with several `--style`, `edit` with several images or `--match` |
| Pro Tools | 20–40 | `object new`, `ui new`, `inpaint`, `tiles variants`, `character state`, `outfit`, `portrait`, `interpolate`, `sprite` with several `--style`, `edit` with several images or `--match` |
| Fonts | 25 fixed | `font` |
| By generation time | 1–6 | `animate` above sixteen frames, which is `animate-pixminimax`: beta, tier 1 and above, and the one route whose estimate here is rough |
| fal | unpriced | `art concept`, `art anchor`, `art boxart`, `art edit` — fal reports no usage and publishes no price, so the ledger records these as unknown rather than as a number nobody checked |
Expand Down Expand Up @@ -70,7 +70,7 @@ Interpolate knows both ends and fills the middle, on a Pro route, at most 128 pe
and picks the count itself. If they described where the motion ends as concretely as where
it starts — shut to open, car to robot — it is interpolate.

**`ui` against `sprite --style`.** A panel is one picture holding a whole layout, for Pro
**`ui new` against `sprite --style`.** A panel is one picture holding a whole layout, for Pro
pricing. Several small repeated pieces — inventory slots, icon buttons — come back as
separate sprites from a style reference, which is usually what was actually wanted.

Expand Down Expand Up @@ -103,5 +103,5 @@ conversion. `pixellab-cli-images` carries the full decision.
- `animate` and `character animate`: at most 256 per side, and width times height times
frames may not exceed 524,288 — so 256x256 stops at eight frames.
- `inpaint`: 32 to 512 per side.
- `ui`: 192 to 688 per axis, the maximum depending on the aspect.
- `ui new`: 192 to 688 per axis, the maximum depending on the aspect.
- `sprite` with several `--style`: no size at all — it is read off the style images.
2 changes: 1 addition & 1 deletion src/pixellab_cli/skill/pixellab-cli-interface/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ With neither `--element` nor `--piece` the result is one full-canvas rounded pan

## A panel, or one element

`ui` makes a **panel** by default, and moves to the single-element route
`ui new` makes a **panel** by default, and moves to the single-element route
(`generate-ui-v2`) on either of the two signals that mean a panel was never possible or
never intended:

Expand Down
40 changes: 40 additions & 0 deletions tests/test_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,43 @@ def test_every_packaged_skill_is_named_here(self):
from pixellab_cli.harness import packaged_skills

assert set(packaged_skills()) == set(ALL_SKILLS)


class TestAPriceNamesACommand:
"""`ui` was listed as a Pro Tools route, and then `ui` became a group whose other
two commands are free. Nothing caught it, because a group's name reads as a command
everywhere else in this file. A price is quoted against what a person types, so
every name in a price list has to be a leaf command."""

PRICED = re.compile(r"Pro Tools[^\n|]*[:|]([^\n]*)")

def priced_names(self, body: str) -> set[str]:
"""Every backticked command named on a line that quotes the Pro Tools price."""
named: set[str] = set()
for line in self.PRICED.findall(body):
for quoted in re.findall(r"`([^`]+)`", line):
# `sprite` with several `--style` is one entry in two spans: a flag is
# not a command and is not what this is checking.
if not quoted.startswith("-"):
named.add(quoted)
return named

def sources(self) -> dict[str, str]:
from pixellab_cli.harness import block_body

return {
"the harness block": block_body(None),
"the entry skill": entry_text(),
"the cost reference": (REFERENCES / "costs.md").read_text(encoding="utf-8"),
}

def test_each_source_quotes_a_price_against_something(self):
"""A regex that matches nothing would pass every assertion below."""
for where, body in self.sources().items():
assert self.priced_names(body), f"no priced command found in {where}"

def test_every_priced_name_is_a_command_somebody_can_type(self):
commands = cli_commands()
for where, body in self.sources().items():
for name in sorted(self.priced_names(body)):
assert name in commands, f"{where} prices {name!r}, which is not a command"
Loading