From d554f79716381a2c2453c1f2923012b2c4d246e5 Mon Sep 17 00:00:00 2001 From: omartelo Date: Thu, 8 Oct 2026 13:57:40 -0300 Subject: [PATCH] fix(theme): validate against lich's current theme template validate.mjs reads its token sets off skills/theme/template.json, a copy taken before lich 0.53.0 made the app tokens tone-pass and tone-wait required (omartelo/lich#617). It called a current theme's tones unknown and passed a theme without them that lich rejects on import. The template is now refreshed from lich at tests/lich-ref by refresh-fixtures.sh, and the CI drift job diffs it with the fixtures, so the next token lich adds reaches the validator by moving the ref. The skill's token count and role table follow. --- .github/workflows/ci.yml | 4 ++-- CHANGELOG.md | 9 ++++++++ CLAUDE.md | 2 +- skills/theme/SKILL.md | 3 ++- skills/theme/template.json | 3 +++ tests/refresh-fixtures.sh | 10 +++++++-- tests/theme-validate.test.mjs | 41 +++++++++++++++++++++++++++++++++++ 7 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 tests/theme-validate.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c13b3f..497c186 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: run: tests/refresh-fixtures.sh - name: Fail if the vendored copies drifted run: | - git diff --exit-code -- tests/fixtures || { - echo "::error::A hook contract moved in lich. Run tests/refresh-fixtures.sh, then make the scripts match." + git diff --exit-code -- tests/fixtures skills/theme/template.json || { + echo "::error::A hook contract or the theme template moved in lich. Run tests/refresh-fixtures.sh, then make the scripts (or the theme skill) match." exit 1 } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a04bd5..f37eab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **The theme validator knows the status tones.** `validate.mjs` read its + tokens off a template copied before lich 0.53.0 made `tone-pass` and + `tone-wait` required, so it called a current theme's tones unknown and passed + a theme without them that lich rejects on import. The template is now + refreshed from lich at `tests/lich-ref` with the contract fixtures, and CI + fails when the copy drifts; the skill lists the two tokens. + ## [0.18.1] - 2026-10-08 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 0ae7379..138bdc4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ Each doc carries the event mapping for **every** provider, one column each — a ## Skills -- [skills/theme](skills/theme/SKILL.md) — writing, porting and validating lich color themes, as a single file or as a versioned theme repository. Mirrors the theme contract in the lich repository (`docs/themes.md` there); `template.json` is a copy of the starter lich itself hands out, and `validate.mjs` derives its token sets from that copy. A change to the theme shape, or to the repository contract, lands here too. +- [skills/theme](skills/theme/SKILL.md) — writing, porting and validating lich color themes, as a single file or as a versioned theme repository. Mirrors the theme contract in the lich repository (`docs/themes.md` there); `template.json` is a copy of the starter lich itself hands out, vendored at `tests/lich-ref` by `tests/refresh-fixtures.sh` and diffed by CI like the fixtures, and `validate.mjs` derives its token sets from that copy. A change to the theme shape, or to the repository contract, lands here too. ## Tests diff --git a/skills/theme/SKILL.md b/skills/theme/SKILL.md index be27031..bae48fd 100644 --- a/skills/theme/SKILL.md +++ b/skills/theme/SKILL.md @@ -122,7 +122,7 @@ manifest**, commit, push. The user takes it with **Update** on the theme's row. `com1`–`com9`, `lpt1`–`lpt9`) — the id names a file. - `name`: required, non-blank, ≤ 128 characters. - `scheme`: `light` or `dark`. Nothing else. -- `app`: **every one of the 31 tokens** must be present. An unknown token is an +- `app`: **every one of the 33 tokens** must be present. An unknown token is an error, not a warning. - `terminal`: `background` and `foreground` required; the other 20 tokens optional. Unknown tokens rejected. @@ -199,6 +199,7 @@ pair contrasty, they are never used apart): | `muted` / `muted-foreground` | secondary text, resting icons, paths, meta | | `primary` / `primary-foreground` | the single high-emphasis button fill, and the Switch when on | | `destructive` | destructive actions and diff removals — must stay legible as red | +| `tone-pass` / `tone-wait` | status tones: a pass (green) and a wait (amber) on session icons, pull request checks and reviews. Drawn as text and icons on `background`/`card`, so they need contrast there. Required since lich 0.53.0 — a pack that carries them is one an older lich rejects as unknown tokens, which is what `minLichVersion: "0.53.0"` names | | `border` | hairline seams. Translucent works well in dark (`oklch(1 0 0 / 10%)`) | | `input` | input and control edges, usually a touch stronger than `border` | | `ring` | focus ring, always visible | diff --git a/skills/theme/template.json b/skills/theme/template.json index ee2f528..df3e3d3 100644 --- a/skills/theme/template.json +++ b/skills/theme/template.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/omartelo/lich/main/themes/lich-theme.schema.json", "id": "purple-night", "name": "Purple Night", "scheme": "dark", @@ -18,6 +19,8 @@ "accent": "#3a2d57", "accent-foreground": "#f4efff", "destructive": "#ff5c8a", + "tone-pass": "oklch(0.696 0.17 162.48)", + "tone-wait": "oklch(0.769 0.188 70.08)", "border": "#4a3a68", "input": "#554178", "ring": "#c4a7ff", diff --git a/tests/refresh-fixtures.sh b/tests/refresh-fixtures.sh index 3210005..a7e7a95 100755 --- a/tests/refresh-fixtures.sh +++ b/tests/refresh-fixtures.sh @@ -1,5 +1,6 @@ #!/bin/sh -# Refreshes the vendored copies of lich's hook-contract fixtures. +# Refreshes the vendored copies of lich's hook-contract fixtures, and of the +# theme template the theme skill hands out and validates against. # # The fixtures are canonical in the lich repository (docs/hooks/fixtures/ # there). This repository only vendors them so the test suite never needs the @@ -15,7 +16,8 @@ set -eu here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) ref=$(tr -d '[:space:]' <"$here/lich-ref") -base=https://raw.githubusercontent.com/omartelo/lich/$ref/docs/hooks/fixtures +raw=https://raw.githubusercontent.com/omartelo/lich/$ref +base=$raw/docs/hooks/fixtures for file in session-state.jsonl session-start.jsonl session-title.jsonl \ session-touched.jsonl mod-usage.jsonl mod-control.jsonl mod-commands.json \ @@ -24,3 +26,7 @@ for file in session-state.jsonl session-start.jsonl session-title.jsonl \ echo "refreshed $file at $ref" done +# validate.mjs reads its token sets off this copy, so a token lich adds reaches +# the validator by moving tests/lich-ref, not by hand. +curl -fsSL "$raw/themes/template.json" -o "$here/../skills/theme/template.json" +echo "refreshed skills/theme/template.json at $ref" diff --git a/tests/theme-validate.test.mjs b/tests/theme-validate.test.mjs new file mode 100644 index 0000000..11a1853 --- /dev/null +++ b/tests/theme-validate.test.mjs @@ -0,0 +1,41 @@ +// Pins skills/theme/validate.mjs against the app tokens lich requires on +// import: a theme it calls ok must be one lich installs, and the other way round. +// +// Run: node --test tests/ + +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const validator = new URL('../skills/theme/validate.mjs', import.meta.url).pathname +const template = JSON.parse( + readFileSync(new URL('../skills/theme/template.json', import.meta.url), 'utf8'), +) + +// The status tones lich 0.53.0 made required (omartelo/lich#617), with the +// bundled Dark theme's values. +const tones = { + 'tone-pass': 'oklch(0.696 0.17 162.48)', + 'tone-wait': 'oklch(0.769 0.188 70.08)', +} + +function validate(app) { + const path = join(mkdtempSync(join(tmpdir(), 'lich-theme-')), 'theme.json') + writeFileSync(path, JSON.stringify({ ...template, id: 'tones', app })) + return spawnSync(process.execPath, [validator, path], { encoding: 'utf8' }) +} + +test('a theme carrying the status tones passes', () => { + const run = validate({ ...template.app, ...tones }) + assert.equal(run.status, 0, run.stderr) +}) + +test('a theme missing a status tone is rejected the way lich rejects it', () => { + const { 'tone-wait': _, ...app } = { ...template.app, ...tones } + const run = validate(app) + assert.equal(run.status, 1) + assert.match(run.stderr, /app\.tone-wait is required/) +})