@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.
@@ -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.0",
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.0
1
+ 0.4.1
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "axe": {
14
14
  "viewports": ["desktop"],
15
- "dedupeBy": ["ruleId", "target"]
15
+ "dedupeBy": ["ruleId", "target"],
16
+ "ignored": []
16
17
  }
17
18
  }
@@ -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 raws) {
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)}-r(\\d+)\\.json$`);
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(raws: RawAxeFinding[], keys: DedupeKey[]): Finding[] {
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 raws) {
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)}-r(\\d+)\\.json$`);
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
+ }
@@ -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.0",
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",
@@ -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 `<repoRoot>/.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.)
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 = <repoRoot>/.cloverleaf/baselines/{slug}-{viewport}.png`
78
+ - `baselinePath = {{repo_root}}/.cloverleaf/baselines/{slug}-{viewport}.png`
66
79
  - `candidateBuf = <candidate PNG>`
67
- - `diffPath = <repoRoot>/.cloverleaf/runs/{taskId}/ui-review/diff-{slug}-{viewport}.png`
68
- - `candidateOutPath = <repoRoot>/.cloverleaf/runs/{taskId}/ui-review/candidate-{slug}-{viewport}.png`
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 `<repoRoot>/.cloverleaf/baselines/` and `<repoRoot>/.cloverleaf/runs/{taskId}/ui-review/` on the feature branch — these are the baselines and artifacts.
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
- ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli load-task <repo_root> <TASK-ID>
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 `~/.claude/plugins/cloverleaf/prompts/documenter.md` with substitutions:
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
- ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli load-task <repo_root> <TASK-ID>
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
- ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli latest-feedback <repo_root> <TASK-ID>
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 `~/.claude/plugins/cloverleaf/prompts/implementer.md`, with placeholders substituted:
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
- - `cloverleaf-cli advance-status <repo_root> <TASK-ID> merged human final_approval_gate full_pipeline`
57
- - Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> merged (full pipeline)"`.
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>` is ready for you to push and open a PR."
63
- - "Suggested: `git push origin cloverleaf/<TASK-ID>` then open a PR against `main`."
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
- ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli infer-project <repo_root>
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
- ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli next-task-id <repo_root> --project=<project>
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. Ensure you are on `main`. If not, `git checkout main`. If main has uncommitted changes, stop and report.
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 ~/.claude/plugins/cloverleaf/config/qa-rules.json
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 `~/.claude/plugins/cloverleaf/prompts/qa.md` with substitutions for `{{task}}`, `{{diff}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{qa_rules}}` (the JSON loaded in step 5).
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. `cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent --path=full_pipeline`
63
- 4. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> qa bounced → implementing"`.
64
- 5. Report: "✗ QA bounced. `<failed>/<total>` tests failed. State → implementing. Next: `/cloverleaf-implement <TASK-ID>`."
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. Ensure you are on `main`. State is authoritative on main. Run:
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
- ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli load-task <repo_root> <TASK-ID>
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 `~/.claude/plugins/cloverleaf/prompts/reviewer.md` with substitutions for `{{task}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{diff}}`.
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. `cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent`loops back.
56
- 4. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> review bounced → implementing"`.
57
- 5. Report: "✗ Review bounced. Findings: <summarize findings by severity>. State → implementing. Next: `/cloverleaf-implement <TASK-ID>`."
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. Ensure you are on `main`. State is authoritative on main. Run:
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
- ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli load-task <repo_root> <TASK-ID>
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=$(~/.claude/plugins/cloverleaf/bin/cloverleaf-cli affected-routes <repo_root> <TASK-ID>)
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 `~/.claude/plugins/cloverleaf/prompts/ui-reviewer.md` with substitutions:
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. `cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent '' full_pipeline`
86
- 4. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> ui-review bounced → implementing"`.
87
- 5. Report: "✗ UI Review bounced. Findings: <summary by severity>. State → implementing. Next: `/cloverleaf-implement <TASK-ID>`."
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`