@cloverleaf/reference-impl 0.4.0 → 0.4.1
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/.claude-plugin/plugin.json +1 -1
- package/VERSION +1 -1
- package/config/ui-review.json +2 -1
- package/dist/axe-dedupe.mjs +6 -2
- package/dist/cli.mjs +8 -1
- package/dist/feedback.mjs +1 -1
- package/dist/plugin-path.mjs +19 -0
- package/dist/ui-review-config.mjs +5 -1
- package/lib/axe-dedupe.ts +13 -2
- package/lib/cli.ts +9 -1
- package/lib/feedback.ts +1 -1
- package/lib/plugin-path.ts +21 -0
- package/lib/ui-review-config.ts +6 -1
- package/package.json +1 -1
- package/prompts/ui-reviewer.md +19 -6
- package/skills/cloverleaf-document/SKILL.md +2 -2
- package/skills/cloverleaf-implement/SKILL.md +3 -3
- package/skills/cloverleaf-merge/SKILL.md +26 -5
- package/skills/cloverleaf-new-task/SKILL.md +2 -2
- package/skills/cloverleaf-qa/SKILL.md +16 -6
- package/skills/cloverleaf-review/SKILL.md +18 -8
- package/skills/cloverleaf-ui-review/SKILL.md +17 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloverleaf",
|
|
3
3
|
"description": "Cloverleaf reference implementation — Claude Code skills for task scaffolding and the Delivery pipeline (implementer, documenter, reviewer, UI reviewer with multi-viewport visual diff, QA, merge).",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Renato D'Arrigo",
|
|
7
7
|
"email": "renato.darrigo@gmail.com"
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.4.
|
|
1
|
+
0.4.1
|
package/config/ui-review.json
CHANGED
package/dist/axe-dedupe.mjs
CHANGED
|
@@ -7,9 +7,13 @@ const SEVERITY_MAP = {
|
|
|
7
7
|
function dedupeKeyOf(raw, keys) {
|
|
8
8
|
return keys.map((k) => raw[k]).join('||');
|
|
9
9
|
}
|
|
10
|
-
export function dedupeAxeFindings(raws, keys) {
|
|
10
|
+
export function dedupeAxeFindings(raws, keys, ignored = []) {
|
|
11
|
+
// Filter out ignored (ruleId, target) tuples BEFORE grouping.
|
|
12
|
+
const filtered = raws.filter((raw) => {
|
|
13
|
+
return !ignored.some((i) => i.ruleId === raw.ruleId && i.target === raw.target);
|
|
14
|
+
});
|
|
11
15
|
const groups = new Map();
|
|
12
|
-
for (const raw of
|
|
16
|
+
for (const raw of filtered) {
|
|
13
17
|
const key = dedupeKeyOf(raw, keys);
|
|
14
18
|
const existing = groups.get(key);
|
|
15
19
|
if (existing) {
|
package/dist/cli.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* latest-feedback <repoRoot> <taskId>
|
|
14
14
|
* emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]
|
|
15
15
|
* ui-review-config --repo-root <repoRoot>
|
|
16
|
+
* plugin-root
|
|
16
17
|
*/
|
|
17
18
|
import { readFileSync } from 'node:fs';
|
|
18
19
|
import { execSync } from 'node:child_process';
|
|
@@ -26,6 +27,7 @@ import { loadUiPathsConfig } from './ui-paths.mjs';
|
|
|
26
27
|
import { computeAffectedRoutes } from './affected-routes.mjs';
|
|
27
28
|
import { loadAffectedRoutesConfig } from './affected-routes.mjs';
|
|
28
29
|
import { loadUiReviewConfig } from './ui-review-config.mjs';
|
|
30
|
+
import { getPluginRoot } from './plugin-path.mjs';
|
|
29
31
|
function die(msg, code = 1) {
|
|
30
32
|
process.stderr.write(msg + '\n');
|
|
31
33
|
process.exit(code);
|
|
@@ -42,7 +44,8 @@ function usage(msg) {
|
|
|
42
44
|
' write-feedback <repoRoot> <taskId> <envelopeJsonPath>\n' +
|
|
43
45
|
' latest-feedback <repoRoot> <taskId>\n' +
|
|
44
46
|
' emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]\n' +
|
|
45
|
-
' ui-review-config --repo-root <repoRoot>\n'
|
|
47
|
+
' ui-review-config --repo-root <repoRoot>\n' +
|
|
48
|
+
' plugin-root\n');
|
|
46
49
|
process.exit(2);
|
|
47
50
|
}
|
|
48
51
|
const [, , command, ...rest] = process.argv;
|
|
@@ -235,6 +238,10 @@ try {
|
|
|
235
238
|
process.stdout.write(JSON.stringify(config, null, 2));
|
|
236
239
|
process.exit(0);
|
|
237
240
|
}
|
|
241
|
+
case 'plugin-root': {
|
|
242
|
+
process.stdout.write(getPluginRoot());
|
|
243
|
+
process.exit(0);
|
|
244
|
+
}
|
|
238
245
|
default:
|
|
239
246
|
usage(`Unknown command: ${command}`);
|
|
240
247
|
}
|
package/dist/feedback.mjs
CHANGED
|
@@ -29,7 +29,7 @@ export function allFeedback(repoRoot, taskId) {
|
|
|
29
29
|
const dir = feedbackDir(repoRoot);
|
|
30
30
|
if (!existsSync(dir))
|
|
31
31
|
return [];
|
|
32
|
-
const re = new RegExp(`^${escapeRegex(taskId)}-
|
|
32
|
+
const re = new RegExp(`^${escapeRegex(taskId)}-[ruq](\\d+)\\.json$`);
|
|
33
33
|
const entries = readdirSync(dir)
|
|
34
34
|
.map((f) => ({ f, m: f.match(re) }))
|
|
35
35
|
.filter((x) => !!x.m)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
4
|
+
/**
|
|
5
|
+
* Absolute path to the plugin root.
|
|
6
|
+
*
|
|
7
|
+
* At runtime, this module lives at <plugin-root>/lib/plugin-path.js (or .ts in dev),
|
|
8
|
+
* so the plugin root is the parent directory.
|
|
9
|
+
*
|
|
10
|
+
* Works under:
|
|
11
|
+
* - dev mode (repo source: <repo>/reference-impl/)
|
|
12
|
+
* - npm install (node_modules/@cloverleaf/reference-impl/)
|
|
13
|
+
* - claude plugin install cache (~/.claude/plugins/cache/cloverleaf-local/cloverleaf/0.4.1/)
|
|
14
|
+
* - legacy symlink into ~/.claude/plugins/cloverleaf/
|
|
15
|
+
* - claude --plugin-dir <path>
|
|
16
|
+
*/
|
|
17
|
+
export function getPluginRoot() {
|
|
18
|
+
return resolve(here, '..');
|
|
19
|
+
}
|
|
@@ -10,11 +10,15 @@ const HARDCODED_FALLBACK = {
|
|
|
10
10
|
desktop: { width: 1280, height: 800 },
|
|
11
11
|
},
|
|
12
12
|
visualDiff: { enabled: true, threshold: 0.1, maxDiffRatio: 0.01, mask: [] },
|
|
13
|
-
axe: { viewports: ['desktop'], dedupeBy: ['ruleId', 'target'] },
|
|
13
|
+
axe: { viewports: ['desktop'], dedupeBy: ['ruleId', 'target'], ignored: [] },
|
|
14
14
|
};
|
|
15
15
|
function readAsConfig(path) {
|
|
16
16
|
try {
|
|
17
17
|
const doc = JSON.parse(readFileSync(path, 'utf-8'));
|
|
18
|
+
// Back-compat: if ignored is missing from an older override, default it.
|
|
19
|
+
if (doc.axe && !('ignored' in doc.axe)) {
|
|
20
|
+
doc.axe.ignored = [];
|
|
21
|
+
}
|
|
18
22
|
return doc;
|
|
19
23
|
}
|
|
20
24
|
catch {
|
package/lib/axe-dedupe.ts
CHANGED
|
@@ -24,9 +24,20 @@ function dedupeKeyOf(raw: RawAxeFinding, keys: DedupeKey[]): string {
|
|
|
24
24
|
return keys.map((k) => raw[k]).join('||');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function dedupeAxeFindings(
|
|
27
|
+
export function dedupeAxeFindings(
|
|
28
|
+
raws: RawAxeFinding[],
|
|
29
|
+
keys: DedupeKey[],
|
|
30
|
+
ignored: Array<{ ruleId: string; target: string }> = []
|
|
31
|
+
): Finding[] {
|
|
32
|
+
// Filter out ignored (ruleId, target) tuples BEFORE grouping.
|
|
33
|
+
const filtered = raws.filter((raw) => {
|
|
34
|
+
return !ignored.some(
|
|
35
|
+
(i) => i.ruleId === raw.ruleId && i.target === raw.target
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
28
39
|
const groups = new Map<string, { first: RawAxeFinding; viewports: string[] }>();
|
|
29
|
-
for (const raw of
|
|
40
|
+
for (const raw of filtered) {
|
|
30
41
|
const key = dedupeKeyOf(raw, keys);
|
|
31
42
|
const existing = groups.get(key);
|
|
32
43
|
if (existing) {
|
package/lib/cli.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* latest-feedback <repoRoot> <taskId>
|
|
14
14
|
* emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]
|
|
15
15
|
* ui-review-config --repo-root <repoRoot>
|
|
16
|
+
* plugin-root
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { readFileSync } from 'node:fs';
|
|
@@ -27,6 +28,7 @@ import { loadUiPathsConfig } from './ui-paths.js';
|
|
|
27
28
|
import { computeAffectedRoutes } from './affected-routes.js';
|
|
28
29
|
import { loadAffectedRoutesConfig } from './affected-routes.js';
|
|
29
30
|
import { loadUiReviewConfig } from './ui-review-config.js';
|
|
31
|
+
import { getPluginRoot } from './plugin-path.js';
|
|
30
32
|
import type { FeedbackEnvelope } from './feedback.js';
|
|
31
33
|
|
|
32
34
|
function die(msg: string, code = 1): never {
|
|
@@ -46,7 +48,8 @@ function usage(msg?: string): never {
|
|
|
46
48
|
' write-feedback <repoRoot> <taskId> <envelopeJsonPath>\n' +
|
|
47
49
|
' latest-feedback <repoRoot> <taskId>\n' +
|
|
48
50
|
' emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]\n' +
|
|
49
|
-
' ui-review-config --repo-root <repoRoot>\n'
|
|
51
|
+
' ui-review-config --repo-root <repoRoot>\n' +
|
|
52
|
+
' plugin-root\n'
|
|
50
53
|
);
|
|
51
54
|
process.exit(2);
|
|
52
55
|
}
|
|
@@ -244,6 +247,11 @@ try {
|
|
|
244
247
|
process.exit(0);
|
|
245
248
|
}
|
|
246
249
|
|
|
250
|
+
case 'plugin-root': {
|
|
251
|
+
process.stdout.write(getPluginRoot());
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
|
|
247
255
|
default:
|
|
248
256
|
usage(`Unknown command: ${command}`);
|
|
249
257
|
}
|
package/lib/feedback.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function latestFeedback(repoRoot: string, taskId: string): FeedbackEnvelo
|
|
|
70
70
|
export function allFeedback(repoRoot: string, taskId: string): FeedbackEnvelope[] {
|
|
71
71
|
const dir = feedbackDir(repoRoot);
|
|
72
72
|
if (!existsSync(dir)) return [];
|
|
73
|
-
const re = new RegExp(`^${escapeRegex(taskId)}-
|
|
73
|
+
const re = new RegExp(`^${escapeRegex(taskId)}-[ruq](\\d+)\\.json$`);
|
|
74
74
|
const entries = readdirSync(dir)
|
|
75
75
|
.map((f) => ({ f, m: f.match(re) }))
|
|
76
76
|
.filter((x): x is { f: string; m: RegExpMatchArray } => !!x.m)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Absolute path to the plugin root.
|
|
8
|
+
*
|
|
9
|
+
* At runtime, this module lives at <plugin-root>/lib/plugin-path.js (or .ts in dev),
|
|
10
|
+
* so the plugin root is the parent directory.
|
|
11
|
+
*
|
|
12
|
+
* Works under:
|
|
13
|
+
* - dev mode (repo source: <repo>/reference-impl/)
|
|
14
|
+
* - npm install (node_modules/@cloverleaf/reference-impl/)
|
|
15
|
+
* - claude plugin install cache (~/.claude/plugins/cache/cloverleaf-local/cloverleaf/0.4.1/)
|
|
16
|
+
* - legacy symlink into ~/.claude/plugins/cloverleaf/
|
|
17
|
+
* - claude --plugin-dir <path>
|
|
18
|
+
*/
|
|
19
|
+
export function getPluginRoot(): string {
|
|
20
|
+
return resolve(here, '..');
|
|
21
|
+
}
|
package/lib/ui-review-config.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface UiReviewConfig {
|
|
|
21
21
|
axe: {
|
|
22
22
|
viewports: string[];
|
|
23
23
|
dedupeBy: ('ruleId' | 'target')[];
|
|
24
|
+
ignored: Array<{ ruleId: string; target: string }>;
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -31,12 +32,16 @@ const HARDCODED_FALLBACK: UiReviewConfig = {
|
|
|
31
32
|
desktop: { width: 1280, height: 800 },
|
|
32
33
|
},
|
|
33
34
|
visualDiff: { enabled: true, threshold: 0.1, maxDiffRatio: 0.01, mask: [] },
|
|
34
|
-
axe: { viewports: ['desktop'], dedupeBy: ['ruleId', 'target'] },
|
|
35
|
+
axe: { viewports: ['desktop'], dedupeBy: ['ruleId', 'target'], ignored: [] },
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
function readAsConfig(path: string): UiReviewConfig | null {
|
|
38
39
|
try {
|
|
39
40
|
const doc = JSON.parse(readFileSync(path, 'utf-8')) as UiReviewConfig;
|
|
41
|
+
// Back-compat: if ignored is missing from an older override, default it.
|
|
42
|
+
if (doc.axe && !('ignored' in doc.axe)) {
|
|
43
|
+
(doc.axe as UiReviewConfig['axe']).ignored = [];
|
|
44
|
+
}
|
|
40
45
|
return doc;
|
|
41
46
|
} catch {
|
|
42
47
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloverleaf/reference-impl",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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
|
@@ -13,9 +13,21 @@ You are the Cloverleaf UI Reviewer. Your job: review a task's UI changes at mult
|
|
|
13
13
|
- **Affected routes**: {{affected_routes}} — either a JSON array of route paths (e.g., `["/faq/"]`), or the string `"all"`, or `[]`
|
|
14
14
|
- **UI review config**: {{ui_review_config}} — the loaded `UiReviewConfig` object (viewports, visualDiff, axe) as JSON. The `viewports` array contains named entries such as `mobile`, `tablet`, and `desktop` with their respective `{ width, height }` dimensions.
|
|
15
15
|
|
|
16
|
+
## Paths
|
|
17
|
+
|
|
18
|
+
You operate in two filesystem locations — keep them straight:
|
|
19
|
+
|
|
20
|
+
- `<worktree>` — the ephemeral worktree at `$TMPDIR` (set up in step 2 of the Runtime procedure). You run the dev server here and execute Playwright here.
|
|
21
|
+
- `<repoRoot>` — the main repository root at `{{repo_root}}` (always an absolute path). This is the ONLY location where baselines, diff PNGs, candidate PNGs, and artifacts are written.
|
|
22
|
+
|
|
23
|
+
**All `compareVisual` paths MUST be rooted at `{{repo_root}}`, NOT at `$TMPDIR`.**
|
|
24
|
+
|
|
25
|
+
The rationale: baselines on `{{repo_root}}/.cloverleaf/baselines/` get picked up by subsequent `git add` + `git commit` steps in the UI Reviewer, which run on the feature branch. The merge skill (v0.4.1+) then merges those commits to main via `git merge --no-ff`. Writing to the worktree's `.cloverleaf/` would strand the files and `git worktree remove --force` would discard them on teardown.
|
|
26
|
+
|
|
16
27
|
## Scope (v0.4)
|
|
17
28
|
|
|
18
29
|
- **Accessibility (axe-core):** run at the viewports listed in `{{ui_review_config}}.axe.viewports`.
|
|
30
|
+
Apply the allowlist in `{{ui_review_config}}.axe.ignored` to drop pre-existing violations that the consumer has accepted (e.g., a11y debt being tracked separately).
|
|
19
31
|
Dedupe findings across viewports by the `{{ui_review_config}}.axe.dedupeBy` composite key (default `["ruleId", "target"]`).
|
|
20
32
|
Emit one finding per (ruleId, target) pair, with a `metadata.viewports` array aggregating the viewports where the violation was detected.
|
|
21
33
|
- **Visual diff (pixelmatch):** when `{{ui_review_config}}.visualDiff.enabled` is true, screenshot each route at each viewport in `{{ui_review_config}}.viewports`, compare to `.cloverleaf/baselines/{route-slug}-{viewport}.png`, emit `severity: "info"` findings with baseline/candidate/diff attachments when the diff ratio exceeds `maxDiffRatio`.
|
|
@@ -50,7 +62,7 @@ The `PLAYWRIGHT_BROWSERS_PATH` environment variable is set to `~/.cache/ms-playw
|
|
|
50
62
|
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`.
|
|
51
63
|
|
|
52
64
|
5. Determine the site base path:
|
|
53
|
-
1. Check
|
|
65
|
+
1. Check `{{repo_root}}/.cloverleaf/config/astro-base.json`. Expected shape: `{ "base": "<path>" }`. If present, use the `base` field verbatim and skip to step 6. (Consumer override — checked before parsing astro config.)
|
|
54
66
|
2. Otherwise, attempt to locate and parse an astro config file (common locations: `site/astro.config.mjs`, `astro.config.mjs` at repo root, `apps/web/astro.config.mjs`). Best-effort fallback.
|
|
55
67
|
3. If both fail, treat base as empty string.
|
|
56
68
|
|
|
@@ -61,11 +73,12 @@ The `PLAYWRIGHT_BROWSERS_PATH` environment variable is set to `~/.cache/ms-playw
|
|
|
61
73
|
- Navigate to `http://localhost:{{preview_port}}<base><route>`. If 404, retry without the base.
|
|
62
74
|
- `page.screenshot({ fullPage: false })` → candidate PNG buffer.
|
|
63
75
|
- Compute slug for the route (lowercase, strip leading/trailing slashes, replace slashes with hyphens; `/` → `index`).
|
|
76
|
+
- Note: use `{{repo_root}}` (the absolute main-repo path), NOT `$TMPDIR` or the worktree. See the "Paths" section.
|
|
64
77
|
- Call `compareVisual` (from `lib/visual-diff.ts`) with:
|
|
65
|
-
- `baselinePath =
|
|
78
|
+
- `baselinePath = {{repo_root}}/.cloverleaf/baselines/{slug}-{viewport}.png`
|
|
66
79
|
- `candidateBuf = <candidate PNG>`
|
|
67
|
-
- `diffPath =
|
|
68
|
-
- `candidateOutPath =
|
|
80
|
+
- `diffPath = {{repo_root}}/.cloverleaf/runs/{taskId}/ui-review/diff-{slug}-{viewport}.png`
|
|
81
|
+
- `candidateOutPath = {{repo_root}}/.cloverleaf/runs/{taskId}/ui-review/candidate-{slug}-{viewport}.png`
|
|
69
82
|
- `threshold = visualDiff.threshold`
|
|
70
83
|
- `maxDiffRatio = visualDiff.maxDiffRatio`
|
|
71
84
|
- Map result to a finding:
|
|
@@ -86,7 +99,7 @@ The `PLAYWRIGHT_BROWSERS_PATH` environment variable is set to `~/.cache/ms-playw
|
|
|
86
99
|
```
|
|
87
100
|
- Collect each violation as a raw tuple: `{ viewport, ruleId, target, impact, message, helpUrl }` (from `axe.run` output).
|
|
88
101
|
|
|
89
|
-
8. Dedupe raw axe findings via `dedupeAxeFindings(raws, {{ui_review_config}}.axe.dedupeBy)` (from `lib/axe-dedupe.ts`). Emit the returned `Finding[]`.
|
|
102
|
+
8. Dedupe raw axe findings via `dedupeAxeFindings(raws, {{ui_review_config}}.axe.dedupeBy, {{ui_review_config}}.axe.ignored)` (from `lib/axe-dedupe.ts`). The `ignored` parameter drops any finding whose `(ruleId, target)` exactly matches an allowlist entry BEFORE dedupe/grouping. Emit the returned `Finding[]`.
|
|
90
103
|
|
|
91
104
|
9. Severity mapping (preserved from v0.3 via `dedupeAxeFindings`):
|
|
92
105
|
- axe `impact: "critical"` → `severity: "blocker"`
|
|
@@ -109,7 +122,7 @@ The `PLAYWRIGHT_BROWSERS_PATH` environment variable is set to `~/.cache/ms-playw
|
|
|
109
122
|
## Tool constraints
|
|
110
123
|
|
|
111
124
|
- Read-only for source files and tests.
|
|
112
|
-
- You MAY write under
|
|
125
|
+
- You MAY write under `{{repo_root}}/.cloverleaf/baselines/` and `{{repo_root}}/.cloverleaf/runs/{taskId}/ui-review/` on the feature branch — these are the baselines and artifacts.
|
|
113
126
|
- Use `git worktree`: do NOT `git checkout` in the main working directory.
|
|
114
127
|
- Always teardown the server and worktree, even on error.
|
|
115
128
|
|
|
@@ -21,7 +21,7 @@ description: Run the Documenter agent on a task in the `implementing` state (ful
|
|
|
21
21
|
|
|
22
22
|
2. Load the task:
|
|
23
23
|
```
|
|
24
|
-
|
|
24
|
+
cloverleaf-cli load-task <repo_root> <TASK-ID>
|
|
25
25
|
```
|
|
26
26
|
Verify `status === "implementing"`. Verify `risk_class === "high"`. If either check fails, report and stop.
|
|
27
27
|
|
|
@@ -36,7 +36,7 @@ description: Run the Documenter agent on a task in the `implementing` state (ful
|
|
|
36
36
|
5. Dispatch the Documenter subagent via the Task tool:
|
|
37
37
|
- `subagent_type`: `general-purpose`
|
|
38
38
|
- `model`: `sonnet`
|
|
39
|
-
- Prompt: contents of
|
|
39
|
+
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/documenter.md` with substitutions:
|
|
40
40
|
- `{{task}}` → full task JSON (pretty-printed)
|
|
41
41
|
- `{{diff}}` → diff output
|
|
42
42
|
- `{{branch}}` → `cloverleaf/<TASK-ID>`
|
|
@@ -13,20 +13,20 @@ The user has invoked this skill with a TASK-ID (e.g., `DEMO-001`).
|
|
|
13
13
|
|
|
14
14
|
2. Load the task:
|
|
15
15
|
```
|
|
16
|
-
|
|
16
|
+
cloverleaf-cli load-task <repo_root> <TASK-ID>
|
|
17
17
|
```
|
|
18
18
|
Parse the JSON. Verify `status === "pending"` OR `status === "implementing"` (the second case is a re-run after a Reviewer bounce). If neither, report the current status and ask the user to use the correct command for that state.
|
|
19
19
|
|
|
20
20
|
3. Load any outstanding feedback:
|
|
21
21
|
```
|
|
22
|
-
|
|
22
|
+
cloverleaf-cli latest-feedback <repo_root> <TASK-ID>
|
|
23
23
|
```
|
|
24
24
|
Capture the output. If present and the latest verdict is `bounce`, pass it into the subagent.
|
|
25
25
|
|
|
26
26
|
4. Dispatch the Implementer subagent via the Task tool:
|
|
27
27
|
- `subagent_type`: `general-purpose`
|
|
28
28
|
- `model`: `sonnet`
|
|
29
|
-
- Prompt: the contents of
|
|
29
|
+
- Prompt: the contents of `$(cloverleaf-cli plugin-root)/prompts/implementer.md`, with placeholders substituted:
|
|
30
30
|
- `{{task}}` → the full task JSON (pretty-printed)
|
|
31
31
|
- `{{feedback}}` → the feedback JSON if present, else the literal string `null`
|
|
32
32
|
- `{{repo_root}}` → absolute path to the current repo
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloverleaf-merge
|
|
3
|
-
description: Human gate for merging a Cloverleaf task. Branches on state — from `automated-gates` (fast lane) via `human_merge`, or from `final-gate` (full pipeline) via `final_approval_gate`. Requires explicit user confirmation. Usage — /cloverleaf-merge <TASK-ID>.
|
|
3
|
+
description: Human gate for merging a Cloverleaf task. Branches on state — from `automated-gates` (fast lane) via `human_merge`, or from `final-gate` (full pipeline) via `final_approval_gate`. For full-pipeline tasks, performs a real `git merge --no-ff` of the feature branch into main before advancing state. Requires explicit user confirmation. Usage — /cloverleaf-merge <TASK-ID>.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cloverleaf — merge
|
|
@@ -53,18 +53,39 @@ description: Human gate for merging a Cloverleaf task. Branches on state — fro
|
|
|
53
53
|
|
|
54
54
|
3B.3. On explicit `y`:
|
|
55
55
|
- `cloverleaf-cli emit-gate-decision <repo_root> <TASK-ID> final_approval_gate approve human`
|
|
56
|
-
-
|
|
57
|
-
|
|
56
|
+
- Verify we are on main with a clean working tree:
|
|
57
|
+
```bash
|
|
58
|
+
cd <repo_root>
|
|
59
|
+
git checkout main
|
|
60
|
+
git status
|
|
61
|
+
# must be clean
|
|
62
|
+
```
|
|
63
|
+
If not clean, stop and report.
|
|
64
|
+
- Perform the real merge — brings the feature branch's Implementer commit, ui-review baseline commit, and any feedback commits into main:
|
|
65
|
+
```bash
|
|
66
|
+
git merge --no-ff cloverleaf/${TASK_ID} -m "cloverleaf: ${TASK_ID} merged (full pipeline)"
|
|
67
|
+
```
|
|
68
|
+
If git reports conflicts: abort and escalate.
|
|
69
|
+
```bash
|
|
70
|
+
git merge --abort
|
|
71
|
+
cloverleaf-cli advance-status <repo_root> ${TASK_ID} escalated agent
|
|
72
|
+
```
|
|
73
|
+
Exit with a human-readable error explaining the conflict.
|
|
74
|
+
- Advance task status on main (commits `.cloverleaf/tasks/${TASK_ID}.json` + event):
|
|
75
|
+
```bash
|
|
76
|
+
cloverleaf-cli advance-status <repo_root> ${TASK_ID} merged agent
|
|
77
|
+
```
|
|
58
78
|
|
|
59
79
|
### 4. Common: report
|
|
60
80
|
|
|
61
81
|
Report:
|
|
62
|
-
- "✓ Merged `<TASK-ID>`. Branch `cloverleaf/<TASK-ID>`
|
|
63
|
-
- "Suggested: `git push origin
|
|
82
|
+
- "✓ Merged `<TASK-ID>`. Branch `cloverleaf/<TASK-ID>` has been merged into main."
|
|
83
|
+
- "Suggested: `git push origin main` to push the merge commit."
|
|
64
84
|
|
|
65
85
|
## Rules
|
|
66
86
|
|
|
67
87
|
- Only proceed on explicit `y`, `Y`, `yes`, `YES`. Anything else: abort without state change.
|
|
68
88
|
- The skill does NOT push the branch or open a PR.
|
|
69
89
|
- Fast lane and full pipeline use distinct gates — the state machine records which path was taken.
|
|
90
|
+
- Full-pipeline merges perform a real `git merge --no-ff` before advancing state — the feature branch's code, baselines, and feedback commits all land on main.
|
|
70
91
|
- If the user declines, no state change and no commit.
|
|
@@ -11,13 +11,13 @@ The user has invoked this skill with a brief. Your job: turn the brief into a st
|
|
|
11
11
|
|
|
12
12
|
1. Determine the active project. Run:
|
|
13
13
|
```
|
|
14
|
-
|
|
14
|
+
cloverleaf-cli infer-project <repo_root>
|
|
15
15
|
```
|
|
16
16
|
where `<repo_root>` is the current working directory. On failure (no projects, or multiple projects), report the error and ask the user to specify `--project=<id>` or to create a project config first.
|
|
17
17
|
|
|
18
18
|
2. Allocate the next task ID:
|
|
19
19
|
```
|
|
20
|
-
|
|
20
|
+
cloverleaf-cli next-task-id <repo_root> --project=<project>
|
|
21
21
|
```
|
|
22
22
|
Capture the output (e.g., `DEMO-002`).
|
|
23
23
|
|
|
@@ -7,7 +7,11 @@ description: Run the QA agent on a task in the `qa` state (full pipeline only).
|
|
|
7
7
|
|
|
8
8
|
## Steps
|
|
9
9
|
|
|
10
|
-
0.
|
|
10
|
+
0. Pre-flight: ensure you are on `main` and clean stale feedback temp files from previous runs (prevents /tmp leakage between tasks). If not on main, `git checkout main`. If main has uncommitted changes, stop and report.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
rm -f /tmp/cloverleaf-fb-r.json /tmp/cloverleaf-fb-u.json /tmp/cloverleaf-fb-q.json
|
|
14
|
+
```
|
|
11
15
|
|
|
12
16
|
1. Capture the TASK-ID argument.
|
|
13
17
|
|
|
@@ -30,7 +34,7 @@ description: Run the QA agent on a task in the `qa` state (full pipeline only).
|
|
|
30
34
|
if [ -f "<repo_root>/.cloverleaf/config/qa-rules.json" ]; then
|
|
31
35
|
cat "<repo_root>/.cloverleaf/config/qa-rules.json"
|
|
32
36
|
else
|
|
33
|
-
cat
|
|
37
|
+
cat $(cloverleaf-cli plugin-root)/config/qa-rules.json
|
|
34
38
|
fi
|
|
35
39
|
```
|
|
36
40
|
Capture for the subagent as `qa_rules`.
|
|
@@ -43,7 +47,7 @@ description: Run the QA agent on a task in the `qa` state (full pipeline only).
|
|
|
43
47
|
7. Dispatch the QA subagent via the Task tool:
|
|
44
48
|
- `subagent_type`: `general-purpose`
|
|
45
49
|
- `model`: `sonnet`
|
|
46
|
-
- Prompt: contents of
|
|
50
|
+
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/qa.md` with substitutions for `{{task}}`, `{{diff}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{qa_rules}}` (the JSON loaded in step 5).
|
|
47
51
|
|
|
48
52
|
8. Parse response: expect `{"verdict": "pass"|"bounce"|"escalate", "summary", "findings", "results"}`.
|
|
49
53
|
|
|
@@ -59,9 +63,15 @@ description: Run the QA agent on a task in the `qa` state (full pipeline only).
|
|
|
59
63
|
**Bounce:**
|
|
60
64
|
1. Write feedback envelope: `echo '<json>' > /tmp/cloverleaf-fb-q.json`
|
|
61
65
|
2. `cloverleaf-cli write-feedback <repo_root> <TASK-ID> /tmp/cloverleaf-fb-q.json --prefix=q`
|
|
62
|
-
3.
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
3. Commit the persisted feedback file (was missing pre-v0.4.1 — bug #3):
|
|
67
|
+
```bash
|
|
68
|
+
cd <repo_root>
|
|
69
|
+
git add .cloverleaf/feedback/
|
|
70
|
+
git commit -m "cloverleaf: <TASK-ID> qa feedback"
|
|
71
|
+
```
|
|
72
|
+
4. `cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent --path=full_pipeline`
|
|
73
|
+
5. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> qa bounced → implementing"`.
|
|
74
|
+
6. Report: "✗ QA bounced. `<failed>/<total>` tests failed. State → implementing. Next: `/cloverleaf-implement <TASK-ID>`."
|
|
65
75
|
|
|
66
76
|
**Escalate:**
|
|
67
77
|
1. `cloverleaf-cli advance-status <repo_root> <TASK-ID> escalated agent`
|
|
@@ -7,7 +7,7 @@ description: Run the Reviewer agent on a task in the `review` state. Emits a fee
|
|
|
7
7
|
|
|
8
8
|
## Steps
|
|
9
9
|
|
|
10
|
-
0.
|
|
10
|
+
0. Pre-flight: ensure you are on `main` and clean stale feedback temp files from previous runs (prevents /tmp leakage between tasks):
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
cd <repo_root>
|
|
@@ -17,11 +17,15 @@ description: Run the Reviewer agent on a task in the `review` state. Emits a fee
|
|
|
17
17
|
|
|
18
18
|
If main has uncommitted changes, stop and report — the user must clean up first.
|
|
19
19
|
|
|
20
|
+
```bash
|
|
21
|
+
rm -f /tmp/cloverleaf-fb-r.json /tmp/cloverleaf-fb-u.json /tmp/cloverleaf-fb-q.json
|
|
22
|
+
```
|
|
23
|
+
|
|
20
24
|
1. Capture the TASK-ID argument.
|
|
21
25
|
|
|
22
26
|
2. Load the task:
|
|
23
27
|
```
|
|
24
|
-
|
|
28
|
+
cloverleaf-cli load-task <repo_root> <TASK-ID>
|
|
25
29
|
```
|
|
26
30
|
Verify `status === "review"`. If not, report the current status and stop.
|
|
27
31
|
|
|
@@ -36,7 +40,7 @@ description: Run the Reviewer agent on a task in the `review` state. Emits a fee
|
|
|
36
40
|
5. Dispatch the Reviewer subagent via the Task tool:
|
|
37
41
|
- `subagent_type`: `general-purpose`
|
|
38
42
|
- `model`: `sonnet`
|
|
39
|
-
- Prompt: contents of
|
|
43
|
+
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/reviewer.md` with substitutions for `{{task}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{diff}}`.
|
|
40
44
|
|
|
41
45
|
6. Parse the subagent's response. Expect a feedback envelope JSON of the form `{"verdict": "pass"|"bounce", "summary": "...", "findings": [...]}`. Validate shape: verdict must be `pass` or `bounce`; if `bounce`, findings must have at least one entry with `severity` (one of `blocker|error|warning|info`) and `message`.
|
|
42
46
|
|
|
@@ -50,11 +54,17 @@ description: Run the Reviewer agent on a task in the `review` state. Emits a fee
|
|
|
50
54
|
Report: "✓ Review passed. State → automated-gates. Next: `/cloverleaf-merge <TASK-ID>`."
|
|
51
55
|
|
|
52
56
|
**Bounce:**
|
|
53
|
-
1. Write the feedback envelope to a temp file: `echo '<envelope-json>' > /tmp/cloverleaf-fb.json`.
|
|
54
|
-
2. `cloverleaf-cli write-feedback <repo_root> <TASK-ID> /tmp/cloverleaf-fb.json` — captures the path like `.cloverleaf/feedback/<TASK-ID>-r<N>.json`.
|
|
55
|
-
3.
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
1. Write the feedback envelope to a temp file: `echo '<envelope-json>' > /tmp/cloverleaf-fb-r.json`.
|
|
58
|
+
2. `cloverleaf-cli write-feedback <repo_root> <TASK-ID> /tmp/cloverleaf-fb-r.json` — captures the path like `.cloverleaf/feedback/<TASK-ID>-r<N>.json`.
|
|
59
|
+
3. Commit the persisted feedback file (was missing pre-v0.4.1 — bug #3):
|
|
60
|
+
```bash
|
|
61
|
+
cd <repo_root>
|
|
62
|
+
git add .cloverleaf/feedback/
|
|
63
|
+
git commit -m "cloverleaf: <TASK-ID> review feedback"
|
|
64
|
+
```
|
|
65
|
+
4. `cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent` — loops back.
|
|
66
|
+
5. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> review bounced → implementing"`.
|
|
67
|
+
6. Report: "✗ Review bounced. Findings: <summarize findings by severity>. State → implementing. Next: `/cloverleaf-implement <TASK-ID>`."
|
|
58
68
|
|
|
59
69
|
## Rules
|
|
60
70
|
|
|
@@ -7,7 +7,7 @@ description: Run the UI Reviewer agent on a task in the `ui-review` state (full
|
|
|
7
7
|
|
|
8
8
|
## Steps
|
|
9
9
|
|
|
10
|
-
0.
|
|
10
|
+
0. Pre-flight: ensure you are on `main` and clean stale feedback temp files from previous runs (prevents /tmp leakage between tasks):
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
cd <repo_root>
|
|
@@ -17,11 +17,15 @@ description: Run the UI Reviewer agent on a task in the `ui-review` state (full
|
|
|
17
17
|
|
|
18
18
|
If main has uncommitted changes, stop and report.
|
|
19
19
|
|
|
20
|
+
```bash
|
|
21
|
+
rm -f /tmp/cloverleaf-fb-r.json /tmp/cloverleaf-fb-u.json /tmp/cloverleaf-fb-q.json
|
|
22
|
+
```
|
|
23
|
+
|
|
20
24
|
1. Capture the TASK-ID argument.
|
|
21
25
|
|
|
22
26
|
2. Load the task:
|
|
23
27
|
```
|
|
24
|
-
|
|
28
|
+
cloverleaf-cli load-task <repo_root> <TASK-ID>
|
|
25
29
|
```
|
|
26
30
|
Verify `status === "ui-review"`. If not, report and stop.
|
|
27
31
|
|
|
@@ -35,7 +39,7 @@ description: Run the UI Reviewer agent on a task in the `ui-review` state (full
|
|
|
35
39
|
|
|
36
40
|
5. Compute affected routes:
|
|
37
41
|
```bash
|
|
38
|
-
AFFECTED=$(
|
|
42
|
+
AFFECTED=$(cloverleaf-cli affected-routes <repo_root> <TASK-ID>)
|
|
39
43
|
```
|
|
40
44
|
|
|
41
45
|
6. **Empty-set early-exit.** If `AFFECTED` is `[]`, skip the subagent entirely:
|
|
@@ -63,7 +67,7 @@ description: Run the UI Reviewer agent on a task in the `ui-review` state (full
|
|
|
63
67
|
10. Dispatch the UI Reviewer subagent via the Task tool:
|
|
64
68
|
- `subagent_type`: `general-purpose`
|
|
65
69
|
- `model`: `sonnet`
|
|
66
|
-
- Prompt: contents of
|
|
70
|
+
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/ui-reviewer.md` with substitutions:
|
|
67
71
|
- `{{task}}`, `{{diff}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{preview_port}}`
|
|
68
72
|
- `{{affected_routes}}` → the value of `$AFFECTED` (verbatim — may be `"all"`, a JSON array, or `[]` but step 6 handled `[]` already)
|
|
69
73
|
- `{{ui_review_config}}` → JSON-stringified result of `cloverleaf-cli ui-review-config <repo_root>` (used by the subagent to scope viewport sizes, thresholds, and axe rule overrides)
|
|
@@ -82,9 +86,15 @@ description: Run the UI Reviewer agent on a task in the `ui-review` state (full
|
|
|
82
86
|
**Bounce:**
|
|
83
87
|
1. Write feedback: `echo '<envelope-json>' > /tmp/cloverleaf-fb-u.json`
|
|
84
88
|
2. `cloverleaf-cli write-feedback <repo_root> <TASK-ID> /tmp/cloverleaf-fb-u.json --prefix=u`
|
|
85
|
-
3.
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
3. Commit the persisted feedback file (was missing pre-v0.4.1 — bug #3):
|
|
90
|
+
```bash
|
|
91
|
+
cd <repo_root>
|
|
92
|
+
git add .cloverleaf/feedback/
|
|
93
|
+
git commit -m "cloverleaf: <TASK-ID> ui-review feedback"
|
|
94
|
+
```
|
|
95
|
+
4. `cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent '' full_pipeline`
|
|
96
|
+
5. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> ui-review bounced → implementing"`.
|
|
97
|
+
6. Report: "✗ UI Review bounced. Findings: <summary by severity>. State → implementing. Next: `/cloverleaf-implement <TASK-ID>`."
|
|
88
98
|
|
|
89
99
|
**Escalate:**
|
|
90
100
|
1. `cloverleaf-cli advance-status <repo_root> <TASK-ID> escalated agent`
|