feat(cli): skipContrastInMotion moves to HF_TOOL_CONTEXT check-review (#6400)

* feat(cli): skipContrastInMotion moves to HF_TOOL_CONTEXT check-review

The --skip-contrast-in-motion flag and HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION
env give way to a check-review capability in HF_TOOL_CONTEXT, so only a harness
turns it on. The skill text and manifest go back to main.

* fix(cli): satisfy comment ratchet for check capability
This commit is contained in:
Xuanru Li
2026-10-26 07:42:50 +00:00
committed by GitHub
parent a988afa00b
commit 257bf285ff
8 changed files with 58 additions and 29 deletions
+14
View File
@@ -77,6 +77,20 @@ id, and says to delete it to continue. The lock is the file `.hyperframes/projec
Choose `waitMs` below your tool call's own timeout, so a wait ends in the CLI's message rather than
the harness killing the call.
## `check-review`: review switches for `check`
```json
{ "skipContrastInMotion": true }
```
Each field is a boolean and defaults to `false`; a value that is not `true` is
off, and unknown fields are ignored. Without the capability, `check` output is
the same as in a terminal.
| Field | What it does |
| --- | --- |
| `skipContrastInMotion` | Leaves out contrast failures measured while a GSAP tween moves the text or an ancestor, or while the text is partly transparent (see [check](/packages/cli#check)). Counts, `ok` and persistence leave them out too. |
## Hooks that run code
Running your own command before or after a CLI command is not a `HF_TOOL_CONTEXT` capability and
+4 -4
View File
@@ -1058,7 +1058,6 @@ cover every finding from the full check.
| `--tolerance` | Allowed overflow in px before reporting (default 2) |
| `--timeout` | Render-ready budget in ms; also raises page navigation above its 10s floor (default 3000) |
| `--no-contrast` | Skip the WCAG pass while iterating |
| `--skip-contrast-in-motion` | Leave out contrast failures measured mid-animation (see below); also `HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION=1` |
| `--strict` | Exit non-zero on warnings too (default: errors only) |
| `--caption-zone "<x0=..;y0=..;x1=..;y1=..>"` | Opt-in band gate. Flags a text element's DOM box that overlaps the fractional band. Optional `severity` and `seek`. |
| `--frame-check` | Opt-in out-of-frame detection for `img`, `svg`, `video`, and `canvas` |
@@ -1098,9 +1097,10 @@ Contrast failures are errors, and each one reports the sampled foreground and
background colours, the measured ratio against the required one, and a
compliant colour you could use instead.
`--skip-contrast-in-motion`, or `HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION=1` in the
environment, leaves out a contrast failure measured while a GSAP tween on the text
or an ancestor is running, or while the text's opacity multiplied up its ancestors
An agent harness can turn on `skipContrastInMotion` in the `check-review`
capability of [`HF_TOOL_CONTEXT`](/developers/harness-tool-context). It leaves
out a contrast failure measured while a GSAP tween on the text or an ancestor
is running, or while the text's opacity multiplied up its ancestors
is below 1. The failure is not reported, and the counts and `ok` leave it out.
Repeating tweens, tweens inside infinitely repeating timelines, and tweens of
4s or longer, such as a slow camera drift, do not count as motion. The opacity
+11 -10
View File
@@ -453,18 +453,19 @@ describe("skip contrast in motion", () => {
vi.unstubAllEnvs();
});
it("is off unless the flag or the environment turns it on", async () => {
vi.stubEnv("HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION", "");
it("is off unless HF_TOOL_CONTEXT's check-review capability turns it on", async () => {
const review = (value: unknown) =>
vi.stubEnv(
"HF_TOOL_CONTEXT",
JSON.stringify({ version: 1, capabilities: { "check-review": value } }),
);
vi.stubEnv("HF_TOOL_CONTEXT", "");
expect(await parsedOptions([])).not.toHaveProperty("skipContrastInMotion");
expect(await parsedOptions(["--skip-contrast-in-motion"])).toMatchObject({
skipContrastInMotion: true,
});
vi.stubEnv("HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION", "1");
review({ skipContrastInMotion: true });
expect(await parsedOptions([])).toMatchObject({ skipContrastInMotion: true });
expect(await parsedOptions(["--no-skip-contrast-in-motion"])).toMatchObject({
skipContrastInMotion: true,
});
vi.stubEnv("HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION", "0");
review({ skipContrastInMotion: "1" });
expect(await parsedOptions([])).not.toHaveProperty("skipContrastInMotion");
review({});
expect(await parsedOptions([])).not.toHaveProperty("skipContrastInMotion");
});
+6 -12
View File
@@ -11,7 +11,7 @@ import {
} from "@hyperframes/studio-server/history";
import { formatFindingTimes } from "../utils/checkFindings.js";
import { withProjectLock } from "../utils/projectLock.js";
import { reportArtifacts } from "../utils/toolContext.js";
import { readToolContext, reportArtifacts, type ToolContext } from "../utils/toolContext.js";
import { defineCommand, parseArgs } from "citty";
import type { ArgsDef } from "citty";
import type { Example } from "./_examples.js";
@@ -37,7 +37,6 @@ import {
} from "../utils/checkPipeline.js";
import type { CaptionZoneOptions, FrameCheckOptions, LayoutOptions } from "../utils/checkTypes.js";
import { resolveLocalBrowserGpuMode } from "../browser/gpuPolicy.js";
import { envFlagEnabled } from "../utils/attendedTerminal.js";
import { preResolveHostileMediaProxies, type PreparedMediaFile } from "../utils/checkBrowser.js";
export const examples: Example[] = [
@@ -216,12 +215,6 @@ const CHECK_COMMAND_ARGS = {
description: "Run the WCAG AA contrast pass (enabled by default)",
default: true,
},
"skip-contrast-in-motion": {
type: "boolean",
description:
"Leave out contrast failures measured while a GSAP tween moves the text or an ancestor, or at partial opacity (also HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION=1)",
default: false,
},
strict: {
type: "boolean",
description: "Exit non-zero on warnings too",
@@ -382,6 +375,10 @@ async function runPrepareMedia(
setCommandExitCode(failed.length === 0 ? 0 : 1);
}
function checkReview(): ToolContext["checkReview"] {
return readToolContext().checkReview;
}
function parseCheckOptions(args: Record<string, unknown>): CheckOptions {
const maxTransitionSamples = positiveInteger(args["max-transition-samples"], 0);
return {
@@ -396,10 +393,7 @@ function parseCheckOptions(args: Record<string, unknown>): CheckOptions {
tolerance: nonNegativeNumber(args.tolerance, DEFAULT_CHECK_OPTIONS.tolerance),
timeout: Math.max(500, positiveInteger(args.timeout, DEFAULT_CHECK_OPTIONS.timeout)),
contrast: args.contrast !== false,
...(args["skip-contrast-in-motion"] === true ||
envFlagEnabled("HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION")
? { skipContrastInMotion: true }
: {}),
...(checkReview()?.skipContrastInMotion ? { skipContrastInMotion: true } : {}),
strict: args.strict === true,
snapshots: args.snapshots === true,
captionZone: parseCaptionZone(args["caption-zone"]),
@@ -47,6 +47,16 @@ describe("readToolContext", () => {
readToolContext(context({ events: { transport: { type: "http", path: "/x" } } })),
).toEqual({});
});
it("reads check-review switches as booleans that default to false", () => {
expect(
readToolContext(context({ "check-review": { skipContrastInMotion: true, other: 1 } })),
).toEqual({ checkReview: { skipContrastInMotion: true } });
expect(readToolContext(context({ "check-review": { skipContrastInMotion: 1 } }))).toEqual({
checkReview: { skipContrastInMotion: false },
});
expect(readToolContext(context({ "check-review": true }))).toEqual({});
});
});
describe("reportArtifacts", () => {
+12 -1
View File
@@ -6,6 +6,7 @@ const TOOL_CONTEXT_VERSION = 1;
export interface ToolContext {
events?: { path: string };
projectLock?: { waitMs: number };
checkReview?: { skipContrastInMotion: boolean };
}
export type ArtifactRole = "contact-sheet" | "frame" | "finding" | "check-report";
@@ -34,6 +35,11 @@ function parseProjectLock(value: unknown): ToolContext["projectLock"] {
return { waitMs };
}
function parseCheckReview(value: unknown): ToolContext["checkReview"] {
if (!isRecord(value)) return undefined;
return { skipContrastInMotion: value.skipContrastInMotion === true };
}
export function readToolContext(raw = process.env.HF_TOOL_CONTEXT): ToolContext {
if (!raw) return {};
let value: unknown;
@@ -46,7 +52,12 @@ export function readToolContext(raw = process.env.HF_TOOL_CONTEXT): ToolContext
return {};
const events = parseEvents(value.capabilities.events);
const projectLock = parseProjectLock(value.capabilities["project-lock"]);
return { ...(events && { events }), ...(projectLock && { projectLock }) };
const checkReview = parseCheckReview(value.capabilities["check-review"]);
return {
...(events && { events }),
...(projectLock && { projectLock }),
...(checkReview && { checkReview }),
};
}
const MEDIA_TYPES: Record<string, string> = {
+1 -1
View File
@@ -10,7 +10,7 @@
"files": 77
},
"hyperframes-cli": {
"hash": "70516c62caf45a8a",
"hash": "08a4a3a815d71988",
"files": 13
},
"hyperframes-registry": {
@@ -38,7 +38,6 @@ npx hyperframes check --at-transitions # also sample every tween start/end bou
npx hyperframes check --tolerance 4 # allowed overflow px before reporting (default 2)
npx hyperframes check --timeout 30000 # initial render-ready + navigation minimum in ms (defaults: 3000 / 10000)
npx hyperframes check --no-contrast # skip the WCAG audit while iterating
npx hyperframes check --skip-contrast-in-motion # leave out contrast failures measured mid-animation (or HYPERFRAMES_CHECK_SKIP_CONTRAST_IN_MOTION=1)
npx hyperframes check --strict # exit non-zero on warnings too (default: only errors)
```