@cloverleaf/reference-impl 0.2.0 → 0.3.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.
- package/README.md +12 -4
- package/VERSION +1 -1
- package/config/affected-routes.json +12 -0
- package/dist/affected-routes.mjs +66 -0
- package/dist/cli.mjs +28 -0
- package/install.sh +7 -0
- package/lib/affected-routes.ts +80 -0
- package/lib/cli.ts +28 -0
- package/package.json +1 -1
- package/prompts/ui-reviewer.md +27 -17
- package/skills/cloverleaf-ui-review.md +43 -23
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npm install # pulls @cloverleaf/standard + deps
|
|
|
15
15
|
./install.sh --project # local install into ./.claude/plugins/cloverleaf/
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
## Scope (v0.
|
|
18
|
+
## Scope (v0.3)
|
|
19
19
|
|
|
20
20
|
v0.2 implements both paths of the Delivery track:
|
|
21
21
|
|
|
@@ -29,7 +29,7 @@ v0.2 implements both paths of the Delivery track:
|
|
|
29
29
|
| Implementer | Real | Subagent, code + tests on feature branch |
|
|
30
30
|
| Documenter | Real (v0.2) | Subagent, doc-only commits per file-path rules |
|
|
31
31
|
| Reviewer | Real | Subagent, read-only review of diff |
|
|
32
|
-
| UI Reviewer | Real (v0.
|
|
32
|
+
| UI Reviewer | Real (v0.3) | Playwright + axe-core, diff-scoped to affected routes, single viewport, a11y only |
|
|
33
33
|
| QA | Real (v0.2) | Per-package test runner via `git worktree` |
|
|
34
34
|
| Plan | Stub | Deferred to v0.3 |
|
|
35
35
|
| Researcher | Stub | Deferred to v0.3 |
|
|
@@ -54,10 +54,18 @@ Two JSON config files in `config/` (overridable per consumer project):
|
|
|
54
54
|
|
|
55
55
|
### Known limitations
|
|
56
56
|
|
|
57
|
-
- Playwright installs ~300MB into each `git worktree` (v0.3 will cache).
|
|
58
57
|
- Concurrent `/cloverleaf-run` on the same repo may race on preview ports.
|
|
59
|
-
- UI Reviewer visual diff + multi-viewport deferred to v0.
|
|
58
|
+
- UI Reviewer visual diff + multi-viewport deferred to v0.4.
|
|
60
59
|
- QA does not produce HTML reports (no `report_uri`).
|
|
60
|
+
- Astro `base` path is parsed best-effort; if misdetected, UI Reviewer may return escalate.
|
|
61
|
+
|
|
62
|
+
### Prerequisites for UI Reviewer
|
|
63
|
+
|
|
64
|
+
Run once per machine:
|
|
65
|
+
|
|
66
|
+
npx playwright install chromium
|
|
67
|
+
|
|
68
|
+
This installs chromium into `~/.cache/ms-playwright/` (the default `PLAYWRIGHT_BROWSERS_PATH`). Subsequent `/cloverleaf-ui-review` invocations reuse this cache — no ~300 MB re-download per run.
|
|
61
69
|
|
|
62
70
|
## Quick start — toy repo
|
|
63
71
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pageRoots": ["site/src/pages/"],
|
|
3
|
+
"globalPatterns": [
|
|
4
|
+
"site/src/layouts/**",
|
|
5
|
+
"site/src/components/**",
|
|
6
|
+
"site/src/styles/**",
|
|
7
|
+
"site/public/**",
|
|
8
|
+
"site/astro.config.*",
|
|
9
|
+
"site/src/content/**"
|
|
10
|
+
],
|
|
11
|
+
"routeScope": ["site/src/**", "site/public/**"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const DEFAULT_CONFIG = join(here, '..', 'config', 'affected-routes.json');
|
|
6
|
+
function globToRegex(pattern) {
|
|
7
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
const regex = escaped
|
|
9
|
+
.replace(/\*\*/g, '\u0000')
|
|
10
|
+
.replace(/\*/g, '[^/]*')
|
|
11
|
+
.replace(/\u0000/g, '.*');
|
|
12
|
+
return new RegExp(`^${regex}$`);
|
|
13
|
+
}
|
|
14
|
+
function matchesAny(file, patterns) {
|
|
15
|
+
return patterns.some((p) => globToRegex(p).test(file));
|
|
16
|
+
}
|
|
17
|
+
function routeForPage(file, pageRoot) {
|
|
18
|
+
if (!file.startsWith(pageRoot))
|
|
19
|
+
return null;
|
|
20
|
+
const rel = file.slice(pageRoot.length);
|
|
21
|
+
const withoutExt = rel.replace(/\.(astro|mdx)$/, '');
|
|
22
|
+
if (!withoutExt || withoutExt === rel)
|
|
23
|
+
return null;
|
|
24
|
+
if (withoutExt === 'index')
|
|
25
|
+
return '/';
|
|
26
|
+
return `/${withoutExt}/`;
|
|
27
|
+
}
|
|
28
|
+
export function loadDefaultConfig() {
|
|
29
|
+
if (!existsSync(DEFAULT_CONFIG)) {
|
|
30
|
+
throw new Error(`affected-routes config not found at ${DEFAULT_CONFIG}`);
|
|
31
|
+
}
|
|
32
|
+
const doc = JSON.parse(readFileSync(DEFAULT_CONFIG, 'utf-8'));
|
|
33
|
+
return {
|
|
34
|
+
pageRoots: Array.isArray(doc.pageRoots) ? doc.pageRoots : [],
|
|
35
|
+
globalPatterns: Array.isArray(doc.globalPatterns) ? doc.globalPatterns : [],
|
|
36
|
+
routeScope: Array.isArray(doc.routeScope) ? doc.routeScope : [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function computeAffectedRoutes(changedFiles, config) {
|
|
40
|
+
const routes = new Set();
|
|
41
|
+
let inScopeButUnmatched = false;
|
|
42
|
+
for (const file of changedFiles) {
|
|
43
|
+
if (matchesAny(file, config.globalPatterns)) {
|
|
44
|
+
return 'all';
|
|
45
|
+
}
|
|
46
|
+
let mapped = null;
|
|
47
|
+
for (const root of config.pageRoots) {
|
|
48
|
+
const r = routeForPage(file, root);
|
|
49
|
+
if (r) {
|
|
50
|
+
mapped = r;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (mapped) {
|
|
55
|
+
routes.add(mapped);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (matchesAny(file, config.routeScope)) {
|
|
59
|
+
inScopeButUnmatched = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (inScopeButUnmatched) {
|
|
63
|
+
return 'all';
|
|
64
|
+
}
|
|
65
|
+
return Array.from(routes).sort();
|
|
66
|
+
}
|
package/dist/cli.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { emitGateDecision } from './events.mjs';
|
|
|
21
21
|
import { writeFeedback, latestFeedback } from './feedback.mjs';
|
|
22
22
|
import { nextTaskId, inferProject } from './ids.mjs';
|
|
23
23
|
import { matchesUiPaths, loadDefaultPatterns } from './ui-paths.mjs';
|
|
24
|
+
import { computeAffectedRoutes, loadDefaultConfig } from './affected-routes.mjs';
|
|
24
25
|
function die(msg, code = 1) {
|
|
25
26
|
process.stderr.write(msg + '\n');
|
|
26
27
|
process.exit(code);
|
|
@@ -181,6 +182,33 @@ try {
|
|
|
181
182
|
process.stdout.write(`${result}\n`);
|
|
182
183
|
process.exit(0);
|
|
183
184
|
}
|
|
185
|
+
case 'affected-routes': {
|
|
186
|
+
const [repoRoot, taskId] = rest;
|
|
187
|
+
if (!repoRoot || !taskId) {
|
|
188
|
+
console.error('usage: affected-routes <repo_root> <task-id>');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const branch = `cloverleaf/${taskId}`;
|
|
192
|
+
let changed;
|
|
193
|
+
try {
|
|
194
|
+
const out = execSync(`git diff --name-only main..${branch}`, {
|
|
195
|
+
cwd: repoRoot,
|
|
196
|
+
encoding: 'utf-8',
|
|
197
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
198
|
+
});
|
|
199
|
+
changed = out.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
const err = e;
|
|
203
|
+
const stderrStr = typeof err.stderr === 'string' ? err.stderr : err.stderr?.toString() ?? '';
|
|
204
|
+
console.error(`branch ${branch} not found: ${stderrStr || err.message || 'unknown'}`);
|
|
205
|
+
process.exit(2);
|
|
206
|
+
}
|
|
207
|
+
const config = loadDefaultConfig();
|
|
208
|
+
const result = computeAffectedRoutes(changed, config);
|
|
209
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
184
212
|
default:
|
|
185
213
|
usage(`Unknown command: ${command}`);
|
|
186
214
|
}
|
package/install.sh
CHANGED
|
@@ -55,3 +55,10 @@ echo "Skills available: $(ls "${INSTALL_ROOT}/skills" | wc -l | tr -d ' ')"
|
|
|
55
55
|
echo ""
|
|
56
56
|
echo "Add ${INSTALL_ROOT}/bin to your PATH if you want to invoke cloverleaf-cli directly,"
|
|
57
57
|
echo "or reference it by absolute path from your skill calls."
|
|
58
|
+
|
|
59
|
+
# Post-install: warn about Playwright chromium if not cached
|
|
60
|
+
if [ ! -d "${HOME}/.cache/ms-playwright" ] || [ -z "$(ls -A "${HOME}/.cache/ms-playwright" 2>/dev/null)" ]; then
|
|
61
|
+
echo ""
|
|
62
|
+
echo "Note: UI Reviewer uses Playwright chromium. If you plan to run /cloverleaf-ui-review, install once with:"
|
|
63
|
+
echo " npx playwright install chromium"
|
|
64
|
+
fi
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DEFAULT_CONFIG = join(here, '..', 'config', 'affected-routes.json');
|
|
7
|
+
|
|
8
|
+
export interface AffectedRoutesConfig {
|
|
9
|
+
pageRoots: string[];
|
|
10
|
+
globalPatterns: string[];
|
|
11
|
+
routeScope: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function globToRegex(pattern: string): RegExp {
|
|
15
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
const regex = escaped
|
|
17
|
+
.replace(/\*\*/g, '\u0000')
|
|
18
|
+
.replace(/\*/g, '[^/]*')
|
|
19
|
+
.replace(/\u0000/g, '.*');
|
|
20
|
+
return new RegExp(`^${regex}$`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchesAny(file: string, patterns: string[]): boolean {
|
|
24
|
+
return patterns.some((p) => globToRegex(p).test(file));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function routeForPage(file: string, pageRoot: string): string | null {
|
|
28
|
+
if (!file.startsWith(pageRoot)) return null;
|
|
29
|
+
const rel = file.slice(pageRoot.length);
|
|
30
|
+
const withoutExt = rel.replace(/\.(astro|mdx)$/, '');
|
|
31
|
+
if (!withoutExt || withoutExt === rel) return null;
|
|
32
|
+
if (withoutExt === 'index') return '/';
|
|
33
|
+
return `/${withoutExt}/`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadDefaultConfig(): AffectedRoutesConfig {
|
|
37
|
+
if (!existsSync(DEFAULT_CONFIG)) {
|
|
38
|
+
throw new Error(`affected-routes config not found at ${DEFAULT_CONFIG}`);
|
|
39
|
+
}
|
|
40
|
+
const doc = JSON.parse(readFileSync(DEFAULT_CONFIG, 'utf-8')) as Partial<AffectedRoutesConfig>;
|
|
41
|
+
return {
|
|
42
|
+
pageRoots: Array.isArray(doc.pageRoots) ? doc.pageRoots : [],
|
|
43
|
+
globalPatterns: Array.isArray(doc.globalPatterns) ? doc.globalPatterns : [],
|
|
44
|
+
routeScope: Array.isArray(doc.routeScope) ? doc.routeScope : [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function computeAffectedRoutes(
|
|
49
|
+
changedFiles: string[],
|
|
50
|
+
config: AffectedRoutesConfig
|
|
51
|
+
): string[] | 'all' {
|
|
52
|
+
const routes = new Set<string>();
|
|
53
|
+
let inScopeButUnmatched = false;
|
|
54
|
+
|
|
55
|
+
for (const file of changedFiles) {
|
|
56
|
+
if (matchesAny(file, config.globalPatterns)) {
|
|
57
|
+
return 'all';
|
|
58
|
+
}
|
|
59
|
+
let mapped: string | null = null;
|
|
60
|
+
for (const root of config.pageRoots) {
|
|
61
|
+
const r = routeForPage(file, root);
|
|
62
|
+
if (r) {
|
|
63
|
+
mapped = r;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (mapped) {
|
|
68
|
+
routes.add(mapped);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (matchesAny(file, config.routeScope)) {
|
|
72
|
+
inScopeButUnmatched = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (inScopeButUnmatched) {
|
|
77
|
+
return 'all';
|
|
78
|
+
}
|
|
79
|
+
return Array.from(routes).sort();
|
|
80
|
+
}
|
package/lib/cli.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { emitGateDecision } from './events.js';
|
|
|
22
22
|
import { writeFeedback, latestFeedback } from './feedback.js';
|
|
23
23
|
import { nextTaskId, inferProject } from './ids.js';
|
|
24
24
|
import { matchesUiPaths, loadDefaultPatterns } from './ui-paths.js';
|
|
25
|
+
import { computeAffectedRoutes, loadDefaultConfig } from './affected-routes.js';
|
|
25
26
|
import type { FeedbackEnvelope } from './feedback.js';
|
|
26
27
|
|
|
27
28
|
function die(msg: string, code = 1): never {
|
|
@@ -191,6 +192,33 @@ try {
|
|
|
191
192
|
process.exit(0);
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
case 'affected-routes': {
|
|
196
|
+
const [repoRoot, taskId] = rest;
|
|
197
|
+
if (!repoRoot || !taskId) {
|
|
198
|
+
console.error('usage: affected-routes <repo_root> <task-id>');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
const branch = `cloverleaf/${taskId}`;
|
|
202
|
+
let changed: string[];
|
|
203
|
+
try {
|
|
204
|
+
const out = execSync(`git diff --name-only main..${branch}`, {
|
|
205
|
+
cwd: repoRoot,
|
|
206
|
+
encoding: 'utf-8',
|
|
207
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
208
|
+
});
|
|
209
|
+
changed = out.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
210
|
+
} catch (e: unknown) {
|
|
211
|
+
const err = e as { stderr?: Buffer | string; message?: string };
|
|
212
|
+
const stderrStr = typeof err.stderr === 'string' ? err.stderr : err.stderr?.toString() ?? '';
|
|
213
|
+
console.error(`branch ${branch} not found: ${stderrStr || err.message || 'unknown'}`);
|
|
214
|
+
process.exit(2);
|
|
215
|
+
}
|
|
216
|
+
const config = loadDefaultConfig();
|
|
217
|
+
const result = computeAffectedRoutes(changed, config);
|
|
218
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
194
222
|
default:
|
|
195
223
|
usage(`Unknown command: ${command}`);
|
|
196
224
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloverleaf/reference-impl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Reference implementation of the Cloverleaf methodology as Claude Code skills. Implements the Tight Loop (Implementer + Reviewer).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/prompts/ui-reviewer.md
CHANGED
|
@@ -10,23 +10,33 @@ You are the Cloverleaf UI Reviewer. Your job: review a task's UI changes for acc
|
|
|
10
10
|
- **Repo root**: {{repo_root}}
|
|
11
11
|
- **Diff from base**: {{diff}}
|
|
12
12
|
- **Preview port**: {{preview_port}} (an already-allocated free local port; use it for the dev server)
|
|
13
|
+
- **Affected routes**: {{affected_routes}} — either a JSON array of route paths (e.g., `["/faq/"]`), or the string `"all"`, or `[]`
|
|
13
14
|
|
|
14
|
-
## Scope (v0.
|
|
15
|
+
## Scope (v0.3)
|
|
15
16
|
|
|
16
17
|
- Accessibility only (axe-core). No visual diff, no responsive checks.
|
|
17
18
|
- Single viewport: 1280×800.
|
|
18
|
-
-
|
|
19
|
-
-
|
|
19
|
+
- Run axe ONLY on the pages listed in `{{affected_routes}}`.
|
|
20
|
+
- If `{{affected_routes}}` is `"all"`: crawl up to 20 pages reachable from `/` via same-origin link discovery (v0.2 fallback behavior).
|
|
21
|
+
- If `{{affected_routes}}` is `[]`: return `verdict: "pass"` with summary "No renderable routes affected, skipping axe." Do NOT start the preview server.
|
|
22
|
+
- Otherwise: visit exactly the URLs listed. No link-discovery crawl.
|
|
23
|
+
- Visual diff, viewports loop, and `visual_diff_uri` are deferred to v0.4.
|
|
24
|
+
|
|
25
|
+
## Playwright cache
|
|
26
|
+
|
|
27
|
+
The `PLAYWRIGHT_BROWSERS_PATH` environment variable is set to `~/.cache/ms-playwright` before you are invoked. Playwright resolves chromium from this shared cache, so `npm ci` in the worktree does NOT re-download ~300 MB of browser binaries. If the browser is missing, return `verdict: "escalate"` with a synthetic finding: `"Playwright chromium not installed. Run 'npx playwright install chromium' on this machine."`
|
|
20
28
|
|
|
21
29
|
## Runtime procedure
|
|
22
30
|
|
|
23
|
-
1.
|
|
31
|
+
1. If `{{affected_routes}}` is `[]`, return immediately (pass-skip) — no worktree, no server, no browser.
|
|
32
|
+
|
|
33
|
+
2. Set up an isolated worktree of the feature branch:
|
|
24
34
|
```bash
|
|
25
35
|
TMPDIR=$(mktemp -d)
|
|
26
36
|
git worktree add "$TMPDIR" {{branch}}
|
|
27
37
|
```
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
3. For this repo, UI lives in `site/`. Install dependencies and start the dev server:
|
|
30
40
|
```bash
|
|
31
41
|
cd "$TMPDIR/site"
|
|
32
42
|
npm ci
|
|
@@ -34,32 +44,32 @@ You are the Cloverleaf UI Reviewer. Your job: review a task's UI changes for acc
|
|
|
34
44
|
SERVER_PID=$!
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
4. Wait up to 30s for `http://localhost:{{preview_port}}/` to respond 200. If the server fails to start in 30s, kill it and return verdict `escalate`.
|
|
48
|
+
|
|
49
|
+
5. Determine the site base path: read `astro.config.*` in the worktree for a `base: '<path>'` entry. Default to empty string if not found or unparseable.
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
- On each page, inject and run `axe-core`:
|
|
51
|
+
6. For each route in `{{affected_routes}}` (or the crawl set, if `"all"`):
|
|
52
|
+
- Construct URL `http://localhost:{{preview_port}}<base><route>`.
|
|
53
|
+
- Navigate. If 404, retry at `http://localhost:{{preview_port}}<route>` (without base).
|
|
54
|
+
- Inject and run axe-core:
|
|
44
55
|
```javascript
|
|
45
56
|
import axe from 'axe-core';
|
|
46
57
|
const results = await axe.run(document);
|
|
47
58
|
```
|
|
48
|
-
- Collect
|
|
59
|
+
- Collect violations.
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
7. Map violations to findings:
|
|
51
62
|
- axe `impact: "critical"` → `severity: "blocker"`
|
|
52
63
|
- axe `impact: "serious"` → `severity: "error"`
|
|
53
64
|
- axe `impact: "moderate"` → `severity: "warning"`
|
|
54
65
|
- axe `impact: "minor"` → `severity: "info"`
|
|
55
|
-
- Each finding: `{severity, rule: "a11y.<wcag-id-or-rule-id>", message: <axe description>, location: <page url>}`
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
8. Compute verdict:
|
|
58
68
|
- `pass` — zero findings with severity `blocker` or `error`
|
|
59
69
|
- `bounce` — ≥1 finding with severity `blocker` or `error`
|
|
60
|
-
- `escalate` — preview server failed to start, OR axe threw ≥3 consecutive times
|
|
70
|
+
- `escalate` — preview server failed to start, OR axe threw ≥3 consecutive times, OR Playwright chromium missing.
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
9. Teardown:
|
|
63
73
|
```bash
|
|
64
74
|
kill $SERVER_PID 2>/dev/null || true
|
|
65
75
|
cd {{repo_root}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloverleaf-ui-review
|
|
3
|
-
description: Run the UI Reviewer agent on a task in the `ui-review` state (full pipeline only).
|
|
3
|
+
description: Run the UI Reviewer agent on a task in the `ui-review` state (full pipeline only). Computes diff-affected routes via CLI; if empty, skips axe and advances ui-review → qa. Otherwise dispatches a subagent with Playwright + axe-core scoped to those routes. Usage — /cloverleaf-ui-review <TASK-ID>.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cloverleaf — ui-review
|
|
@@ -27,47 +27,67 @@ description: Run the UI Reviewer agent on a task in the `ui-review` state (full
|
|
|
27
27
|
|
|
28
28
|
3. Confirm feature branch exists: `git rev-parse --verify cloverleaf/<TASK-ID>`. If missing, report and stop.
|
|
29
29
|
|
|
30
|
-
4.
|
|
30
|
+
4. Compute affected routes:
|
|
31
|
+
```bash
|
|
32
|
+
AFFECTED=$(~/.claude/plugins/cloverleaf/bin/cloverleaf-cli affected-routes <repo_root> <TASK-ID>)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
5. **Empty-set early-exit.** If `AFFECTED` is `[]`, skip the subagent entirely:
|
|
36
|
+
```bash
|
|
37
|
+
cloverleaf-cli advance-status <repo_root> <TASK-ID> qa agent '' full_pipeline
|
|
38
|
+
cd <repo_root>
|
|
39
|
+
git add .cloverleaf/
|
|
40
|
+
git commit -m "cloverleaf: <TASK-ID> ui-review skipped (no renderable routes) → qa"
|
|
41
|
+
```
|
|
42
|
+
Report: "✓ UI Review skipped (no renderable routes affected). State → qa. Next: `/cloverleaf-qa <TASK-ID>`."
|
|
43
|
+
Stop here.
|
|
44
|
+
|
|
45
|
+
6. Allocate a free preview port:
|
|
31
46
|
```bash
|
|
32
47
|
PREVIEW_PORT=$(node -e "const net=require('net');const s=net.createServer();s.listen(0,()=>{console.log(s.address().port);s.close()})")
|
|
33
48
|
```
|
|
34
49
|
|
|
35
|
-
|
|
50
|
+
7. Compute diff:
|
|
36
51
|
```bash
|
|
37
52
|
git diff main..cloverleaf/<TASK-ID>
|
|
38
53
|
```
|
|
39
54
|
|
|
40
|
-
|
|
55
|
+
8. **Browser cache env var.** Before the Task-tool dispatch, ensure `PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright` is exported so the subagent inherits it. This keeps Playwright from re-downloading ~300 MB of browser binaries inside the worktree.
|
|
56
|
+
|
|
57
|
+
9. Dispatch the UI Reviewer subagent via the Task tool:
|
|
41
58
|
- `subagent_type`: `general-purpose`
|
|
42
59
|
- `model`: `sonnet`
|
|
43
|
-
- Prompt: contents of `~/.claude/plugins/cloverleaf/prompts/ui-reviewer.md` with substitutions
|
|
60
|
+
- Prompt: contents of `~/.claude/plugins/cloverleaf/prompts/ui-reviewer.md` with substitutions:
|
|
61
|
+
- `{{task}}`, `{{diff}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{preview_port}}`
|
|
62
|
+
- `{{affected_routes}}` → the value of `$AFFECTED` (verbatim — may be `"all"`, a JSON array, or `[]` but step 5 handled `[]` already)
|
|
44
63
|
|
|
45
|
-
|
|
64
|
+
10. Parse the subagent's response. Expect `{"verdict": "pass"|"bounce"|"escalate", "summary": "...", "findings": [...]}`.
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
11. Branch on verdict:
|
|
48
67
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
**Pass:**
|
|
69
|
+
```
|
|
70
|
+
cloverleaf-cli advance-status <repo_root> <TASK-ID> qa agent '' full_pipeline
|
|
71
|
+
```
|
|
72
|
+
Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> ui-review passed → qa"`.
|
|
73
|
+
Report: "✓ UI Review passed. State → qa. Next: `/cloverleaf-qa <TASK-ID>`."
|
|
55
74
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
**Bounce:**
|
|
76
|
+
1. Write feedback: `echo '<envelope-json>' > /tmp/cloverleaf-fb-u.json`
|
|
77
|
+
2. `cloverleaf-cli write-feedback <repo_root> <TASK-ID> /tmp/cloverleaf-fb-u.json --prefix=u`
|
|
78
|
+
3. `cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent '' full_pipeline`
|
|
79
|
+
4. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> ui-review bounced → implementing"`.
|
|
80
|
+
5. Report: "✗ UI Review bounced. Findings: <summary by severity>. State → implementing. Next: `/cloverleaf-implement <TASK-ID>`."
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
**Escalate:**
|
|
83
|
+
1. `cloverleaf-cli advance-status <repo_root> <TASK-ID> escalated agent`
|
|
84
|
+
2. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> ui-review escalated"`.
|
|
85
|
+
3. Report: "✗ UI Review escalated (infrastructure issue). Review and retry manually."
|
|
67
86
|
|
|
68
87
|
## Rules
|
|
69
88
|
|
|
70
89
|
- Never push.
|
|
71
90
|
- Do not modify source code — UI Reviewer is read-only.
|
|
72
91
|
- Always teardown preview server + worktree on error.
|
|
92
|
+
- Empty-set early-exit (step 5) skips the browser entirely — no Playwright invocation, no worktree.
|
|
73
93
|
- On illegal state transition, report and stop without partial commits.
|