kaloko 0.4.8 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,6 +81,15 @@ what still waits for a person, so it can pass the link on. Agents never
81
81
  approve for people: their decisions are marked and do not count as a
82
82
  person's.
83
83
 
84
+ Every AI verdict comes with one sentence saying why. Criteria marked
85
+ `vision: true`, and answers the text evaluator is unsure about, get a second
86
+ look at the screenshot from a vision model (`ANTHROPIC_API_KEY`, or through
87
+ Kaloko); `kaloko build` lists what to act on with the score and the reason.
88
+ The `a11y` pack walks the page with Tab (order, visible focus, traps, skip
89
+ link) and reads names from the accessibility tree. Pixel diffs skip the
90
+ regions a step ignores; `kaloko walk --stability 3` and `kaloko stability`
91
+ find the clocks, ads and carousels worth ignoring.
92
+
84
93
  ## Design (Kaloko Design)
85
94
 
86
95
  Design the screens of a flow as live HTML with your own agent and skills, in the same scenario that development
@@ -98,6 +107,17 @@ npx kaloko share --name "Checkout v2" # a version of the whole flow
98
107
  npx kaloko reference --design --version 2 # after the team accepted it
99
108
  ```
100
109
 
110
+ Developers and their agents build from what was approved. *Dev mode* (`D` in the step detail) shows each
111
+ element's box, sizes and distances, its colors, type and spacing with the token every value comes from (or a
112
+ warning when it is not a token), contrast against WCAG AA, the `:hover`, `:focus`, `:active` and disabled states,
113
+ and its images. The same data is the step spec an agent reads, and `kaloko drift` checks the implementation
114
+ against it:
115
+
116
+ ```bash
117
+ npx kaloko spec payment # elements, styles → tokens, states (also MCP get_step_spec)
118
+ npx kaloko walk && npx kaloko drift # expected token vs actual value per element; exit 1 while it differs
119
+ ```
120
+
101
121
  On the canvas, *Prototype* (or `P`) presents the steps full screen as live pages: click through links between
102
122
  steps, switch the device, walk with the arrow keys, and share a link that opens straight into a step. The rules
103
123
  for pages that work everywhere are in `skills/kaloko/reference/portable-html.md`.
@@ -141,7 +161,9 @@ captured HTML, page text and console output.
141
161
  - **Service and evaluator**: `KALOKO_TOKEN` for `share`, `feedback`, `review`, `inbox` and clean-up (`runs`, `trash`, `prune`, `keep`, `archive`)
142
162
  (`npx kaloko login` approves one in the browser and saves it to `~/.config/kaloko/.env`, or create it in your
143
163
  organization's settings); `TYPESAFE_API_KEY` for the
144
- semantic evaluator (optional). Both names can be changed in the config.
164
+ semantic evaluator and `ANTHROPIC_API_KEY` for the vision evaluator (both
165
+ optional; without them Kaloko evaluates on your plan's allowance). The names
166
+ can be changed in the config.
145
167
 
146
168
  `npx kaloko doctor` checks that every variable the config names is set and that
147
169
  each environment lets you in.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaloko",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "See what your AI agents built: every screen, language and device walked, assessed and approved on one canvas.",
5
5
  "keywords": [
6
6
  "qa",
@@ -45,6 +45,32 @@
45
45
  "signals": { "type": "boolean", "default": true, "description": "Quick signals on every screen (purpose met, expected language, looks broken); never change verdicts." },
46
46
  "concurrency": { "type": "integer", "minimum": 1, "maximum": 16, "default": 6, "description": "Evaluator requests in flight at once." }
47
47
  }
48
+ },
49
+ "vision": {
50
+ "description": "Second, vision-capable evaluator (#17): sees the screenshot (a crop for scoped criteria) and answers with a probability and a one-sentence reason. Asked only for criteria with vision: true and for text answers in the uncertain band; false turns it off.",
51
+ "oneOf": [
52
+ { "const": false },
53
+ {
54
+ "type": "object",
55
+ "additionalProperties": false,
56
+ "properties": {
57
+ "provider": { "enum": ["anthropic"], "default": "anthropic" },
58
+ "model": { "type": "string", "default": "claude-opus-5-5", "description": "Model id of the provider (Anthropic Messages API with image input)." },
59
+ "api_key_env": { "type": "string", "default": "ANTHROPIC_API_KEY" },
60
+ "base_url": { "type": "string", "default": "https://api.anthropic.com" },
61
+ "when": { "enum": ["uncertain", "flagged", "always", "off"], "default": "uncertain", "description": "flagged: only criteria with vision: true; uncertain: those plus text answers in the uncertain band; always: every semantic criterion (most expensive)." },
62
+ "uncertain": { "type": "array", "items": { "type": "number", "minimum": 0, "maximum": 1 }, "minItems": 2, "maxItems": 2, "default": [0.15, 0.85], "description": "Text scores strictly between these go to the vision evaluator." },
63
+ "max_calls": { "type": "integer", "minimum": 0, "default": 40, "description": "Vision requests per evaluation (uncached); the rest stay with the text answer." },
64
+ "max_width": { "type": "integer", "minimum": 320, "maximum": 1568, "default": 1000, "description": "Images are scaled down to this width (px)." },
65
+ "tile_height": { "type": "integer", "minimum": 400, "maximum": 1568, "default": 1400, "description": "A full page is cut into tiles of this height." },
66
+ "max_tiles": { "type": "integer", "minimum": 1, "maximum": 4, "default": 2, "description": "Tiles of a full page sent at most (from the top); scoped criteria get a crop instead." },
67
+ "effort": { "enum": ["low", "medium", "high"], "default": "low" },
68
+ "reason_language": { "type": "string", "default": "en", "description": "Language of the one-sentence reasons (en, cs, …)." },
69
+ "hosted": { "type": "boolean", "default": true, "description": "Without a local key, ask through the organization's service (counts against the monthly allowance; one vision question counts as 5)." },
70
+ "concurrency": { "type": "integer", "minimum": 1, "maximum": 8, "default": 3 }
71
+ }
72
+ }
73
+ ]
48
74
  }
49
75
  }
50
76
  },
@@ -262,6 +262,11 @@
262
262
  "capture": {
263
263
  "$ref": "#/$defs/capture"
264
264
  },
265
+ "ignore": {
266
+ "type": "array",
267
+ "description": "Regions left out of pixel comparisons (clocks, ads, carousels): a selector, or a rectangle in page pixels of one viewport. kaloko stability suggests them.",
268
+ "items": { "$ref": "#/$defs/ignoreRegion" }
269
+ },
265
270
  "items": {
266
271
  "type": "object",
267
272
  "additionalProperties": false,
@@ -500,6 +505,13 @@
500
505
  "a11y_contrast",
501
506
  "a11y_main",
502
507
  "a11y_unique_ids",
508
+ "a11y_focus_order",
509
+ "a11y_focus_visible",
510
+ "a11y_keyboard_trap",
511
+ "a11y_skip_link",
512
+ "a11y_ax_names",
513
+ "a11y_landmarks",
514
+ "a11y_aria",
503
515
  "perf_lcp",
504
516
  "perf_cls",
505
517
  "perf_ttfb",
@@ -588,6 +600,10 @@
588
600
  "console"
589
601
  ],
590
602
  "description": "console: the evaluator reads the captured console errors with the step purpose (triage) instead of the page"
603
+ },
604
+ "vision": {
605
+ "type": "boolean",
606
+ "description": "Also show the evaluator the screenshot (a crop of scope when set): layout, imagery, brand. Needs a vision evaluator (evaluators.vision)."
591
607
  }
592
608
  }
593
609
  },
@@ -632,6 +648,22 @@
632
648
  }
633
649
  }
634
650
  },
651
+ "ignoreRegion": {
652
+ "type": "object",
653
+ "additionalProperties": false,
654
+ "oneOf": [
655
+ { "required": ["selector"] },
656
+ { "required": ["rect", "viewport"] }
657
+ ],
658
+ "properties": {
659
+ "selector": { "type": "string", "minLength": 1, "description": "CSS selector; every visible match is ignored." },
660
+ "rect": { "type": "array", "items": { "type": "number", "minimum": 0 }, "minItems": 4, "maxItems": 4, "description": "[x, y, width, height] in CSS pixels of the page." },
661
+ "viewport": { "type": "string", "description": "The viewport a rect belongs to." },
662
+ "viewports": { "type": "array", "items": { "type": "string" }, "description": "Only in these viewports (selector)." },
663
+ "locales": { "type": "array", "items": { "type": "string" }, "description": "Only in these locales." },
664
+ "note": { "type": "string" }
665
+ }
666
+ },
635
667
  "capture": {
636
668
  "type": "object",
637
669
  "additionalProperties": false,
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://qawalk.com/schemas/spec.v1.json",
4
+ "title": "qawalk.spec.v1",
5
+ "description": "The step spec (#42 Dev mode): what to build for one step as it was approved. Written by the in-page engine src/lib/devspec.mjs at capture (steps/<step>/<locale>/<viewport>.spec.json, and the capture record of a design revision); the canvas Dev mode runs the same engine on the live page. Served by GET …/scenarios/<id>/steps/<step>/spec, MCP get_step_spec and kaloko spec. Elements are listed in page order; a style lists only what differs from what the element inherits (page, then the parent element).",
6
+ "type": "object",
7
+ "required": ["schema", "elements"],
8
+ "$defs": {
9
+ "value": {
10
+ "type": "object",
11
+ "description": "A computed value (colors as #rrggbb[aa], lengths in px) and the token it maps to. token: the DTCG path (several, space-separated, for a shorthand such as padding; 0 for a zero side); null when the set has tokens of that kind but none fits and none is near. nearest: the closest token when the value is not one. mode: the mode whose value it is (light, dark, …). hard: the stylesheet typed the raw value although the token exists. var: the custom property the author used when it names no token.",
12
+ "required": ["value"],
13
+ "properties": {
14
+ "value": { "type": "string" },
15
+ "token": { "type": ["string", "null"] },
16
+ "nearest": { "type": "string" },
17
+ "mode": { "type": "string" },
18
+ "hard": { "type": "boolean" },
19
+ "var": { "type": "string" }
20
+ },
21
+ "additionalProperties": false
22
+ },
23
+ "styles": {
24
+ "type": "object",
25
+ "description": "CSS property → value: color, background-color, border-color, font-family, font-size, font-weight, line-height, letter-spacing, padding, gap, row-gap, column-gap, border-width, border-radius, box-shadow, opacity, display and flex or grid properties.",
26
+ "additionalProperties": { "$ref": "#/$defs/value" }
27
+ }
28
+ },
29
+ "properties": {
30
+ "schema": { "const": "qawalk.spec.v1" },
31
+ "title": { "type": "string" },
32
+ "lang": { "type": ["string", "null"] },
33
+ "viewport": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 },
34
+ "size": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2, "description": "Document width and height." },
35
+ "tokens": { "type": ["object", "null"], "description": "The token set the values were mapped with (the step's tokens.json); null without one.", "properties": { "count": { "type": "integer" }, "modes": { "type": "array", "items": { "type": "string" } } } },
36
+ "page": { "$ref": "#/$defs/styles", "description": "Typography and text color of the page (body), inherited by every element that does not say otherwise." },
37
+ "elements": {
38
+ "type": "array",
39
+ "items": {
40
+ "type": "object",
41
+ "required": ["i", "parent", "tag", "sel", "styles"],
42
+ "properties": {
43
+ "i": { "type": "integer" },
44
+ "parent": { "type": "integer", "description": "Index of the nearest listed ancestor, -1 at the top." },
45
+ "tag": { "type": "string" },
46
+ "role": { "type": ["string", "null"], "description": "ARIA role, explicit or implicit (button, link, heading, main, textbox, …)." },
47
+ "name": { "type": ["string", "null"], "description": "Accessible name or own text (at most 80 characters)." },
48
+ "sel": { "type": "string", "description": "A CSS selector of the element in the design page (ids, data-testid and classes first)." },
49
+ "box": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "x, y, width, height in CSS px of the page." },
50
+ "styles": { "$ref": "#/$defs/styles" },
51
+ "contrast": { "type": "object", "properties": { "ratio": { "type": "number" }, "aa": { "type": "boolean" } } },
52
+ "states": { "type": "object", "description": "hover, focus, active, disabled → the styles that change in that state (the page's rules forced on the element).", "additionalProperties": { "$ref": "#/$defs/styles" } },
53
+ "asset": { "type": "object", "properties": { "kind": { "enum": ["img", "svg"] }, "src": { "type": "string" }, "size": { "type": "array" }, "alt": { "type": ["string", "null"] } } }
54
+ }
55
+ }
56
+ },
57
+ "used": { "type": "object", "description": "Token path → number of values that use it.", "additionalProperties": { "type": "integer" } },
58
+ "off": { "type": "array", "description": "Values outside the token set, most frequent first.", "items": { "type": "object", "properties": { "prop": { "type": "string" }, "value": { "type": "string" }, "count": { "type": "integer" }, "nearest": { "type": ["string", "null"] }, "sel": { "type": "string" } } } }
59
+ }
60
+ }
@@ -11,7 +11,9 @@ description: >
11
11
  reviewer rejected on a canvas, what is waiting in the Kaloko inbox, or how
12
12
  to report a run in an issue, and when designing screens of a flow in a
13
13
  design environment (live HTML steps, drafts, versions, the design
14
- reference, checking implementation against it).
14
+ reference, checking implementation against it), and when implementing a
15
+ screen that has an approved design (read the step spec, build with its
16
+ tokens, check drift).
15
17
  ---
16
18
 
17
19
  # Kaloko
@@ -32,6 +34,10 @@ the canvas, storage and collaboration are the service.
32
34
  for them (`kaloko share` prints it; `kaloko review` repeats it), and you
33
35
  read `kaloko feedback` and `kaloko inbox` before continuing on that task.
34
36
  - Changes to intent are written back to `ACCEPTANCE.md` (change log row).
37
+ - When you implemented a step from an approved design: you read its spec
38
+ (`kaloko spec <step>`), walked the implementation with `packs: [design]`,
39
+ and `kaloko drift` is clean or the PR names each difference left for a
40
+ person to decide.
35
41
  - On a design environment: the folder follows the bundle rules and
36
42
  `kaloko design lint` shows no error, `kaloko walk` and `kaloko sync` ran,
37
43
  `kaloko status` shows no conflict, and you read the draft's feedback; a
@@ -77,7 +83,8 @@ they can do; never write secrets into the config, a scenario or a script.
77
83
  [reference/stacks.md](reference/stacks.md)
78
84
  - Choosing playwright, agent or manual per step, and driving the shared
79
85
  browser with your capabilities: [reference/execution.md](reference/execution.md)
80
- - Evaluation, JEV thresholds and reading results: [reference/evaluation.md](reference/evaluation.md)
86
+ - Evaluation, thresholds, the vision evaluator and its reasons, reading
87
+ results: [reference/evaluation.md](reference/evaluation.md)
81
88
  - Environments, access (gates, accounts, sign-in), e-mails, readonly and
82
89
  production rules, and what is yours to arrange for the project's stack:
83
90
  [reference/environments.md](reference/environments.md)
@@ -94,6 +101,9 @@ they can do; never write secrets into the config, a scenario or a script.
94
101
  references, handoff, tokens, the design pack, canvas edits, branches, HTML
95
102
  view, presenting the prototype, exports, bringing your own design skill):
96
103
  [reference/design.md](reference/design.md)
104
+ - Implementing a screen from the approved design: read the step spec, build
105
+ with its tokens, walk, check drift, fix:
106
+ [reference/design.md#implement-from-the-approved-design](reference/design.md#implement-from-the-approved-design)
97
107
  - Writing design pages that work on the live step, from disk and in other
98
108
  tools (relative URLs, no CDNs, state in the URL, deterministic content,
99
109
  links between steps; what `kaloko design lint` checks):
@@ -40,8 +40,21 @@ for public pages and landings; task flows keep their own criteria.
40
40
 
41
41
  - `a11y` (`AC3xx`): images with alt, links and buttons with a name, labelled
42
42
  fields, heading levels not skipped, WCAG AA text contrast, a `main`
43
- landmark, unique ids. A subset of WCAG 2.2 AA checked in the page; it does
44
- not replace an audit with a screen reader.
43
+ landmark, unique ids (`AC301`–`AC307`, read from the page). After the
44
+ screenshot the walk also presses Tab through the page: `AC308` tab order
45
+ (hidden focus, big jumps back, positive `tabindex`, controls left out or
46
+ mouse-only), `AC309` visible focus with at least 3 : 1 contrast (each stop
47
+ screenshotted focused and unfocused), `AC310` no keyboard trap (a modal
48
+ dialog may keep focus), `AC311` a skip link or main content within three
49
+ stops, `AC312` names from Chrome's accessibility tree (what a screen
50
+ reader announces). `AC313` landmarks (one `main`, labelled repeats) and
51
+ `AC314` ARIA (unknown roles and attributes, references to missing ids,
52
+ `aria-hidden` over focusable content) are read from the page. The first
53
+ five focus stops are kept as screenshots and the canvas can draw the tab
54
+ order on the capture. The walk only moves focus: nothing is clicked or
55
+ typed, and the page is left unfocused at the top. Stack steps walk the
56
+ keyboard on their first five pages. Still a subset of WCAG 2.2 AA; it
57
+ does not replace an audit with a screen reader.
45
58
  - `perf` (`AC4xx`): LCP ≤ 2.5 s, CLS ≤ 0.1, TTFB ≤ 800 ms, page weight ≤ 3 MB,
46
59
  measured in this walk's browser since the page load (lab values: the
47
60
  machine and network of the walk matter; compare runs, not absolutes).
@@ -227,3 +240,22 @@ skips a session's nodes there. The canvas shows a chip with the person on
227
240
  every step, groups the step list by person and draws hand-offs between
228
241
  people as dashed arrows. Sessions run one after another, never in parallel,
229
242
  and never share cookies.
243
+
244
+ ## Ignore regions
245
+
246
+ Clocks, ads, carousels and "3 minutes ago" change on every capture and make
247
+ pixel diffs noisy. List them on the step and both the canvas diff and the
248
+ design pixel check (`AC801`) leave them out:
249
+
250
+ ```yaml
251
+ - id: home
252
+ ignore:
253
+ - { selector: "#clock" } # every visible match
254
+ - { selector: ".ad-slot", viewports: [desktop] }
255
+ - { rect: [0, 620, 1280, 240], viewport: desktop } # page pixels of one viewport
256
+ ```
257
+
258
+ Do not guess them: `kaloko walk --stability 3` notes what moves while the
259
+ page sits still, and `kaloko stability` compares the step with earlier runs
260
+ and writes suggestions (see [execution.md](execution.md)). Ignore only noise;
261
+ a region that matters to a criterion stays in.
@@ -39,8 +39,10 @@ design/
39
39
  the bundle.
40
40
  - Links between steps are plain relative links to the page: `<a href="../payment/index.html">`. The canvas
41
41
  follows them; `data-kaloko-go="payment"` (or `data-qawalk-go`) moves to that step whatever the `href` says.
42
- - Reference token values as CSS custom properties (`var(--color-accent)`), so the inspector and the token
43
- check see where values come from.
42
+ - Reference token values as CSS custom properties named after the token path (`color.accent` →
43
+ `var(--color-accent)`), so Dev mode, the step spec and the token check see which token a value comes from.
44
+ Define interaction states in CSS (`:hover`, `:focus-visible`, `:active`, `:disabled`): Dev mode and the spec
45
+ read them from the stylesheet.
44
46
  - Write pages by the portable HTML rules ([portable-html.md](portable-html.md)): relative URLs only, bundled
45
47
  fonts, no CDNs, state in the URL instead of storage, deterministic content, viewport meta and `lang`.
46
48
  `kaloko design lint` checks the folder; `kaloko sync` runs it first and stops on errors (things that break on
@@ -94,8 +96,39 @@ Development builds from the design reference:
94
96
  - `kaloko export --live [--version n] --out design/` writes the live files into a folder.
95
97
  - MCP `get_step` (scenario, step, `ref: design`) returns the file list, a signed entry URL, the token set, the
96
98
  page structure and open comments; `list_step_changes` names the steps that changed since the last reference.
99
+ - MCP `get_step_spec` (or `kaloko spec <step>`) returns what to build, element by element, with tokens; see
100
+ [Implement from the approved design](#implement-from-the-approved-design).
97
101
  - Translate to the project's stack with the project's own skills; keep the step ids and the token names.
98
102
 
103
+ ## Implement from the approved design
104
+
105
+ Use this when you build or change a screen that has an approved design step (a design reference, or a version
106
+ the person names). The step spec says what was approved in the terms you write code in: elements in page order
107
+ with role, accessible name and selector, and their colors, typography, spacing, radius, shadow and layout, each
108
+ value with the DTCG token it comes from. People see the same data in the canvas: *Dev mode* (`D`) in the step
109
+ detail.
110
+
111
+ 1. **Read the spec** of each step you build: `kaloko spec <step>` (default `--ref design`; `--ref v4`, `draft` or a
112
+ run id), or MCP `get_step_spec`. `--json` gives `qawalk.spec.v1` (`schemas/spec.v1.json`).
113
+ 2. **Implement with the tokens.** A line such as `button "Pay" 120×44 bg #2e6bff=color.brand.primary · pad 12px
114
+ 20px=space.3 space.5` means `background: var(--color-brand-primary); padding: var(--space-3) var(--space-5)`
115
+ (the custom property is the token path with dots as dashes) or the project's own token mechanism. `not a token;
116
+ nearest x` means the design itself used a raw value: ask the designer before you copy it, and say so in the PR.
117
+ `:hover`, `:focus`, `:active` and `:disabled` lines are states to build; `(light)` names the mode of a value.
118
+ Keep the roles and names: they are how the implementation is matched with the design.
119
+ 3. **Walk it** with `packs: [design]` on the implementation steps; the capture then stores the implementation's
120
+ spec next to the screenshot.
121
+ 4. **Check drift**: `kaloko drift [step]` compares the walked run with the design reference of the linked design
122
+ branch and exits 1 while something differs. A shared run: MCP `get_step_drift` (`run`: id or `latest`).
123
+ Findings name the element, the expected token and value, and what the page has; `typed raw` means the value
124
+ is right but should come from the token; `not found` means the design's element (by role and name) is missing.
125
+ 5. **Fix and repeat** until drift is clean or what is left is a decision for a person (a missing element on
126
+ purpose, a raw value the designer accepted); report those in the PR instead of hiding them.
127
+
128
+ Specs exist for captures made with the current CLI: walk the design and sync again if `kaloko spec` says there is
129
+ none. The pixel, structure and token checks of the design pack (`AC801`–`AC803`) still run in `kaloko evaluate`;
130
+ drift is the per-element view an agent can act on.
131
+
99
132
  ## Your data is yours
100
133
 
101
134
  Everything exports in open formats, readable without Kaloko:
@@ -10,9 +10,11 @@ answer, so running it again is cheap.
10
10
  - deterministic results computed in the live page at capture time (with
11
11
  evidence: selector snippet, measured value);
12
12
  - the semantic evaluator for semantic criteria. It is a replaceable provider;
13
- the default is JEV by TypeSafe (`TYPESAFE_API_KEY`), which returns only a
14
- probability per question (`score`, 0–1). Verdicts: `pass` ≥ 0.85,
15
- `fail` ≤ 0.15, otherwise `needs_review` (thresholds in config);
13
+ the default is JEV by TypeSafe (`TYPESAFE_API_KEY`), which reads the
14
+ reduced page state (no images) and returns only a probability per question
15
+ (`score`, 0–1). Verdicts: `pass` ≥ 0.85, `fail` ≤ 0.15, otherwise
16
+ `needs_review` (thresholds in config);
17
+ - a vision evaluator for a second look with the screenshot (below);
16
18
  - `manual` criteria stay `not_evaluated` until a reviewer decides.
17
19
 
18
20
  Where the evaluator runs: with `TYPESAFE_API_KEY` set locally, your own key; without it, the organization's
@@ -25,6 +27,47 @@ report with the reason `kaloko evaluate` gives.
25
27
  The default evaluator is text-only and most accurate in English. It is weak
26
28
  at numbers, which is why those are checked deterministically.
27
29
 
30
+ ## Vision and reasons
31
+
32
+ Every semantic verdict carries one sentence, `reason`, shown as "Why" next to
33
+ the score on the canvas, in `kaloko build`, in the walk summary and in
34
+ `get_run_summary`. With only the text evaluator the sentence says what it
35
+ read and how sure it was (JEV gives no explanation). The vision evaluator
36
+ gives a real one: it sees the screenshot, or a crop of the criterion's
37
+ `scope` (or `highlight`), and answers with a probability and what in the
38
+ image decided it. It is asked for:
39
+
40
+ - criteria with `vision: true` on the check (layout, imagery, "looks like the
41
+ brand", anything the text cannot show);
42
+ - text answers in the uncertain band (0.15–0.85 by default), which would
43
+ otherwise go to a person.
44
+
45
+ Its answer replaces the score (the text score stays as `text_score` in the
46
+ evaluation). Cost stays bounded: only those criteria, one request per
47
+ capture, images scaled to 1000 px wide and at most two tiles of a full page,
48
+ at most 40 uncached requests per evaluation, and answers cached by content
49
+ like the text ones. The provider is Anthropic's Messages API
50
+ (`ANTHROPIC_API_KEY`, model from `evaluators.vision.model`, default
51
+ `claude-opus-5-5`). Without the key it goes through the organization's
52
+ service, where one vision question counts as five of the monthly allowance;
53
+ when the text evaluator runs with your own `TYPESAFE_API_KEY`, the vision
54
+ evaluator also waits for your own key (`evaluators.vision.hosted: true`
55
+ lets it use the service anyway).
56
+ Configure or switch it off in `kaloko.config.yml`:
57
+
58
+ ```yaml
59
+ evaluators:
60
+ vision:
61
+ when: uncertain # flagged (only vision: true) | always | off
62
+ uncertain: [0.15, 0.85]
63
+ max_calls: 40
64
+ reason_language: en # cs for Czech reviewers
65
+ # vision: false # text evaluator only
66
+ ```
67
+
68
+ Report the reason with each `needs_review` or failing criterion; when the
69
+ evaluator saw the screenshot, say so.
70
+
28
71
  When a criterion comes back `needs_review`, read the reduced state the
29
72
  evaluator saw (`steps/<node>/<locale>/<viewport>.capture.json`, key `state`)
30
73
  and the screenshot. Either the page is genuinely unclear (report it), the
@@ -73,3 +73,21 @@ The environment policy and gate apply to every context the CLI creates (the
73
73
  shared browser, `walk` sessions, one-off captures): blocked paths and, when
74
74
  readonly, non-GET requests outside `allowed_post_paths` fail and are listed as
75
75
  blocked requests in the capture; the gate's credentials go to its origins only.
76
+
77
+ ## Noisy regions (`--stability`, `kaloko stability`)
78
+
79
+ When pixel diffs between runs keep flagging the same clock, ad or carousel:
80
+
81
+ ```bash
82
+ kaloko walk --stability 3 # 3 screenshots per capture, a moment apart; what moved is noted
83
+ kaloko stability # this run against the 3 previous local runs of the scenario
84
+ kaloko stability --apply # adds the suggested ignore: to each step (block-style YAML only)
85
+ ```
86
+
87
+ `kaloko stability` writes `stability.json` and `stability.yml` into the run
88
+ and prints the suggestions per step: a selector when an element of the stored
89
+ page covers the region, otherwise a rectangle for one viewport. Look at the
90
+ screenshots before applying; a region that changed because the page really
91
+ changed is not noise. `--same-commit` compares only runs of the same commit,
92
+ `--runs a,b` names them, `--last N` sets how many runs are compared. Capture
93
+ the steps again after applying: the capture records the ignored regions.
@@ -23,7 +23,7 @@ file.
23
23
  | Tool | Use it for |
24
24
  |---|---|
25
25
  | `list_runs` | published runs, newest first; filters `scenario`, `project`, `verdict`, `author`, `environment`, `branch`, `limit` |
26
- | `get_run_summary` | one run: statuses per step and locale, criteria that need attention with evidence, screenshot URLs, verdict, baseline, canvas URL, open feedback |
26
+ | `get_run_summary` | one run: statuses per step and locale, criteria that need attention with evidence (semantic ones with score and the evaluator's one-sentence `reason` per locale and viewport), regions ignored in pixel diffs and regions seen moving (`ignored_regions`, `unstable_regions`), screenshot URLs, verdict, baseline, canvas URL, open feedback |
27
27
  | `get_feedback` | run verdict with note, approved steps, unresolved rejections and comments, failed manual criteria (`run_id`, or `scenario` for its latest run) |
28
28
  | `list_notifications` | the token owner's inbox (`unread_only`, `limit`) |
29
29
  | `list_projects` | projects of the organization with counts; the slug is the `project` filter of `list_runs` and the value for `project:` in a scenario |
@@ -65,6 +65,8 @@ The same data and permissions as the REST API; each tool is its REST route.
65
65
  |---|---|
66
66
  | `get_draft` | which revision each step shows, step status, who changed it (person or agent) |
67
67
  | `get_step` | the handoff: live files, signed entry URL, token set, page structure, open comments (`ref`: draft, design, vN, run id) |
68
+ | `get_step_spec` | what to build for a step as approved: elements with role, name and selector, styles mapped to tokens, states, contrast (`ref`: design by default, draft, vN, run id; `format` text or json) |
69
+ | `get_step_drift` | how an implementation run (`run`: id or `latest`) differs from the design reference, per element: expected token against the actual value |
68
70
  | `get_step_history` | versions, and revisions of a step with authors |
69
71
  | `list_step_changes` | steps that differ between two references (default design reference → draft) |
70
72
  | `get_references`, `get_tokens` | design reference and baseline; the project's token set |
package/src/cli.mjs CHANGED
@@ -17,7 +17,8 @@ import { newContext, accountFor, gateHeaders, gateOrigins, originMatcher, gateFo
17
17
  import { configureGate } from './lib/linkcheck.mjs'
18
18
  import { rememberEnvironment } from './lib/secrets.mjs'
19
19
  import { evaluateRun, semanticPrefetch } from './lib/evaluate.mjs'
20
- import { buildManifest, writePreview } from './lib/build.mjs'
20
+ import { buildManifest, writePreview, attentionLines } from './lib/build.mjs'
21
+ import { stabilityCmd } from './lib/stability.mjs'
21
22
  import { localized, readJson, UserError, globToRegExp, failureStreak, expiryNote } from './lib/util.mjs'
22
23
  import { packageRoot } from './lib/schema.mjs'
23
24
  import { compareRuns, formatComparison } from './lib/compare.mjs'
@@ -35,6 +36,7 @@ import { htmlViewAllowed } from './lib/htmlview.mjs'
35
36
  import { service, serviceUrl } from './lib/service.mjs'
36
37
  import { login } from './lib/login.mjs'
37
38
  import { initNextSteps, loginNextSteps } from './lib/onboarding.mjs'
39
+ import { specCmd, driftCmd } from './lib/spec-cli.mjs'
38
40
  import { walkDesign, syncDesign, publishVersion, historyCmd, referenceCmd, tokensCmd, exportLive, localRevisions, branchCmd, uploadBlobs } from './lib/design-cli.mjs'
39
41
  import { designDir, readBase, headKey } from './lib/design.mjs'
40
42
  import { lintDesign, formatLint } from './lib/design-lint.mjs'
@@ -101,6 +103,8 @@ Run
101
103
  verdicts and what failed (--no-evaluate skips that; kaloko evaluate redoes it)
102
104
  [--items a,b] [--limit N] Stack steps (kind: stack, many pages of one template): only pages whose id or
103
105
  path contains a, b; at most N pages (default items.limit, 500)
106
+ [--stability N] Take N screenshots of each capture a moment apart and note regions that move
107
+ (clocks, carousels); kaloko stability turns them into ignore suggestions
104
108
  status Show what the current run has captured
105
109
  Scenarios may declare "sessions" (people in the flow); walk keeps one browser context per session and locale
106
110
 
@@ -143,6 +147,10 @@ Results
143
147
  Newest capture of every step as steps/<step>/<locale>/<viewport>.<ext> plus
144
148
  index.json (documentation layer, Business plan); unchanged files are skipped
145
149
  public [--run id] [--off] Make a shared run readable without sign-in (admin; testers may refresh an already public scenario)
150
+ stability [--last 4 | --runs a,b] [--same-commit] [--apply] [--json]
151
+ Regions that change on every run or move while the page is idle (clocks, ads,
152
+ carousels): ignore suggestions per step in <run>/stability.yml; --apply adds
153
+ them to the scenario (ignore:), and pixel diffs then skip them
146
154
  compare [--run id] [--baseline id | --design] [--json]
147
155
  Text diff of two runs: step statuses, criterion verdicts, scores and evidence
148
156
  (default: current run against the scenario baseline, else the previous run)
@@ -160,6 +168,12 @@ Design (environments with kind: design; live HTML steps in a folder)
160
168
  share [--name <text>] [--branch b] Publish the draft as a version of the flow (a run): "v4 · 3 changed, 17 unchanged"
161
169
  history [--scenario id] [--step id] [--locale l] [--json]
162
170
  Versions of the flow; revisions of a step with who (person or agent) and when
171
+ spec [step] [--ref design|draft|vN|<run id>] [--locale l] [--viewport v] [--local] [--json]
172
+ What to build for a step as approved: elements with role, name and selector, styles
173
+ mapped to DTCG tokens (or the nearest one), :hover/:focus/:active/:disabled, contrast
174
+ drift [step] [--ref design|vN] [--remote <run id|latest>] [--json]
175
+ How this implementation run differs from the approved design (expected token vs actual
176
+ value, raw values, missing elements and states); exit code 1 when something differs
163
177
  reference [--scenario id] (--design|--baseline) (--run id|--version n|--off) [--branch b]
164
178
  Show or set the design reference (accepted version) and the baseline
165
179
  tokens pull|push|check|validate [--project p] [--file tokens.json|tokens.resolver.json] [--mode m] [--json]
@@ -704,7 +718,7 @@ const commands = {
704
718
  return
705
719
  }
706
720
  const since = (await readCaptures(run.dir)).filter((c) => c.node === node.id).map((c) => c.at).pop()
707
- const results = await captureStep({ page, node, locale, options: captureOptions(scenario, node), viewports: scopeViewports(run, list(args.viewports)), run, consoleFile: conn ? consoleLog(root, config.output, env.name) : null, maxChars: config.evaluators?.jev?.max_state_chars, since, htmlView: htmlViewAllowed(env, scenario) ? { scenarioId: scenario.id } : null })
721
+ const results = await captureStep({ page, node, locale, options: captureOptions(scenario, node), viewports: scopeViewports(run, list(args.viewports)), run, consoleFile: conn ? consoleLog(root, config.output, env.name) : null, maxChars: config.evaluators?.jev?.max_state_chars, since, htmlView: htmlViewAllowed(env, scenario) ? { scenarioId: scenario.id } : null, stability: Number(args.stability) || 0 })
708
722
  for (const r of results) {
709
723
  const failed = Object.entries(r.checks).filter(([, v]) => v.ok === false).map(([k]) => k)
710
724
  say(`✓ ${node.id} ${locale} ${r.viewport} ${r.size.width}×${r.size.height}px ${failed.length ? `failing checks: ${failed.join(', ')}` : 'checks ok'}${r.blocked_requests.length ? ` blocked: ${r.blocked_requests.length}` : ''}`)
@@ -828,7 +842,7 @@ const commands = {
828
842
  // the automatic capture runs unless this script invocation captured the step itself: a crop is an
829
843
  // attachment (#49), and a capture from an earlier walk must not stand in for this one (#50)
830
844
  let captured = false
831
- const capture = (opts = {}) => { captured = true; return captureStep({ page: sPage, node, locale, options: captureOptions(scenario, node), viewports: scopeViewports(run, opts.viewports), run, htmlView: htmlViewAllowed(env, scenario) ? { scenarioId: scenario.id } : null, consoleFile: conn && !sess ? consoleLog(root, config.output, env.name) : null, maxChars: config.evaluators?.jev?.max_state_chars, extra: node.session ? { session: node.session } : {} }).then((r) => { prefetch?.add(r, run.dir); return r }) }
845
+ const capture = (opts = {}) => { captured = true; return captureStep({ page: sPage, node, locale, options: captureOptions(scenario, node), viewports: scopeViewports(run, opts.viewports), run, htmlView: htmlViewAllowed(env, scenario) ? { scenarioId: scenario.id } : null, consoleFile: conn && !sess ? consoleLog(root, config.output, env.name) : null, maxChars: config.evaluators?.jev?.max_state_chars, extra: node.session ? { session: node.session } : {}, stability: Number(args.stability) || 0 }).then((r) => { prefetch?.add(r, run.dir); return r }) }
832
846
  await mod.default({ page: sPage, context: sCtx, locale, node: { id: node.id, title: localized(node.title, locale), kind: node.kind }, session: sessionInfo(node.session), url: (id = node.id) => urlFor(env, stepOf(scenario, id), locale), env, run: run.meta, capture, crop: (selector, title, o = {}) => captureCrop({ page: sPage, node, locale, selector, title, run, padding: Number(o.padding) || 0 }), acceptDialogs: () => acceptDialogs(sPage) })
833
847
  if (!captured) await capture()
834
848
  say(`✓ ${node.id} ${locale}`)
@@ -949,6 +963,7 @@ const commands = {
949
963
  const report = await evaluateRun({ run, scenario, config, root })
950
964
  const via = report.via.qawalk ? ` via Kaloko${report.hosted?.limit != null ? ` (${report.hosted.questions} of ${report.hosted.limit} questions this month)` : ''}` : report.via['own-key'] ? ' with your key' : ''
951
965
  say(`Evaluated ${report.evaluated} capture(s); semantic evaluator ${report.model}${via}: ${report.semantic_calls} call(s), ${report.semantic_cached} cached.`)
966
+ if (report.vision?.asked) say(`Vision evaluator ${report.vision.model}: ${report.vision.asked} capture(s) with flagged or uncertain criteria, ${report.vision.calls} call(s), ${report.vision.cached} cached${report.vision.skipped.length ? `; not asked: ${[...new Set(report.vision.skipped.map((x) => x.split(': ').slice(1).join(': ')))].slice(0, 2).join('; ')}` : ''}.`)
952
967
  if (report.skipped.length) {
953
968
  // one line per reason: "12 captures: TYPESAFE_API_KEY is not set …"
954
969
  const byReason = new Map()
@@ -964,9 +979,15 @@ const commands = {
964
979
  const file = await writePreview({ run, manifest })
965
980
  const s = manifest.summary
966
981
  say(`${manifest.id}: ${s.pass} pass · ${s.partial} partial · ${s.fail} fail · ${s.skip} not checked\nPreview: ${file}`)
982
+ const why = attentionLines(manifest); if (why.length) say(`AI verdicts to act on (score and why):\n${why.join('\n')}`)
967
983
  return file
968
984
  },
969
985
 
986
+ async stability (args) {
987
+ const { config, root, run, scenario } = await runContext(args)
988
+ await stabilityCmd({ root, config, run, scenario, args, say })
989
+ },
990
+
970
991
  async preview (args) {
971
992
  const file = await commands.build(args)
972
993
  say(pathToFileURL(file).href)
@@ -989,7 +1010,7 @@ const commands = {
989
1010
  await writePreview({ run, manifest })
990
1011
  const files = new Set()
991
1012
  for (const n of manifest.nodes) {
992
- for (const byVp of Object.values(n.captures || {})) for (const c of Object.values(byVp)) [c.image || c.png, c.thumb, c.html, c.eval, c.tree].filter(Boolean).forEach((p) => files.add(p))
1013
+ for (const byVp of Object.values(n.captures || {})) for (const c of Object.values(byVp)) [c.image || c.png, c.thumb, c.html, c.eval, c.tree, c.spec].filter(Boolean).forEach((p) => files.add(p))
993
1014
  for (const a of n.attachments || []) files.add(a.file)
994
1015
  // stack steps: the items index and every page's files
995
1016
  if (n.stack?.items) {
@@ -1097,6 +1118,25 @@ const commands = {
1097
1118
  await historyCmd({ config, scenarioId, client: service(config), args, say })
1098
1119
  },
1099
1120
 
1121
+ async spec (args) {
1122
+ const { config, root } = await context(args)
1123
+ let run = null
1124
+ try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
1125
+ const scenario = run ? await loadScenario(resolve(root, run.meta.scenario_file)).catch(() => null) : typeof args.scenario === 'string' && /\.ya?ml$/.test(args.scenario) ? await loadScenario(resolve(args.scenario)) : null
1126
+ const scenarioId = await scenarioIdOf(args, config, root)
1127
+ let client = null
1128
+ if (!args.local) { try { client = service(config) } catch (e) { if (!run) throw e } }
1129
+ await specCmd({ config, client, run, scenario, scenarioId, args, say })
1130
+ },
1131
+
1132
+ async drift (args) {
1133
+ const { config, root } = await context(args)
1134
+ let run = null
1135
+ try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
1136
+ const scenarioId = await scenarioIdOf(args, config, root)
1137
+ await driftCmd({ config, client: service(config), run, scenarioId, args, say })
1138
+ },
1139
+
1100
1140
  async reference (args) {
1101
1141
  const { config, root } = await context(args)
1102
1142
  const scenarioId = await scenarioIdOf(args, config, root)