astack-skills 0.7.0 → 0.8.0

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.
Files changed (45) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +17 -0
  4. package/README.md +5 -5
  5. package/package.json +4 -2
  6. package/plugin.json +1 -1
  7. package/skills/astack/SKILL.md +14 -12
  8. package/skills/astack/agents/openai.yaml +1 -1
  9. package/skills/astack/references/completion-checks.md +2 -1
  10. package/skills/astack/scripts/shoot.mjs +148 -16
  11. package/skills/context/SKILL.md +6 -6
  12. package/skills/context/agents/openai.yaml +1 -1
  13. package/skills/context/references/source-selection.md +1 -1
  14. package/skills/deslop/SKILL.md +6 -2
  15. package/skills/deslop/agents/openai.yaml +1 -1
  16. package/skills/deslop/scripts/slop-scan.mjs +4 -0
  17. package/skills/discoverability/SKILL.md +1 -1
  18. package/skills/discoverability/agents/openai.yaml +1 -1
  19. package/skills/dogfood/SKILL.md +4 -1
  20. package/skills/dogfood/agents/openai.yaml +1 -1
  21. package/skills/dogfood/scripts/shoot.mjs +148 -16
  22. package/skills/instrument/SKILL.md +1 -1
  23. package/skills/instrument/agents/openai.yaml +1 -1
  24. package/skills/launch/SKILL.md +3 -3
  25. package/skills/launch/agents/openai.yaml +1 -1
  26. package/skills/launch/references/film.md +1 -1
  27. package/skills/launch/references/kit.md +9 -2
  28. package/skills/launch/scripts/shoot.mjs +148 -16
  29. package/skills/oneshot/SKILL.md +11 -11
  30. package/skills/oneshot/agents/openai.yaml +2 -2
  31. package/skills/oneshot/scripts/shoot.mjs +148 -16
  32. package/skills/ship/SKILL.md +6 -6
  33. package/skills/ship/agents/openai.yaml +1 -1
  34. package/skills/ship/scripts/shoot.mjs +148 -16
  35. package/skills/web-design/SKILL.md +4 -3
  36. package/skills/web-design/agents/openai.yaml +1 -1
  37. package/skills/web-design/references/anti-patterns.md +16 -0
  38. package/skills/web-design/references/craft-floor.md +1 -1
  39. package/skills/web-design/references/loading.md +35 -0
  40. package/skills/web-design/references/mobile.md +16 -2
  41. package/skills/web-design/references/worst-case-data.md +1 -1
  42. package/skills/web-design/scripts/shoot.mjs +148 -16
  43. package/skills/web-graphics/SKILL.md +4 -4
  44. package/skills/web-graphics/agents/openai.yaml +1 -1
  45. package/skills/web-graphics/scripts/shoot.mjs +148 -16
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astack",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
5
5
  "author": {
6
6
  "name": "Andrew Wilkinson",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astack",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
5
5
  "author": {
6
6
  "name": "Andrew Wilkinson",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ Measured checks for defects a settled screenshot hides, and a harder rule for launch stills. No library was added.
6
+
7
+ - `shoot.mjs` reads the rendered page and reports inputs under 16px, a viewport that disables zoom, `:hover` rules outside `@media (hover: hover)`, selectable controls on a narrow viewport, and a missing tap-highlight reset. `--shift` compares a loading page with the settled page. It exits 1 when a shared region or the document scroll height moved by more than 1px, and exits 2 when no region was marked and the scroll height did not change. It does not read the performance timeline's layout-shift entries: headless Chrome can report none for a jump that has already painted, and a score of 0 would pass that page.
8
+ - `web-design` states how to build that loading layout, and `mobile.md` says which of those lines a desktop capture can see. `dogfood` and `astack`'s completion checks read them before calling a phone journey done.
9
+ - `deslop` names this season's UI tells with examples (pill tags, italic headline words, dividers, opacity ladders, borders on every box, over-detailed diagrams, decorative breadcrumbs, section numbers). `slop-scan.mjs` flags `section-index` and `opacity-ramp`.
10
+ - `launch` and `web-graphics`: a bento, device or clip may frame a real capture. It may not redraw the screen, and it may not show a number the agreed copy file does not contain.
11
+ - New `shift-and-still` eval fixture, with the planted failures named. The blind comparison was not run. The deterministic `shoot.mjs --shift` run is recorded in `evals/RESULTS.md`.
12
+
13
+ ## 0.7.1
14
+
15
+ - Keep coupled work in one agent context and load skills as needed. Independent review now depends on risk, unresolved judgment and repository requirements.
16
+ - Shorten skill descriptions while preserving tested routing cues. Context gathering stops when remaining unknowns cannot change the next step.
17
+ - Reuse existing delivery authority and preserve explicit targets. Skills do not create permission to send, publish or bypass a repository gate.
18
+ - Reject incomplete routing evals and report individual improvements and regressions, including when aggregate scores tie.
19
+
3
20
  ## 0.7.0
4
21
 
5
22
  Review gates and video from LangChain's open-source design agent, [Content Studio](https://github.com/langchain-ai/design-agent) (Apache-2.0), credited where they are used. Its idea is that the model makes the creative calls and code renders, measures and refuses.
package/README.md CHANGED
@@ -40,15 +40,15 @@ Two ways in, depending on whether you're watching.
40
40
  astack this: add annual pricing to the billing page. phones matter most
41
41
  ```
42
42
 
43
- `astack` takes it to a PR you'd be happy to merge. It reads the context, plans, builds, uses the thing like a customer, adds tracking, cleans up the slop, gets a second agent to check the work, and stops there for you.
43
+ `astack` takes it to a PR you'd be happy to merge. It reads the context, plans, builds, uses the thing like a customer, adds relevant tracking, cleans up the diff, and verifies the result. A separate reviewer joins when the risk or judgment calls for one.
44
44
 
45
45
  ```text
46
46
  oneshot this: add annual pricing and get it live
47
47
  ```
48
48
 
49
- `oneshot` is for when you're not watching. Same work, then it merges, deploys, proves production is serving it and tells the team. It only runs when you ask for it by name.
49
+ `oneshot` is for when you're not watching. Same work, then it merges, deploys, proves production is serving it and sends any authorized update. It only runs when you ask for it by name.
50
50
 
51
- Both pull in the other skills as they need them.
51
+ Both keep one working context and load other skills only when their procedure helps the next step. Required repository checks and delivery proof still apply.
52
52
 
53
53
  ## Skills
54
54
 
@@ -63,7 +63,7 @@ Both pull in the other skills as they need them.
63
63
  | [instrument](skills/instrument/SKILL.md) | "can we measure this" | Tracking that fires once, on real success, and a query you can actually trust. |
64
64
  | [discoverability](skills/discoverability/SKILL.md) | "why doesn't ChatGPT mention us" | Checks what Google and AI crawlers really see, fixes it, measures before and after. |
65
65
  | [dogfood](skills/dogfood/SKILL.md) | "use it like a customer" | Walks the real journeys signed in, fixes what breaks and goes round again. |
66
- | [ship](skills/ship/SKILL.md) | "ship it" | Clears CI and review comments, gets an independent check, deploys, and proves production is serving it. Also checks other people's "it's shipped". |
66
+ | [ship](skills/ship/SKILL.md) | "ship it" | Clears CI and review comments, verifies the affected behavior, deploys, and proves production is serving it. Also checks other people's "it's shipped". |
67
67
  | [launch](skills/launch/SKILL.md) | "we launch tomorrow" | Share cards, page checks, posts for X and Show HN, a tracker and how you'll read the results. |
68
68
 
69
69
  Each one has a short guide in [docs/skills](docs/skills/).
@@ -72,7 +72,7 @@ Each one has a short guide in [docs/skills](docs/skills/).
72
72
 
73
73
  Most skill packs tell the model to be thorough. These come with small scripts so it can actually check. Node 22, nothing else to install, and every skill carries its own copy.
74
74
 
75
- - `shoot.mjs` screenshots a page on a phone and on desktop, light and dark, and flags stranded words, tiny tap targets and anything spilling sideways.
75
+ - `shoot.mjs` screenshots a page on a phone and on desktop, light and dark, and flags stranded words, tiny tap targets, anything spilling sideways, inputs that will zoom the page, hover that is not gated to a real pointer, and (with `--shift`) a loading state whose boxes do not match the loaded page.
76
76
  - `pr-state.mjs` reads every check on the current commit and every unresolved review thread. `--watch` sits in the background and pings you when something changes.
77
77
  - `verify-live.mjs` confirms the URL is serving the commit you shipped, not a cached copy of the last one.
78
78
  - `slop-scan.mjs` finds the usual suspects, from "seamless" to gradient headlines to emoji bullets, and explains why each one reads as generated. <!-- slop-scan: ignore-line -->
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "astack-skills",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
6
6
  "license": "MIT",
7
7
  "repository": "https://github.com/ADWilkinson/astack",
8
8
  "packageManager": "bun@1.4.2",
9
9
  "scripts": {
10
- "check": "bun scripts/check.ts && bun test",
10
+ "check": "bun scripts/check.ts && bun test tests/package.test.ts tests/install.test.ts && bun scripts/smoke.ts",
11
+ "smoke": "bun scripts/smoke.ts",
12
+ "test": "bun test",
11
13
  "validate": "bun scripts/check.ts",
12
14
  "metadata": "bun scripts/metadata.ts",
13
15
  "pack": "bun scripts/pack.ts",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "astack",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
6
6
  "author": {
7
7
  "name": "Andrew Wilkinson",
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: astack
3
- description: Take a product task from "here's the idea" to a pull request that is proven, green and ready for review, covering context, plan, build, real use, measurement and cleanup, without being walked through each step. Use when the user says "astack this", "take this end to end", "do this properly", or hands over a feature, fix, page or experiment and expects it finished rather than planned. Stops before merge; for all the way to production use oneshot.
3
+ description: "Own context, planning, implementation, real use, measurement and checks through a review-ready PR. Use for \"astack this\", \"own this\" or a product task to finish end to end. Load further procedures only as needed. Honor an explicit delivery target."
4
4
  ---
5
5
 
6
6
  # astack
7
7
 
8
- You own the outcome up to a PR the user can review and merge with confidence. Nobody should have to come back and ask "did you check mobile", "is the event firing" or "did CI pass".
8
+ By default, own the outcome up to a PR the user can review and merge with confidence. Honor an explicit target and existing authority. Nobody should have to come back and ask "did you check mobile", "is the event firing" or "did CI pass".
9
9
 
10
10
  `<skill-dir>` means this skill's folder. Run its scripts from the project root.
11
11
 
12
- ## Start by writing the stages into your todo list
12
+ ## Own the work in one loop
13
13
 
14
- Copy these eight lines into your todo list before any task-specific items. Close each one with its proof, or leave it in the list as `skip: <one-line reason>`. A stage that silently disappears is a stage you didn't do.
14
+ Keep context, decisions and execution in the main agent. Load another skill or reference only when its procedure changes the next step; the names below are options, not a required chain. Small fixes need only the relevant checks. For sustained work, track the acceptance criteria and their evidence in the existing plan or PR.
15
15
 
16
16
  1. **Context**: outcome, decisions already made, starting state, the one unknown that could change the approach. (`context`)
17
17
  2. **Plan**: every requirement has a change, a proof and a target. Measurement and delivery are in it. ([planning](references/plan.md); small fixes need three lines)
@@ -19,23 +19,25 @@ Copy these eight lines into your todo list before any task-specific items. Close
19
19
  4. **Use it**: the real journey, in a browser or runtime, to the real outcome; fix and replay. (`dogfood`)
20
20
  5. **Measure**: the success action fires once, on real success, and arrives. (`instrument`, when there is a success action)
21
21
  6. **Clean**: `deslop` scan on changed copy, UI and code; every medium or high hit fixed or dismissed with a reason.
22
- 7. **Verify independently**: a fresh agent that did not write the change exercises it and returns PASS or FAIL with evidence. For visual work it also scores its own renders with `web-design`'s review rubric and gives ship, fix or rework. (`ship`; docs-only or trivial changes may skip)
22
+ 7. **Verify**: run the repo's required checks and exercise the affected behavior. Use an independent reviewer when subjective quality, security, money, migrations or unresolved uncertainty warrants a separate perspective. Give it acceptance criteria and source evidence, not your conclusions. For visual work, inspect renders against the product's design and the user's references.
23
23
  8. **PR ready**: `node <skill-dir>/scripts/pr-state.mjs` reports no blockers on the current head; the PR body says what changed, how it was proven, and what was not verified. (`ship`)
24
24
 
25
- For visual changes, capture 390 and 1440 in light and dark with [`scripts/shoot.mjs`](scripts/shoot.mjs) and look at the images. Use [completion checks](references/completion-checks.md) as the final gate.
25
+ For visual changes, capture 390 and 1440 with [`scripts/shoot.mjs`](scripts/shoot.mjs) and inspect the images, including the themes and states the product supports. Read [completion checks](references/completion-checks.md) when reconciling a larger change before delivery.
26
+
27
+ Delegate when parallel work needs independent context, reading would crowd out the main task, or an independent verdict matters. Keep ownership separate and return evidence, limitations and affected files. A specialist name alone is no reason to start another agent.
26
28
 
27
29
  ## What you may do without asking
28
30
 
29
- Invoking astack is the permission for everything reversible that the stages need: create a branch, run the app and dev servers, drive a browser, take screenshots, run any test method, install dev dependencies the repo already declares, push the branch, open or update the PR, reply to review threads with fixes, and re-run CI after a diagnosis.
31
+ Within the user's authorized task and target: create a branch, run the app and dev servers, drive a browser, take screenshots, run tests, install dev dependencies the repo already declares, push the branch, open or update the PR, and re-run CI after a diagnosis. External replies still need authority for that destination. A skill describes procedure; it does not expand the user's grant.
30
32
 
31
33
  ## Ask only when
32
34
 
33
35
  - you cannot find a way to prove a change;
34
36
  - the goal is ambiguous in a way that changes the result;
35
- - the next step is outside the grant: merging, deploying to production, deleting data, spending money, messaging people, changing anything outside the repo;
37
+ - the next step is outside the existing grant, such as merging, production deployment, deleting data, spending money or messaging people;
36
38
  - another PR or decision makes this one obsolete.
37
39
 
38
- Ask with the harness's question tool (`AskUserQuestion` in Claude Code, `request_user_input` in Codex), not in a normal message, or the thread looks finished. Put the context and your recommendation in the question. If no tool exists or nobody answers, take the safe path: leave the work on its branch or open PR, and start your final message with the question. Decide everything else yourself and list those decisions in the report.
40
+ Reuse authority already given for the same action and target. If a material decision remains, use the client's available question mechanism and explain what it changes. Continue independent work while waiting. Without required authority, leave the result concrete and reviewable at the authorized target. Decide routine implementation details yourself.
39
41
 
40
42
  ## Do not stop early
41
43
 
@@ -43,9 +45,9 @@ Ending your turn while checks or reviews are pending ends the work, because nobo
43
45
 
44
46
  ## Finish
45
47
 
46
- - **Outcome**: one sentence, at the PR-ready target.
47
- - **Proof**: PR link, checks, the independent verdict, screenshots or queries, each with how it was observed.
48
+ - **Outcome**: one sentence at the agreed target, defaulting to PR-ready.
49
+ - **Proof**: PR link, verification method and any independent verdict, screenshots or queries, each with how it was observed.
48
50
  - **Decisions you made** for the user.
49
51
  - **Not verified**: the exact gap and next step, if any.
50
52
 
51
- To take it further (merge, deploy, verify live, tell the team) the user says `oneshot`.
53
+ Further delivery follows the user's explicit target and existing authority; `oneshot` requests the full path.
@@ -1,3 +1,3 @@
1
- interface:
1
+ interface:
2
2
  display_name: astack
3
3
  short_description: "Own a product task up to a proven, review-ready PR"
@@ -13,7 +13,8 @@ Run this before writing "done". Every line is either checked, not applicable (sa
13
13
 
14
14
  - [ ] Captured at 390 and 1440, light and dark, reduced motion on (`node <skill-dir>/scripts/shoot.mjs <url>`), and the screenshots were looked at.
15
15
  - [ ] No horizontal overflow, stranded single words in headings, tap targets under 44px or text under 12px that you did not choose on purpose.
16
- - [ ] Loading, empty, error and success states seen, not assumed.
16
+ - [ ] Phone lines from that capture are clean or chosen on purpose: no input under 16px, no zoom lock, no ungated `:hover`, no selectable control text, tap highlight disabled. Name anything that still needs a phone in the hand.
17
+ - [ ] Loading, empty, error and success states seen, not assumed. Where a screen waits on data, `shoot.mjs <loading> --shift <loaded>` exits 0. Exit 1 is a shift to fix. Exit 2 means the regions were not marked, which is not a pass.
17
18
  - [ ] `deslop` scan run on changed copy and UI; every medium/high hit fixed or dismissed with a reason.
18
19
 
19
20
  ## Measurement (when the change has a success action)
@@ -29,23 +29,43 @@ Options:
29
29
  offsets depend on capture speed and are reported)
30
30
  --click SELECTOR Click this element before capturing (e.g. to open a menu)
31
31
  --scale N Device scale factor (default: 2)
32
+ --shift URL Load this second page at the same sizes. The first
33
+ page is the loading state, this one the settled state.
34
+ Reports regions (same id or data-region) whose box
35
+ moved by more than 1px, and any change in document
36
+ scroll height. Exits 1 when one did. Exits 2 when no
37
+ region was marked and the scroll height did not change,
38
+ which is not a pass. Settled shots are named with -settled.
32
39
  --json Print a JSON manifest instead of text
33
40
 
34
41
  Set SHOOT_CHROME_FLAGS for extra browser flags. --no-sandbox is added
35
42
  automatically when CI is set or when running as root.
36
43
 
37
- Also reports: horizontal overflow, stranded words in wrapped text, tap
38
- targets under 44px (widths < 768), text under 12px, console errors, failed
39
- requests and
40
- elements wider than the viewport, per capture. Set CHROME_PATH to override
41
- browser discovery. The temporary browser profile is deleted afterwards.
42
- Exits 1 if the page fails to load or returns an HTTP error. A bare
44
+ Also reports, per capture: horizontal overflow, elements wider than the
45
+ viewport, stranded words in wrapped text, tap targets under 44px and
46
+ selectable control text (widths < 768), text under 12px, inputs under 16px,
47
+ a viewport that disables zoom, :hover rules outside @media (hover: hover),
48
+ a missing tap-highlight reset when the page has controls, console errors
49
+ and failed requests. Set CHROME_PATH to override browser discovery. The
50
+ temporary browser profile is deleted afterwards. Exits 1 if a page fails
51
+ to load, returns an HTTP error, or --shift finds a moved region or a
52
+ document scroll height change of more than 1px. Exits 2 when --shift could
53
+ not compare a marked region and the scroll height did not change. A bare
43
54
  host:port (localhost:3000) is treated as http://.
55
+ The layout-shift entries on the performance timeline are not read. Headless
56
+ Chrome can report none of them for a jump that has already painted, so a
57
+ score of 0 would pass a page that moved. The two-page box compare is the check.
44
58
  `;
45
59
 
60
+ function resolveTarget(value) {
61
+ if (/^[a-z]+:\/\//i.test(value)) return value;
62
+ return !existsSync(value) && /^(?:localhost|[\w-]+(?:\.[\w-]+)+|\d+\.\d+\.\d+\.\d+)(?::\d+)?(?:\/|$)|^[\w.-]+:\d+/.test(value)
63
+ ? `http://${value}` : pathToFileURL(resolve(value)).href;
64
+ }
65
+
46
66
  function parse(argv) {
47
- const o = { target: "", out: "shots", widths: [390, 1440], height: 0, themes: ["light", "dark"],
48
- motion: "reduce", full: false, wait: 600, frames: 0, every: 100, click: "", scale: 2, json: false };
67
+ const o = { target: "", shift: "", out: "shots", widths: [390, 1440], height: 0, themes: ["light", "dark"],
68
+ motion: "reduce", full: false, wait: 600, frames: 0, every: 100, click: "", scale: 2, shotSuffix: "", json: false };
49
69
  for (let i = 0; i < argv.length; i++) {
50
70
  const a = argv[i], v = () => { const x = argv[++i]; if (x === undefined) throw new Error(`${a} needs a value`); return x; };
51
71
  if (a === "--help" || a === "-h") { process.stdout.write(HELP); process.exit(0); }
@@ -60,14 +80,14 @@ function parse(argv) {
60
80
  else if (a === "--every") o.every = Number(v());
61
81
  else if (a === "--click") o.click = v();
62
82
  else if (a === "--scale") o.scale = Number(v());
83
+ else if (a === "--shift") o.shift = v();
63
84
  else if (a === "--json") o.json = true;
64
85
  else if (a.startsWith("--")) throw new Error(`Unknown option ${a}`);
65
86
  else o.target = a;
66
87
  }
67
88
  if (!o.target) { process.stdout.write(HELP); process.exit(1); }
68
- if (!/^[a-z]+:\/\//i.test(o.target))
69
- o.target = !existsSync(o.target) && /^(?:localhost|[\w-]+(?:\.[\w-]+)+|\d+\.\d+\.\d+\.\d+)(?::\d+)?(?:\/|$)|^[\w.-]+:\d+/.test(o.target)
70
- ? `http://${o.target}` : pathToFileURL(resolve(o.target)).href;
89
+ o.target = resolveTarget(o.target);
90
+ if (o.shift) o.shift = resolveTarget(o.shift);
71
91
  if (o.frames) o.motion = "no-preference";
72
92
  return o;
73
93
  }
@@ -197,7 +217,7 @@ async function capture(cdp, o, width, theme) {
197
217
  if (!o.frames) await sleep(o.wait);
198
218
  }
199
219
  const files = [], frameTimes = [];
200
- const base = `${String(width)}-${theme}`;
220
+ const base = `${String(width)}-${theme}${o.shotSuffix || ""}`;
201
221
  if (o.frames) {
202
222
  // Capture first, measure the page afterwards, so frame 0 is as close to the trigger as possible.
203
223
  for (let f = 0; f < o.frames; f++) {
@@ -277,8 +297,64 @@ async function capture(cdp, o, width, theme) {
277
297
  const size = parseFloat(getComputedStyle(el).fontSize);
278
298
  if (size < 12) { tinyText.push(label(el) + " " + size + "px"); if (tinyText.length >= 5) break; }
279
299
  }
300
+ const inputsUnder16 = [];
301
+ for (const el of document.querySelectorAll("input:not([type=hidden]):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=reset]):not([type=file]):not([type=image]), textarea, select")) {
302
+ if (!visible(el)) continue;
303
+ const size = parseFloat(getComputedStyle(el).fontSize);
304
+ if (size < 16) { inputsUnder16.push((el.getAttribute("aria-label") || el.id || el.getAttribute("name") || el.getAttribute("placeholder") || el.tagName.toLowerCase()) + " " + (Math.round(size * 10) / 10) + "px"); if (inputsUnder16.length >= 8) break; }
305
+ }
306
+ const viewportMeta = document.querySelector('meta[name="viewport" i]')?.getAttribute("content") || "";
307
+ const zoomLocked = /user-scalable\\s*=\\s*(?:no|0)\\b|maximum-scale\\s*=\\s*1(?:\\.0+)?(?![.\\d])/i.test(viewportMeta);
308
+ const ungatedHovers = [];
309
+ let ungatedHoverCount = 0, tapHighlightDisabled = false;
310
+ const coversRoot = selector => selector.split(",").some(s => /^(?:html|:root|\\*|body)(?:\\s|$)/.test(s.trim()));
311
+ const visitRules = (rules, media) => {
312
+ if (!rules) return;
313
+ for (const rule of rules) {
314
+ if (rule.type === 4) visitRules(rule.cssRules, (media ? media + " " : "") + (rule.conditionText || ""));
315
+ else if (rule.type === 3 && rule.styleSheet) { try { visitRules(rule.styleSheet.cssRules, media); } catch (e) { /* unreadable import */ } }
316
+ else {
317
+ if (rule.type === 1 && rule.selectorText) {
318
+ if (/:hover\\b/.test(rule.selectorText) && !/\\(\\s*(?:any-)?hover\\s*:\\s*hover\\s*\\)/.test(media) && !/hover\\s*:\\s*none/.test(media)) {
319
+ ungatedHoverCount++;
320
+ if (ungatedHovers.length < 8) ungatedHovers.push(rule.selectorText.slice(0, 100));
321
+ }
322
+ const tap = rule.style && rule.style.getPropertyValue("-webkit-tap-highlight-color");
323
+ if (tap && /transparent|rgba\\(\\s*0\\s*,\\s*0\\s*,\\s*0\\s*,\\s*0\\s*\\)/i.test(tap) && coversRoot(rule.selectorText)) tapHighlightDisabled = true;
324
+ }
325
+ if (rule.cssRules && rule.type !== 4) visitRules(rule.cssRules, media);
326
+ }
327
+ }
328
+ };
329
+ const seenSheets = new Set();
330
+ for (const sheet of [...document.styleSheets, ...(document.adoptedStyleSheets || [])]) {
331
+ if (seenSheets.has(sheet)) continue;
332
+ seenSheets.add(sheet);
333
+ try { visitRules(sheet.cssRules, ""); } catch (e) { /* cross-origin sheet */ }
334
+ }
335
+ const inlineTap = document.documentElement.style.getPropertyValue("-webkit-tap-highlight-color");
336
+ if (inlineTap && /transparent/i.test(inlineTap)) tapHighlightDisabled = true;
337
+ const hasControls = !!document.querySelector("a[href], button, [role=button], input, select, textarea");
338
+ const selectableControls = [];
339
+ if (vw < 768) for (const el of document.querySelectorAll("button, [role=button]")) {
340
+ if (!visible(el) || !(el.innerText || "").trim()) continue;
341
+ const cs = getComputedStyle(el);
342
+ if ((cs.userSelect || cs.webkitUserSelect) !== "none") { selectableControls.push(label(el)); if (selectableControls.length >= 8) break; }
343
+ }
344
+ const regions = [], seenKeys = new Set();
345
+ for (const el of document.querySelectorAll("[id], [data-region]")) {
346
+ if (!visible(el)) continue;
347
+ const key = el.id ? "#" + el.id : "region:" + el.getAttribute("data-region");
348
+ if (seenKeys.has(key)) continue;
349
+ seenKeys.add(key);
350
+ const box = el.getBoundingClientRect();
351
+ regions.push({ key, top: Math.round(box.top), left: Math.round(box.left), width: Math.round(box.width), height: Math.round(box.height) });
352
+ if (regions.length >= 80) break;
353
+ }
280
354
  return { scrollWidth: document.documentElement.scrollWidth, viewport: vw, overflowing: wide.map(w => w.text), stranded, smallTargets, tinyText,
281
- height: document.documentElement.scrollHeight, title: document.title };
355
+ height: document.documentElement.scrollHeight, title: document.title,
356
+ inputsUnder16, zoomLocked, ungatedHovers, ungatedHoverCount, tapHighlightDisabled: hasControls ? tapHighlightDisabled : null,
357
+ selectableControls, regions };
282
358
  })()` });
283
359
  if (!o.frames) {
284
360
  const shot = await cdp.send("Page.captureScreenshot", { format: "png", captureBeyondViewport: o.full });
@@ -287,10 +363,36 @@ async function capture(cdp, o, width, theme) {
287
363
  }
288
364
  const l = layout.result.value;
289
365
  return { width, height, theme, motion: o.motion, files, ...(o.frames ? { frameTimesMs: frameTimes } : {}), title: l.title,
366
+ scrollHeight: l.height, regions: l.regions,
367
+ inputsUnder16: l.inputsUnder16, zoomLocked: l.zoomLocked, ungatedHovers: l.ungatedHovers, ungatedHoverCount: l.ungatedHoverCount,
368
+ tapHighlightDisabled: l.tapHighlightDisabled, selectableControls: l.selectableControls,
290
369
  horizontalOverflow: l.scrollWidth > l.viewport ? `${l.scrollWidth - l.viewport}px` : null,
291
370
  overflowingElements: l.overflowing, strandedWords: l.stranded, smallTapTargets: l.smallTargets, tinyText: l.tinyText, ...issues };
292
371
  }
293
372
 
373
+ // Same id or data-region in both pages. A disappearing spinner is not a shift;
374
+ // a card that grows, or a control that moves, is.
375
+ function compareShift(before, after) {
376
+ const settled = new Map((after.regions || []).map(r => [r.key, r]));
377
+ const shifts = [];
378
+ let shared = 0;
379
+ for (const region of before.regions || []) {
380
+ const other = settled.get(region.key);
381
+ if (!other) continue;
382
+ shared++;
383
+ const dx = other.left - region.left, dy = other.top - region.top, dw = other.width - region.width, dh = other.height - region.height;
384
+ if (Math.abs(dx) > 1 || Math.abs(dy) > 1 || Math.abs(dw) > 1 || Math.abs(dh) > 1)
385
+ shifts.push({ key: region.key, before: region, after: other, dx, dy, dw, dh });
386
+ }
387
+ const scrollDelta = (after.scrollHeight ?? 0) - (before.scrollHeight ?? 0);
388
+ return {
389
+ scrollBefore: before.scrollHeight ?? null, scrollAfter: after.scrollHeight ?? null,
390
+ scrollDelta: Math.abs(scrollDelta) > 1 ? scrollDelta : 0, shared, shifts,
391
+ settledFiles: after.files,
392
+ ...(shared === 0 ? { note: "no shared id or data-region; only the document scroll height was compared" } : {}),
393
+ };
394
+ }
395
+
294
396
  async function main() {
295
397
  const o = parse(process.argv.slice(2));
296
398
  mkdirSync(o.out, { recursive: true });
@@ -300,15 +402,31 @@ async function main() {
300
402
  for (const width of o.widths) for (const theme of o.themes) {
301
403
  const cdp = await connect(browser.port);
302
404
  await cdp.send("Page.enable"); await cdp.send("Runtime.enable"); await cdp.send("Network.enable");
303
- results.push(await capture(cdp, o, width, theme));
405
+ const before = await capture(cdp, o, width, theme);
304
406
  cdp.close();
407
+ if (o.shift) {
408
+ const settled = await connect(browser.port);
409
+ await settled.send("Page.enable"); await settled.send("Runtime.enable"); await settled.send("Network.enable");
410
+ const after = await capture(settled, { ...o, target: o.shift, shotSuffix: "-settled" }, width, theme);
411
+ settled.close();
412
+ before.shift = compareShift(before, after);
413
+ if (after.loadError && !before.loadError) before.loadError = `settled page: ${after.loadError}`;
414
+ }
415
+ results.push(before);
305
416
  }
306
417
  } finally { await browser.close(); }
307
418
  const failed = results.some(r => r.loadError);
308
- if (failed) process.exitCode = 1;
419
+ const shifted = results.some(r => r.shift && (r.shift.shifts.length > 0 || r.shift.scrollDelta));
420
+ // No marked region and no scroll change means the jump was not measured.
421
+ // Scroll height stays at the viewport when both states fit, so that case must not pass.
422
+ const unchecked = results.some(r => r.shift && r.shift.shared === 0 && !r.shift.scrollDelta);
423
+ if (failed || shifted) process.exitCode = 1;
424
+ else if (unchecked) process.exitCode = 2;
309
425
  if (o.json) { process.stdout.write(JSON.stringify({ target: o.target, captures: results }, null, 2) + "\n"); return; }
310
426
  for (const r of results) {
311
- const flags = [r.loadError, r.horizontalOverflow && `horizontal overflow ${r.horizontalOverflow}`,
427
+ const shiftFlag = r.shift && (r.shift.shifts.length > 0 || r.shift.scrollDelta) ? "layout shift" : "";
428
+ const uncheckedFlag = r.shift && r.shift.shared === 0 && !r.shift.scrollDelta ? "shift not checked" : "";
429
+ const flags = [r.loadError, shiftFlag, uncheckedFlag, r.horizontalOverflow && `horizontal overflow ${r.horizontalOverflow}`,
312
430
  r.consoleErrors.length && `${r.consoleErrors.length} console error(s)`,
313
431
  r.failedRequests.length && `${r.failedRequests.length} failed request(s)`].filter(Boolean);
314
432
  process.stdout.write(`${r.width}px ${r.theme}: ${r.files.join(", ")}${flags.length ? " !! " + flags.join("; ") : ""}\n`);
@@ -316,6 +434,20 @@ async function main() {
316
434
  for (const e of r.strandedWords) process.stdout.write(` stranded words: ${e}\n`);
317
435
  for (const e of r.smallTapTargets) process.stdout.write(` tap target < 44px: ${e}\n`);
318
436
  for (const e of r.tinyText) process.stdout.write(` text < 12px: ${e}\n`);
437
+ for (const e of r.inputsUnder16 || []) process.stdout.write(` input under 16px: ${e}\n`);
438
+ if (r.zoomLocked) process.stdout.write(` viewport disables zoom\n`);
439
+ if (r.ungatedHoverCount) process.stdout.write(` hover not gated (${r.ungatedHoverCount}): ${(r.ungatedHovers || []).join("; ")}\n`);
440
+ if (r.tapHighlightDisabled === false) process.stdout.write(` tap highlight not disabled\n`);
441
+ for (const e of r.selectableControls || []) process.stdout.write(` control text selectable: ${e}\n`);
442
+ if (r.shift) {
443
+ if (r.shift.note) process.stdout.write(` shift: ${r.shift.note}\n`);
444
+ if (r.shift.scrollDelta) process.stdout.write(` document scroll height ${r.shift.scrollDelta > 0 ? "+" : ""}${r.shift.scrollDelta}px after load\n`);
445
+ for (const s of r.shift.shifts.slice(0, 12)) {
446
+ const part = (n, label) => n ? `${label} ${n > 0 ? "+" : ""}${n}px` : "";
447
+ const bits = [part(s.dy, "top"), part(s.dh, "height"), part(s.dx, "left"), part(s.dw, "width")].filter(Boolean);
448
+ process.stdout.write(` layout shift ${s.key}: ${bits.join(", ")}\n`);
449
+ }
450
+ }
319
451
  for (const e of [...new Set(r.consoleErrors)].slice(0, 5)) process.stdout.write(` console: ${e}\n`);
320
452
  for (const e of [...new Set(r.failedRequests)].slice(0, 5)) process.stdout.write(` request: ${e}\n`);
321
453
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: context
3
- description: Gather what actually changes a task before starting it, from Slack threads, issues, PRs, docs, git history, the live product and analytics, and reconcile it into a short brief. Use when picking up a ticket, a Slack ask, a half-finished branch, "look into X", or any task where earlier decisions, corrections or numbers live somewhere other than the prompt.
3
+ description: "Reconcile prior decisions and current evidence into a brief that changes the next step. Use when picking up a ticket, thread or unfinished branch whose scope depends on history outside the prompt."
4
4
  ---
5
5
 
6
6
  # Context
@@ -21,7 +21,7 @@ Search for those answers. Stop reading sources that answer nothing on the list.
21
21
 
22
22
  If the task touches existing code, anchor first: the files and line ranges involved, `git blame` on them, the last commits and the PRs those commits came from (`gh pr view <n> --json title,body,comments,reviews`). That anchor tells you which threads and decisions to look for.
23
23
 
24
- Then go through each kind of evidence that is available: code history, the issue tracker, docs, team chat, error tracking, analytics. One that has nothing relevant is still a finding; write "searched, nothing" rather than skipping it silently. One you cannot reach is a gap; say so.
24
+ Follow the sources most likely to settle the named unknowns: code history, the issue tracker, docs, team chat, error tracking or analytics. Stop when the next action is clear. Report an unreachable source when it leaves a material question unresolved; an exhaustive source sweep is not a completion requirement.
25
25
 
26
26
  Use the connected tools; prefer the original over a summary of it. Commands that pay off:
27
27
 
@@ -34,10 +34,10 @@ git log -p --follow -- path/to/file | head -200
34
34
  git blame -L 120,160 path/to/file
35
35
  ```
36
36
 
37
- - **Slack / chat**: read the whole thread including late replies; the correction is usually at the bottom. Search with the channel and date bounds (`in:#channel after:2026-01-01 "keyword"`), then open each hit in context.
38
- - **Docs and specs**: check the date and whether later code or chat superseded them.
39
- - **Live product**: open the real page or call the real endpoint. What it does today beats what a doc says it does.
40
- - **Analytics**: read the definition (event, filters, window) before reading the number. A number without its definition is not context.
37
+ - Slack / chat: read the whole thread including late replies; the correction is usually at the bottom. Search with the channel and date bounds (`in:#channel after:2026-01-01 "keyword"`), then open each hit in context.
38
+ - Docs and specs: check the date and whether later code or chat superseded them.
39
+ - Live product: open the real page or call the real endpoint. What it does today beats what a doc says it does.
40
+ - Analytics: read the definition (event, filters, window) before reading the number. A number without its definition is not context.
41
41
 
42
42
  See [source selection](references/source-selection.md) for which source wins when they disagree, and for multi-source tasks.
43
43
 
@@ -1,3 +1,3 @@
1
- interface:
1
+ interface:
2
2
  display_name: Context
3
3
  short_description: Gather and reconcile what changes the task
@@ -22,7 +22,7 @@
22
22
 
23
23
  ## Signs the context is wrong
24
24
 
25
- - The brief has no "Open" line. Something is always uncertain; find it.
25
+ - A material unknown is hidden. If no remaining uncertainty changes the next step, say "Open: none material" and proceed.
26
26
  - The brief repeats the prompt. You read nothing that changed the work.
27
27
  - A number appears without project, population, filter and window.
28
28
  - A "decided" item has no link or date.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: deslop
3
- description: Strip the tells of generated work from copy, UI and code, such as inflated vocabulary, "not just X but Y", staged reveals, em-dash rhythm, emoji bullets, gradient-and-glass UI, narrating comments and defensive clutter, while keeping facts, behaviour and voice. Use when the user says deslop, "this sounds like AI", "make it sound human", "tighten this", before publishing copy or merging UI, or when writing anything public. Also builds a voice profile from someone's real writing.
3
+ description: "Write or revise public copy, UI and code without generated tells, preserving facts and voice. Use for \"deslop\", \"sounds like AI\", tightening prose, user emails, removing generic UI or learning a voice from real writing."
4
4
  ---
5
5
 
6
6
  # Deslop
@@ -33,7 +33,11 @@ Work through [prose patterns](references/prose-patterns.md). The ones that matte
33
33
 
34
34
  ## 3. Fix UI
35
35
 
36
- Run the scan on components and check the rendered page against `web-design`'s anti-patterns if installed. The usual repairs: delete the eyebrow labels and explainer subtitles; replace gradient text with solid ink; remove glass, glows and sparkles; flatten nested cards; give CTAs real verbs; remove emoji from interface copy. Re-shoot and look after removing structure: a deleted container can take a needed grouping with it.
36
+ Run the scan on components and check the rendered page against `web-design`'s anti-patterns if installed. The usual repairs: delete the eyebrow labels and explainer subtitles; replace gradient text with solid ink; remove glass, glows and sparkles; flatten nested cards; give CTAs real verbs; remove emoji from interface copy.
37
+
38
+ This season's tells, each fine when the brief asks and slop when it shows up anyway: a pill tag above the heading (`New`), one italic word in the headline, a divider under every section, four opacities of the same neutral instead of two inks, a border on every box, a diagram that draws the whole system, a decorative `Home / Settings / Billing`, and `01` `02` `03` on items that are not a sequence. The scan flags the numbered markers (`section-index`) and the opacity ladder (`opacity-ramp`). The rest need the render. Instrument Serif with tight tracking is the current reflex display; it stays only when DESIGN.md names it.
39
+
40
+ Re-shoot and look after removing structure: a deleted container can take a needed grouping with it.
37
41
 
38
42
  ## 4. Fix code
39
43
 
@@ -1,3 +1,3 @@
1
- interface:
1
+ interface:
2
2
  display_name: Deslop
3
3
  short_description: "Strip AI tells from copy, UI and code"
@@ -65,6 +65,7 @@ const RULES = [
65
65
  ["caps-eyebrow", "ui", "info", /\buppercase\b[^"'`]*\btracking-(?:widest|\[0\.[1-9]\d*em\])|\btracking-(?:widest|\[0\.[1-9]\d*em\])[^"'`]*\buppercase\b|text-transform:\s*uppercase;\s*letter-spacing:\s*0?\.[12]\d*em/g, "Tracked-out all-caps label; the eyebrow above every heading."],
66
66
  ["generic-cta", "ui", "info", />\s*(?:Get Started|Learn More|Start (?:your )?(?:free )?trial|Join the waitlist|Explore (?:now|features))\s*</g, "Generic CTA label; name what happens next ('Create a listing', 'See rates')."],
67
67
  ["emoji-in-ui", "ui", "medium", />[^<{]*?(?:🚀|✨|🔥|💡|⚡|🎉|👋|💪|🙌|📈|🎯)[^<]*?</gu, "Emoji in interface copy; usually stands in for an icon system or a point of view."],
68
+ ["section-index", "ui", "info", />\s*0[1-9]\s*(?:<\/|—|–|-|\/)/g, "Numbered marker (01, 02) on content that is not a sequence. Delete it, or keep it only where order is the point."],
68
69
  // Motion and mobile details that make an interface feel slightly off.
69
70
  ["transition-all", "ui", "medium", /\btransition-all\b|transition:\s*all\b/g, "Transitions every property, including layout ones nobody meant to animate. Name them: transform, opacity."],
70
71
  ["ease-in-ui", "ui", "info", /\bease-in\b(?!-out)|(?:transition|animation)(?:-timing-function)?:[^;]*\bease-in\b(?!-out)/g, "ease-in starts slowly, exactly when the user is watching; UI enters and exits on ease-out."],
@@ -161,6 +162,9 @@ function scanFile(file, o) {
161
162
  const acid = hexes.some(([r, g, b]) => (g >= 0xd0 && r >= 0x80 && r <= 0xe0 && b <= 0x60) || (r >= 0xe0 && g <= 0x60 && b <= 0x40)) || /\b(?:text|bg|border)-lime-\d{3}\b/.test(src);
162
163
  if (cream && serif) density.push({ rule: "warm-editorial", severity: "medium", detail: `cream background with a serif display${clay ? " and a clay/terracotta accent" : ""}`, why: "Today's most common generated look; fine if the brief asked for it, slop if it arrived by default." });
163
164
  if (nearBlack && acid) density.push({ rule: "dark-acid", severity: "info", detail: "near-black background with an acid/vermilion accent", why: "A current generated-design default; check it was a choice." });
165
+ const opacities = [...src.matchAll(/\b(?:text|bg)-(?:black|white|neutral|zinc|gray|slate|stone)\/(?:[1-9]\d)\b/g)].map(m => m[0].replace(/.*\//, ""));
166
+ const distinct = [...new Set(opacities)];
167
+ if (distinct.length >= 4) density.push({ rule: "opacity-ramp", severity: "info", detail: `${distinct.length} opacities of one neutral (${distinct.sort().join(", ")})`, why: "A ladder of opacities standing in for a type scale. Two inks, primary and secondary, are enough unless the brand defines more." });
164
168
  }
165
169
  const boldLeads = hits.filter(h => h.rule === "bold-lead-list").length;
166
170
  if (boldLeads > DENSITY["bold-lead-list"]) density.push({ rule: "bold-lead-density", severity: "medium", detail: `${boldLeads} bold-lead bullets`, why: "Every list as '**Label:** text' reads as generated; use prose or a table where it fits." });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: discoverability
3
- description: Find out why the right people do not find or choose a product through Google, Bing and AI answer engines (ChatGPT, Claude, Perplexity, AI Overviews), fix the cause, and measure the result. Use for SEO audits, "why aren't we ranking", "does ChatGPT recommend us", GEO/AEO, indexing problems, canonical/robots/sitemap issues, comparison and alternatives pages, or before and after a content push.
3
+ description: "Diagnose and fix why Google, Bing or AI answers miss or misdescribe a product. Use for SEO/GEO/AEO, ranking or indexing problems, canonical/robots/sitemap checks and search-focused content."
4
4
  ---
5
5
 
6
6
  # Discoverability
@@ -1,3 +1,3 @@
1
- interface:
1
+ interface:
2
2
  display_name: Discoverability
3
3
  short_description: Fix why search and AI answers miss the product
@@ -1,12 +1,14 @@
1
1
  ---
2
2
  name: dogfood
3
- description: Use the product like a real user, signed in, with real data, through to the result they came for, then fix what breaks and repeat until the journeys pass. Use for "test this end to end", "QA the app", "use it like a customer", "find what's broken", pre-launch checks, after a big change, or for AI features whose answers need repeated real trials. Fixes as it goes when fixes are authorised.
3
+ description: "Walk real product journeys, fix authorized problems and replay through the outcome. Use for end-to-end QA, signed-in mobile checks, pre-launch testing and repeated trials of AI features. Review-only requests produce findings."
4
4
  ---
5
5
 
6
6
  # Dogfood
7
7
 
8
8
  Tests check what the author thought of. Dogfooding finds what a user hits. The output is fixed journeys, not a list of complaints.
9
9
 
10
+ `<skill-dir>` below means this skill's own folder. Run its scripts from the project root so output lands in the project, not in the installed skill.
11
+
10
12
  ## 0. Pick the mode
11
13
 
12
14
  - **Diff-aware** (default on a branch): start from what the branch changed (`git diff origin/main...HEAD --stat`), map each change to the screens and journeys it touches, and test those first.
@@ -28,6 +30,7 @@ Use the browser session and account the user selected; never sign in as someone
28
30
  - Real clicks and typing in a real browser, at real speed. No mocked responses, no seeding past the hard part.
29
31
  - Follow through to the outcome: the record exists, the email arrived, the export opens and contains the right rows, the public page shows the change.
30
32
  - Look at each screen. Capture the important ones at phone and desktop width with [`scripts/shoot.mjs`](scripts/shoot.mjs) when the URL is reachable without your session; use the browser tool's screenshots for signed-in states.
33
+ - When the journey waits on data, capture the waiting page and the settled page and compare them: `node <skill-dir>/scripts/shoot.mjs <waiting> --shift <settled> --widths 390 --themes light`. Exit 1 is a finding (a marked region or the document moved). Exit 2 means the regions were not marked, so the jump was not measured. Read the phone lines on that capture too: input size, zoom lock, ungated hover, selectable controls, tap highlight. A 390px picture can look aligned and still jump, zoom and stick. The lines that need a physical phone (a hover that stays after a tap, the notch, the keyboard) stay named as unverified.
31
34
  - Keep the console and network panel open. A 500 behind a friendly toast is a finding.
32
35
  - Note friction as well as failure: a step that needs thinking, a label that misleads, a wait with no feedback.
33
36
 
@@ -1,3 +1,3 @@
1
- interface:
1
+ interface:
2
2
  display_name: Dogfood
3
3
  short_description: "Use the product for real, fix what breaks"