ultra11y 2.26.0 → 2.28.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 (36) hide show
  1. package/.agents/plugins/marketplace.json +12 -0
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +21 -0
  5. package/.opencode/plugins/ultra11y.js +102 -0
  6. package/README.md +34 -16
  7. package/hooks/hooks.json +1 -1
  8. package/hooks/pre-tool-use.mjs +19 -8
  9. package/package.json +6 -2
  10. package/scripts/sync-version.mjs +12 -4
  11. package/scripts/ultra11y.mjs +831 -150
  12. package/scripts/vendor/wcag-2.2-fr.json +1 -1
  13. package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
  14. package/scripts/verify-skill-bundle.mjs +134 -1
  15. package/skills/review-a11y/SKILL.md +5 -1
  16. package/skills/review-a11y/scripts/ultra11y.mjs +831 -150
  17. package/skills/ultra11y/SKILL.md +2 -1
  18. package/skills/ultra11y/references/automation.md +9 -4
  19. package/skills/ultra11y/references/harnesses.md +117 -0
  20. package/skills/ultra11y/scripts/ultra11y.mjs +831 -150
  21. package/src/cli.ts +116 -17
  22. package/src/data/wcag-universe.json +1 -1
  23. package/src/hook.ts +0 -0
  24. package/src/init.ts +26 -2
  25. package/src/install/agents-md.ts +78 -0
  26. package/src/install/claude-code.ts +61 -0
  27. package/src/install/codex.ts +166 -0
  28. package/src/install/index.ts +204 -0
  29. package/src/install/json-edit.ts +97 -0
  30. package/src/install/opencode.ts +90 -0
  31. package/src/install/paths.ts +132 -0
  32. package/src/install/text-edit.ts +54 -0
  33. package/src/types.ts +1 -1
  34. package/src/vendor/codeindex-engine.d.mts +3 -3
  35. package/src/vendor/codeindex-engine.mjs +23 -3
  36. package/src/vendor/engine.meta.json +5 -5
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "ultra11y",
3
+ "interface": { "displayName": "ultra11y" },
4
+ "plugins": [
5
+ {
6
+ "name": "ultra11y",
7
+ "source": { "source": "url", "url": "./" },
8
+ "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" },
9
+ "category": "Productivity"
10
+ }
11
+ ]
12
+ }
@@ -10,7 +10,7 @@
10
10
  "name": "ultra11y",
11
11
  "source": "./",
12
12
  "description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
13
- "version": "2.26.0",
13
+ "version": "2.28.0",
14
14
  "author": { "name": "maxgfr" },
15
15
  "homepage": "https://github.com/maxgfr/ultra11y",
16
16
  "repository": "https://github.com/maxgfr/ultra11y",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultra11y",
3
3
  "description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
4
- "version": "2.26.0",
4
+ "version": "2.28.0",
5
5
  "author": { "name": "maxgfr" },
6
6
  "homepage": "https://github.com/maxgfr/ultra11y",
7
7
  "repository": "https://github.com/maxgfr/ultra11y",
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "ultra11y",
3
+ "description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
4
+ "version": "2.28.0",
5
+ "author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
6
+ "homepage": "https://github.com/maxgfr/ultra11y",
7
+ "repository": "https://github.com/maxgfr/ultra11y",
8
+ "license": "MIT",
9
+ "keywords": ["accessibility", "a11y", "wcag", "rgaa", "code-review", "audit"],
10
+ "skills": "./skills/",
11
+ "interface": {
12
+ "displayName": "ultra11y",
13
+ "shortDescription": "WCAG 2.2 AA review before you commit, push or open a PR.",
14
+ "longDescription": "Reviews exactly the code under change against WCAG 2.2 AA. A zero-dependency static engine — 78 checks tied to the success criteria, scored against the W3C ACT corpus — plus the agent's judgment on what a static pass cannot decide, with check/verify gates against invented non-conformities. Country standards (RGAA…) are pluggable in-repo packs.",
15
+ "developerName": "maxgfr",
16
+ "category": "Productivity",
17
+ "capabilities": ["Read", "Write"],
18
+ "websiteURL": "https://github.com/maxgfr/ultra11y",
19
+ "defaultPrompt": "Review my staged changes for accessibility."
20
+ }
21
+ }
@@ -0,0 +1,102 @@
1
+ // The OpenCode plugin — what makes the `review-a11y` skill fire on its own before a
2
+ // commit, push or pull request.
3
+ //
4
+ // This is hooks/pre-tool-use.mjs in a different costume, and deliberately so. OpenCode has
5
+ // no permission-decision channel: a plugin blocks a tool call by THROWING, and the message
6
+ // is what the agent sees. So the shape is the same two-stage split — this file pays only
7
+ // the cost of a string test on each shell call, and the 2.5 MB engine is spawned only once
8
+ // something could possibly be a publishing command.
9
+ //
10
+ // TWO RULES, the same two that govern src/hook.ts.
11
+ //
12
+ // 1. NEVER break a session. Every unexpected condition — no engine, spawn failure,
13
+ // timeout, unreadable output — is a silent no-op. The single `throw` below is our own
14
+ // decision and nothing else; note that the try/catch closes BEFORE it, so a bug in here
15
+ // can never surface to the user as a blocked `git push`.
16
+ // 2. NEVER loop. The anti-loop marker lives in the engine (loopKey/firstSighting in
17
+ // src/hook.ts) and keys on the session id, so passing OpenCode's sessionID through is
18
+ // all that is required: one review per state of the change, exactly as on Claude Code.
19
+ //
20
+ // Zero dependencies, `node:` builtins only — it is loaded straight from disk by OpenCode.
21
+ import { spawnSync } from "node:child_process";
22
+ import { existsSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { dirname, join } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+
27
+ // Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
28
+ // does not carry it, so a user's own ultra11y.js is never clobbered.
29
+ const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
30
+ const ULTRA11Y_PLUGIN_VERSION = "2.28.0";
31
+
32
+ const here = dirname(fileURLToPath(import.meta.url));
33
+
34
+ /** The engine bundle, or null. Never `require.resolve` — this module is ESM and OpenCode's
35
+ * loader is not guaranteed to have the package resolvable from here. */
36
+ function resolveEngine() {
37
+ const candidates = [
38
+ process.env.ULTRA11Y_BIN, // explicit override (and what the tests set)
39
+ join(here, "ultra11y.mjs"), // a copy dropped beside the plugin
40
+ join(here, "..", "..", "scripts", "ultra11y.mjs"), // the npm-pin route: <pkgroot>/.opencode/plugins/
41
+ join(homedir(), ".ultra11y", "bin", "ultra11y.mjs"), // the pin written by `install --opencode`
42
+ ];
43
+ for (const c of candidates) if (c && existsSync(c)) return c;
44
+ return null;
45
+ }
46
+
47
+ /** Same literal as hooks/pre-tool-use.mjs — tests/harness-sync.test.ts asserts they match,
48
+ * because two prefilters that disagree mean one harness silently reviews less. */
49
+ const PREFILTER = /\bgit\b|\bgh\b/;
50
+
51
+ export const Ultra11yPlugin = async (ctx) => {
52
+ const skillsDir = join(here, "..", "..", "skills");
53
+
54
+ return {
55
+ /** Register the two bundled skills so `review-a11y` is invocable by name once the gate
56
+ * names it. Wrapped: if OpenCode's config shape differs, this is a harmless no-op and
57
+ * the user still has the skills via `ultra11y install --opencode`. */
58
+ config: async (config) => {
59
+ try {
60
+ if (!existsSync(skillsDir)) return;
61
+ config.skills ??= {};
62
+ config.skills.paths ??= [];
63
+ if (!config.skills.paths.includes(skillsDir)) config.skills.paths.push(skillsDir);
64
+ } catch {
65
+ /* not fatal — the skills can also be installed into ~/.config/opencode/skills/ */
66
+ }
67
+ },
68
+
69
+ "tool.execute.before": async (input, output) => {
70
+ let text;
71
+ try {
72
+ if (String(input?.tool ?? "").toLowerCase() !== "bash") return;
73
+ const command = output?.args?.command;
74
+ if (typeof command !== "string" || !PREFILTER.test(command)) return;
75
+ if (process.env.SKIP_A11Y || process.env.ULTRA11Y_HOOK === "off") return;
76
+
77
+ const engine = resolveEngine();
78
+ if (!engine) return;
79
+
80
+ const payload = {
81
+ tool_name: "Bash",
82
+ tool_input: { command },
83
+ cwd: ctx?.directory ?? ctx?.worktree ?? process.cwd(),
84
+ session_id: input?.sessionID ?? `opencode-${process.pid}`,
85
+ };
86
+ const run = spawnSync(process.execPath, [engine, "hook", "--opencode"], {
87
+ input: JSON.stringify(payload),
88
+ encoding: "utf8",
89
+ timeout: 20_000,
90
+ maxBuffer: 8 * 1024 * 1024,
91
+ });
92
+ text = (run.stdout ?? "").trim();
93
+ } catch {
94
+ return; // unreadable input, spawn failure, timeout — say nothing
95
+ }
96
+ // Outside the try on purpose: this is the ONLY error allowed to reach the agent.
97
+ if (text) throw new Error(text);
98
+ },
99
+ };
100
+ };
101
+
102
+ export { ULTRA11Y_OPENCODE_PLUGIN, ULTRA11Y_PLUGIN_VERSION };
package/README.md CHANGED
@@ -2,21 +2,25 @@
2
2
 
3
3
  [![CI](https://github.com/maxgfr/ultra11y/actions/workflows/ci.yml/badge.svg)](https://github.com/maxgfr/ultra11y/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/ultra11y?logo=npm)](https://www.npmjs.com/package/ultra11y)
5
- [![Claude Code plugin](https://img.shields.io/badge/Claude_Code-plugin-6b46c1)](#add-to-claude-code)
5
+ [![agent plugin](https://img.shields.io/badge/Claude_Code_·_Codex_·_OpenCode-plugin-6b46c1)](#add-to-your-coding-agent)
6
6
  [![node](https://img.shields.io/node/v/ultra11y)](https://nodejs.org)
7
7
  [![license](https://img.shields.io/npm/l/ultra11y)](LICENSE)
8
8
 
9
- ### Add to Claude Code
9
+ ### Add to your coding agent
10
10
 
11
11
  Two lines, and the accessibility review runs **by itself** before every commit, push and
12
12
  pull request — you never type the skill's name:
13
13
 
14
- ```
15
- /plugin marketplace add maxgfr/ultra11y
16
- /plugin install ultra11y@ultra11y
17
- ```
18
-
19
- <sub>Prefer the skills only, without the automatic hook? `npx skills add maxgfr/ultra11y`.
14
+ | | |
15
+ |---|---|
16
+ | **Claude Code** | `/plugin marketplace add maxgfr/ultra11y` then `/plugin install ultra11y@ultra11y` |
17
+ | **OpenAI Codex CLI** | `codex plugin marketplace add maxgfr/ultra11y` then `codex plugin add ultra11y@ultra11y` |
18
+ | **OpenCode** | `npx ultra11y install --opencode` — or pin `"plugin": ["ultra11y@latest"]` in `opencode.json` |
19
+ | **Anything else** | `npx ultra11y install --agents-md` (no hook API: see the caveat below) |
20
+
21
+ <sub>Prefer the skills only, without the automatic hook? `npx skills add maxgfr/ultra11y`
22
+ (`--agent codex`, `--agent opencode`, …). Already on npm and just want the gate?
23
+ `npx ultra11y install --all`, and `npx ultra11y status` to check what is actually wired.
20
24
  Full details, thresholds and off-switches under [Install](#install).</sub>
21
25
 
22
26
  > Audit HTML/CSS/JSX against **WCAG 2.2 AA** accessibility and produce a dated compliance report — or author/review accessible markup without regressions. A [skills.sh](https://skills.sh) agent skill: a deterministic, zero-dependency static engine **plus** the agent's judgment, with `check`/`verify` gates against hallucinated non-conformities. **The central deliverable is the auditor conformance block** — theme, criterion + official wording, test(s), WCAG mapping + level, finding, expected state, verification, `file:line` occurrences — rendered identically by the `report` (compliance doc), the `prd` backlog and the GitHub issues, in the active standard's vocabulary and **in your language** (`--lang auto` follows the conversation/repo). **WCAG is the worldwide core; country standards (RGAA, …) are pluggable in-repo packs.**
@@ -61,27 +65,41 @@ working diff, or branch vs merge-base) and reports like a code reviewer. Both bu
61
65
  install-free engine.
62
66
 
63
67
  ```sh
64
- npx skills add maxgfr/ultra11y
68
+ npx skills add maxgfr/ultra11y # this agent
69
+ npx skills add maxgfr/ultra11y --agent codex # …or opencode, cursor, gemini, amp, zed…
65
70
  ```
66
71
 
67
- **As a Claude Code plugin** — the same two skills, plus the hook that makes the review
72
+ **As an agent plugin** — the same two skills, plus the hook that makes the review
68
73
  **automatic**. Installed this way, a pending `git commit`, `git push` or `gh pr create`
69
74
  that carries accessibility findings is stopped once and the agent is handed the findings,
70
75
  so `review-a11y` runs before the work goes out — you never type the skill's name.
71
76
 
72
77
  ```
73
- /plugin marketplace add maxgfr/ultra11y
74
- /plugin install ultra11y@ultra11y
78
+ /plugin marketplace add maxgfr/ultra11y # Claude Code
79
+ codex plugin marketplace add maxgfr/ultra11y # Codex CLI
80
+ npx ultra11y install --opencode # OpenCode
75
81
  ```
76
82
 
77
- Three things worth knowing, because they bound what "automatic" means here:
83
+ Codex's hook engine is a near-clone of Claude Code's — same events, same envelope, and it
84
+ exports `${CLAUDE_PLUGIN_ROOT}` too — so both harnesses share one `hooks/hooks.json`.
85
+ OpenCode has no permission-decision channel, so there the gate surfaces as a **failed bash
86
+ call whose error message carries the findings**. Per-harness deltas, the Codex feature flag
87
+ and the trust prompt: [`skills/ultra11y/references/harnesses.md`](skills/ultra11y/references/harnesses.md).
88
+
89
+ Four things worth knowing, because they bound what "automatic" means here:
78
90
 
79
91
  - A hook **cannot force** a skill to be invoked. It blocks the command and hands over the
80
92
  reason and the findings; the agent is what invokes `review-a11y`. In practice a motivated
81
93
  `deny` is enough — it is not a guarantee.
82
- - **Hooks do not enable themselves.** Installing the plugin is what turns them on.
83
- - Claude Code has **no git event** (no `PreCommit`, no `PRCreated`), which is why the hook
84
- listens on `PreToolUse` for the Bash tool and recognises the commands that publish work.
94
+ - **Hooks do not enable themselves.** Installing the plugin is what turns them on. On Codex
95
+ they also need `[features] hooks = true`, which `install --codex` sets for you; run
96
+ `npx ultra11y status` if you are not sure what is live.
97
+ - **No harness has a git event** (no `PreCommit`, no `PRCreated`), which is why the hook
98
+ listens on `PreToolUse` for the shell tool — `Bash` on Claude Code, `shell` on Codex —
99
+ and recognises the commands that publish work.
100
+ - On a harness with **no hook API at all** (Cursor, Amp, Zed, Gemini CLI…), nothing is
101
+ automatic. `npx ultra11y install --agents-md` makes the engine discoverable and hands the
102
+ agent the adjudication protocol; `init --hook` below is what actually enforces the gate.
85
103
 
86
104
  The gate never blocks twice for the same findings, so a retry always lands. It stays out of
87
105
  the way when git's own `no-verify` bypass is used, outside a repository, and whenever the
package/hooks/hooks.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "hooks": {
3
3
  "PreToolUse": [
4
4
  {
5
- "matcher": "Bash",
5
+ "matcher": "Bash|bash|shell|local_shell|exec_command|run_command|exec",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
@@ -1,16 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  // The plugin's PreToolUse guard — deliberately tiny.
3
3
  //
4
- // Claude Code has no git-aware hook event, so this runs on EVERY Bash tool call. The
5
- // engine next to it is a 2.4 MB bundle; parsing that before each shell command would be
6
- // an unacceptable tax on a session. So the split is: this file pays only a bare Node
7
- // start and answers "could this possibly be a publishing command?", and the engine —
8
- // spawned only when the answer is yes — makes the real decision (src/hook.ts).
4
+ // No harness has a git-aware hook event, so this runs on EVERY shell tool call. The engine
5
+ // next to it is a 2.4 MB bundle; parsing that before each shell command would be an
6
+ // unacceptable tax on a session. So the split is: this file pays only a bare Node start
7
+ // and answers "could this possibly be a publishing command?", and the engine — spawned
8
+ // only when the answer is yes — makes the real decision (src/hook.ts).
9
9
  //
10
10
  // The prefilter is intentionally OVER-permissive: it matches any command mentioning git
11
11
  // or gh at all. Precision lives in one place, `matchGitIntent`, so the two cannot drift.
12
12
  //
13
- // Contract with Claude Code: print the hook JSON on stdout to act, print nothing to stay
13
+ // Shared by Claude Code and Codex: both set ${CLAUDE_PLUGIN_ROOT}, use the same PreToolUse
14
+ // payload fields and read the same `hookSpecificOutput` envelope.
15
+ //
16
+ // Contract with the harness: print the hook JSON on stdout to act, print nothing to stay
14
17
  // out of the way. Exit code is ALWAYS 0 — a guard that errors out on a user's `git push`
15
18
  // is worse than no guard, so every failure path here is silence.
16
19
  import { spawnSync } from "node:child_process";
@@ -20,8 +23,16 @@ import { fileURLToPath } from "node:url";
20
23
 
21
24
  try {
22
25
  const payload = JSON.parse(readFileSync(0, "utf8"));
23
- if (payload.tool_name !== "Bash") process.exit(0);
24
- const command = payload.tool_input?.command ?? "";
26
+ // Anchored so `BashOutput`/`KillShell` (background-shell polls) never reach the engine,
27
+ // but deliberately loose about WHICH shell: `Bash` on Claude Code, `shell` and friends on
28
+ // Codex. `isShellTool` in src/hook.ts is the precise check; tests/harness-sync.test.ts
29
+ // asserts this regex accepts exactly the same names.
30
+ if (!/^(?:bash|shell|exec|exec_command|local_?shell|run_command)$/i.test(String(payload.tool_name ?? ""))) process.exit(0);
31
+ // Codex passes an argv array. Joining it is WRONG for deciding intent (see `commandOf`)
32
+ // but exactly right here, where the only question is whether the words appear at all —
33
+ // and the payload is forwarded untouched, so the engine re-parses it properly.
34
+ const raw = payload.tool_input?.command;
35
+ const command = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.join(" ") : "";
25
36
  // Prefilter: no mention of git or gh ⇒ nothing to do, and we never touched the engine.
26
37
  if (!/\bgit\b|\bgh\b/.test(command)) process.exit(0);
27
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "2.26.0",
3
+ "version": "2.28.0",
4
4
  "description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -13,6 +13,9 @@
13
13
  "skills",
14
14
  "docker",
15
15
  ".claude-plugin",
16
+ ".codex-plugin",
17
+ ".agents",
18
+ ".opencode",
16
19
  "hooks"
17
20
  ],
18
21
  "engines": {
@@ -75,5 +78,6 @@
75
78
  },
76
79
  "publishConfig": {
77
80
  "access": "public"
78
- }
81
+ },
82
+ "main": ".opencode/plugins/ultra11y.js"
79
83
  }
@@ -41,13 +41,21 @@ edit("src/types.ts", (s) => s.replace(/(export const VERSION = ")[^"]+(";)/, `$1
41
41
  edit("skills/ultra11y/SKILL.md", setVersionField);
42
42
  edit("skills/review-a11y/SKILL.md", setVersionField);
43
43
 
44
- // The Claude Code plugin manifests — what carries the automatic-review hook. A stale
45
- // version here is what makes an installed plugin report the wrong engine; both list it,
46
- // so both are rewritten (verify-skill-bundle.mjs fails the build if they drift).
44
+ // The plugin manifests — what carries the automatic-review hook. A stale version here is
45
+ // what makes an installed plugin report the wrong engine, so every harness's manifest is
46
+ // rewritten (verify-skill-bundle.mjs fails the build if they drift). Codex resolves
47
+ // .codex-plugin/plugin.json first and falls back to .claude-plugin/plugin.json, so the two
48
+ // must agree. .agents/plugins/marketplace.json carries no version by design — it points at
49
+ // the repo, and the plugin manifest inside it is the single source of the number.
47
50
  const setJsonVersion = (s) => s.replace(/("version":\s*")[^"]+(")/, `$1${version}$2`);
48
51
  edit(".claude-plugin/plugin.json", setJsonVersion);
49
52
  edit(".claude-plugin/marketplace.json", setJsonVersion);
53
+ edit(".codex-plugin/plugin.json", setJsonVersion);
54
+
55
+ // The OpenCode plugin is committed JS, not a manifest — its version lives in a constant the
56
+ // installer and `status` read back. verify-skill-bundle.mjs fails the build if it drifts.
57
+ edit(".opencode/plugins/ultra11y.js", (s) => s.replace(/(const ULTRA11Y_PLUGIN_VERSION = ")[^"]+(")/, `$1${version}$2`));
50
58
 
51
59
  console.log(
52
- `sync-version: set ${version} in package.json, src/types.ts, skills/ultra11y/SKILL.md, skills/review-a11y/SKILL.md, .claude-plugin/plugin.json, .claude-plugin/marketplace.json`,
60
+ `sync-version: set ${version} in package.json, src/types.ts, skills/ultra11y/SKILL.md, skills/review-a11y/SKILL.md, .claude-plugin/plugin.json, .claude-plugin/marketplace.json, .codex-plugin/plugin.json`,
53
61
  );