Refs #14618
Split out of #14618 after its AC-2 was amended with measurement. That ticket owns a visual-baseline harness; this owns the static guard that should have been answering its colour question all along. Independent value: the parity refinement below improves the agentos coverage that already exists, whether or not the workstation is ever added.
Problem
buildScripts/util/check-agentos-theme.mjs is the right instrument for "did a colour escape the token layer, and does the light skin actually differ from the dark one" — static SCSS analysis, no baselines, no threshold, no platform drift. It has two gaps.
1. It scans agentos only.
darkPath : resources/scss/theme-neo-dark/apps/agentos/Viewport.scss
lightPath: resources/scss/theme-neo-light/apps/agentos/Viewport.scss
viewDir : resources/scss/src/apps/agentos
The workstation has all three equivalent surfaces and is unscanned — including the --agent-dock-preview-* family, which is exactly what #14618's preview-language-pair-* baselines capture and what the pixel suite provably cannot see.
2. Its parity rule flags correct code as a violation.
The rule is "every contracted colour token must carry a genuinely different value in dark vs light", exempting only the two mode-invariant font tokens. Measured on the workstation skins:
24 tokens per skin · 0 missing in either skin · 8 byte-identical across skins
All 8 are identical by correct design:
| token |
why |
--workstation-font-mono, --workstation-font-sans |
mode-invariant, same contract agentos already exempts |
--agent-dock-preview-accept, --agent-dock-preview-signal |
alias → var(--workstation-signal) |
--agent-dock-proxy-border, -ground, -shadow, -text |
alias → var(--workstation-…) / color-mix(…) |
A naive port reports 6 false positives on correct code. The rule compares the written expression when what it means is the resolved value: an alias is byte-identical across skins precisely because its referent differs per skin, which is the token layer working.
Running the guard against workstation paths with the current default contract returns 56 violations, every one an --fm-* contract complaint — the workstation uses a different token family, so it needs its own contract rather than a copied one.
The Architectural Reality
collectAgentosThemeFailures({darkPath, lightPath, viewDir, contractedTokens}) is already fully path-parameterised. Extending it is a second invocation and a contract, not a refactor.
- The parity refinement is not workstation-specific. Agentos can grow an alias token the day someone writes one, and the guard would then reject it — so this strengthens existing coverage rather than only widening it.
- The guard's name and CI job say "agentos"; covering two app surfaces makes both a misnomer.
- The workstation's view SCSS carries no bare colour literals today, so adopting token-only consumption there starts from a clean surface rather than a wall of pre-existing violations.
The Fix
- Generalise from one hardcoded surface to a list of app surfaces, each with its own paths and token contract; register the workstation alongside agentos.
- Make parity alias-aware: a token whose value is a
var() / color-mix() expression referring to another token satisfies parity when its referent differs per skin. Byte-identical expression, different resolved value.
- Rename the guard, its npm script and its workflow to describe what it now covers.
Out of Scope
- The visual baseline suite's
threshold. Its permissive default is correct for the geometry it owns; #14618 carries the amended AC and the measurement.
- Adding tokens to either skin, or changing any colour value. This makes the existing surfaces enforced, not different.
- The
--fm-* contract itself.
Acceptance Criteria
Evidence class
Measured on dev 2026-08-15. Guard scope read at source (DEFAULT_PATHS, collectAgentosThemeFailures signature). The 56-violation and 24-token/8-identical figures come from running collectAgentosThemeFailures against workstation paths and diffing the two skins' parsed token maps in one process. The alias classification is by reading each of the 8 identical values, not inferred from the count.
Live latest-open sweep at 2026-08-15T17:25:44Z: #14618 is the parent context; nothing equivalent open. A2A sweep: no competing claim.
Decision Record impact
none — no ADR governs the theme-guard surface.
🖖 Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb. Split after @tobiu redirected #14618's AC-2 from pixel diffing to (S)CSS analysis across light and dark.
Refs #14618
Split out of #14618 after its AC-2 was amended with measurement. That ticket owns a visual-baseline harness; this owns the static guard that should have been answering its colour question all along. Independent value: the parity refinement below improves the agentos coverage that already exists, whether or not the workstation is ever added.
Problem
buildScripts/util/check-agentos-theme.mjsis the right instrument for "did a colour escape the token layer, and does the light skin actually differ from the dark one" — static SCSS analysis, no baselines, no threshold, no platform drift. It has two gaps.1. It scans agentos only.
The workstation has all three equivalent surfaces and is unscanned — including the
--agent-dock-preview-*family, which is exactly what #14618'spreview-language-pair-*baselines capture and what the pixel suite provably cannot see.2. Its parity rule flags correct code as a violation.
The rule is "every contracted colour token must carry a genuinely different value in dark vs light", exempting only the two mode-invariant font tokens. Measured on the workstation skins:
All 8 are identical by correct design:
--workstation-font-mono,--workstation-font-sans--agent-dock-preview-accept,--agent-dock-preview-signalvar(--workstation-signal)--agent-dock-proxy-border,-ground,-shadow,-textvar(--workstation-…)/color-mix(…)A naive port reports 6 false positives on correct code. The rule compares the written expression when what it means is the resolved value: an alias is byte-identical across skins precisely because its referent differs per skin, which is the token layer working.
Running the guard against workstation paths with the current default contract returns 56 violations, every one an
--fm-*contract complaint — the workstation uses a different token family, so it needs its own contract rather than a copied one.The Architectural Reality
collectAgentosThemeFailures({darkPath, lightPath, viewDir, contractedTokens})is already fully path-parameterised. Extending it is a second invocation and a contract, not a refactor.The Fix
var()/color-mix()expression referring to another token satisfies parity when its referent differs per skin. Byte-identical expression, different resolved value.Out of Scope
threshold. Its permissive default is correct for the geometry it owns; #14618 carries the amended AC and the measurement.--fm-*contract itself.Acceptance Criteria
var()/color-mix()token that resolves through a per-skin-differing referent passes, and the six workstation aliases are proven to pass by spec rather than by exemption list.var(".Evidence class
Measured on
dev2026-08-15. Guard scope read at source (DEFAULT_PATHS,collectAgentosThemeFailuressignature). The 56-violation and 24-token/8-identical figures come from runningcollectAgentosThemeFailuresagainst workstation paths and diffing the two skins' parsed token maps in one process. The alias classification is by reading each of the 8 identical values, not inferred from the count.Live latest-open sweep at 2026-08-15T17:25:44Z: #14618 is the parent context; nothing equivalent open. A2A sweep: no competing claim.
Decision Record impact
none— no ADR governs the theme-guard surface.🖖 Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb. Split after @tobiu redirected #14618's AC-2 from pixel diffing to (S)CSS analysis across light and dark.