@cloverleaf/reference-impl 0.11.1 → 0.13.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/.claude-plugin/plugin.json +1 -1
- package/README.md +101 -37
- package/VERSION +1 -1
- package/config/council.json +10 -2
- package/dist/cli.mjs +27 -8
- package/dist/council.mjs +327 -45
- package/dist/events.mjs +3 -8
- package/dist/ids.mjs +7 -0
- package/dist/qa-rules.mjs +19 -3
- package/dist/task.mjs +6 -25
- package/dist/work-item.mjs +2 -3
- package/lib/cli.ts +28 -8
- package/lib/council-config.ts +1 -0
- package/lib/council-result.ts +4 -3
- package/lib/council.ts +399 -47
- package/lib/events.ts +4 -8
- package/lib/ids.ts +7 -0
- package/lib/qa-rules.ts +24 -3
- package/lib/task.ts +7 -33
- package/lib/work-item.ts +2 -4
- package/package.json +5 -3
- package/prompts/documenter.md +1 -1
- package/prompts/implementer.md +8 -2
- package/prompts/qa.md +6 -9
- package/prompts/reviewer.md +4 -2
- package/prompts/ui-reviewer.md +83 -11
- package/skills/cloverleaf-approve-baselines/SKILL.md +18 -26
- package/skills/cloverleaf-discover/SKILL.md +7 -1
- package/skills/cloverleaf-document/SKILL.md +5 -6
- package/skills/cloverleaf-implement/SKILL.md +24 -30
- package/skills/cloverleaf-merge/SKILL.md +14 -23
- package/skills/cloverleaf-new-task/SKILL.md +23 -4
- package/skills/cloverleaf-qa/SKILL.md +25 -24
- package/skills/cloverleaf-review/SKILL.md +24 -18
- package/skills/cloverleaf-run/SKILL.md +53 -103
- package/skills/cloverleaf-run-plan/SKILL.md +19 -26
- package/skills/cloverleaf-security-review/SKILL.md +22 -17
- package/skills/cloverleaf-ui-review/SKILL.md +59 -35
package/lib/events.ts
CHANGED
|
@@ -13,7 +13,6 @@ export interface StatusTransitionParams {
|
|
|
13
13
|
to: string;
|
|
14
14
|
actor: 'agent' | 'human' | 'system';
|
|
15
15
|
gate?: string;
|
|
16
|
-
path?: 'fast_lane' | 'full_pipeline';
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export interface GateDecisionParams {
|
|
@@ -31,11 +30,8 @@ function actorObject(kind: 'agent' | 'human' | 'system'): { kind: string; id: st
|
|
|
31
30
|
return { kind, id };
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
export function formatReason(opts: { gate?: string
|
|
35
|
-
|
|
36
|
-
if (opts.gate) parts.push(`gate=${opts.gate}`);
|
|
37
|
-
if (opts.path) parts.push(`path=${opts.path}`);
|
|
38
|
-
return parts.length > 0 ? parts.join('; ') : undefined;
|
|
33
|
+
export function formatReason(opts: { gate?: string }): string | undefined {
|
|
34
|
+
return opts.gate ? `gate=${opts.gate}` : undefined;
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
/**
|
|
@@ -56,8 +52,8 @@ export function emitStatusTransition(repoRoot: string, params: StatusTransitionP
|
|
|
56
52
|
const filename = `${workItemId}-${seqStr}-status.json`;
|
|
57
53
|
const filePath = join(eventsDir(repoRoot), filename);
|
|
58
54
|
|
|
59
|
-
// Build reason from gate
|
|
60
|
-
const reason = formatReason({ gate: params.gate
|
|
55
|
+
// Build reason from the gate if provided (the schema allows only `reason` at top level).
|
|
56
|
+
const reason = formatReason({ gate: params.gate });
|
|
61
57
|
|
|
62
58
|
const doc: Record<string, unknown> = {
|
|
63
59
|
event_id: randomUUID(),
|
package/lib/ids.ts
CHANGED
|
@@ -22,6 +22,13 @@ export function nextEventId(repoRoot: string, workItemId: string): number {
|
|
|
22
22
|
// simultaneously. A global per-project counter (the pre-v0.6 scheme) produced
|
|
23
23
|
// filename collisions when the walker merged sibling feature branches. Per-work-item
|
|
24
24
|
// scoping means each task's counter is independent; merges union cleanly.
|
|
25
|
+
//
|
|
26
|
+
// The `(\d+)` segment is load-bearing, not cosmetic: `.cloverleaf/events/` in
|
|
27
|
+
// long-lived repos also contains pre-v0.6 files named `<PROJECT>-<NNN>-status.json`
|
|
28
|
+
// (a global counter), whose names collide with the task-id namespace — e.g.
|
|
29
|
+
// `CLV-109-status.json` is the 109th project event, not an event for task CLV-109.
|
|
30
|
+
// Loosening this regex to a bare prefix match would fold unrelated tasks' history
|
|
31
|
+
// into a task's counter, including transitions through states retired in 0.8.0.
|
|
25
32
|
const re = new RegExp(`^${escapeRegex(workItemId)}-(\\d+)-(status|gate)\\.json$`);
|
|
26
33
|
const nums = readdirSync(dir)
|
|
27
34
|
.map((f) => f.match(re))
|
package/lib/qa-rules.ts
CHANGED
|
@@ -18,19 +18,40 @@ function loadDefaultRules(): QaRule[] {
|
|
|
18
18
|
return Array.isArray(doc.rules) ? doc.rules : [];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export
|
|
21
|
+
export interface QaRulesDocument {
|
|
22
|
+
rules: QaRule[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The qa-rules document in the shape the prompts consume it: the `{ rules: [...] }`
|
|
27
|
+
* object, not a bare array. `reviewer.md` ({{test_rules}}), `implementer.md`
|
|
28
|
+
* ({{test_rules}}) and `qa.md` ({{qa_rules}}) all document the token as an object —
|
|
29
|
+
* 0.10.1 shipped a fix precisely because `qa.md` had described it as an array, and an
|
|
30
|
+
* agent iterating a non-existent top-level array is the bug that fix closed. Callers
|
|
31
|
+
* substituting one of those tokens must stringify THIS, never `loadQaRulesConfig()`.
|
|
32
|
+
*
|
|
33
|
+
* Precedence matches what the standalone skills `cat`: the consumer's
|
|
34
|
+
* `.cloverleaf/config/qa-rules.json` when it exists and parses to a `rules` array,
|
|
35
|
+
* otherwise the packaged default.
|
|
36
|
+
*/
|
|
37
|
+
export function loadQaRulesDocument(repoRoot: string): QaRulesDocument {
|
|
22
38
|
const consumerPath = join(repoRoot, '.cloverleaf', 'config', 'qa-rules.json');
|
|
23
39
|
if (existsSync(consumerPath)) {
|
|
24
40
|
try {
|
|
25
41
|
const doc = JSON.parse(readFileSync(consumerPath, 'utf-8')) as { rules?: QaRule[] };
|
|
26
42
|
if (Array.isArray(doc.rules)) {
|
|
27
|
-
return doc.rules;
|
|
43
|
+
return { rules: doc.rules };
|
|
28
44
|
}
|
|
29
45
|
} catch {
|
|
30
46
|
// fall through
|
|
31
47
|
}
|
|
32
48
|
}
|
|
33
|
-
return loadDefaultRules();
|
|
49
|
+
return { rules: loadDefaultRules() };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The rules array alone, for callers that select/execute commands rather than prompt with them. */
|
|
53
|
+
export function loadQaRulesConfig(repoRoot: string): QaRule[] {
|
|
54
|
+
return loadQaRulesDocument(repoRoot).rules;
|
|
34
55
|
}
|
|
35
56
|
|
|
36
57
|
export function selectTestCommands(changedFiles: string[], rules: QaRule[]): QaRule[] {
|
package/lib/task.ts
CHANGED
|
@@ -53,16 +53,16 @@ export function advanceStatus(
|
|
|
53
53
|
taskId: string,
|
|
54
54
|
toStatus: string,
|
|
55
55
|
actor: 'agent' | 'human',
|
|
56
|
-
options: { gate?: string
|
|
56
|
+
options: { gate?: string } = {}
|
|
57
57
|
): TaskDoc {
|
|
58
58
|
let task = loadTask(repoRoot, taskId);
|
|
59
59
|
const from = task.status;
|
|
60
60
|
const sm = loadStateMachine('task');
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
if (
|
|
62
|
+
// Security classification at council entry: a declared-low task whose diff touches a
|
|
63
|
+
// sensitive path is upgraded to security_class:high so the delivery council runs its
|
|
64
|
+
// blocking security member. (v0.8.0: replaces the retired security_gate FSM annotation.)
|
|
65
|
+
if (from === 'documenting' && toStatus === 'council') {
|
|
66
66
|
let classification;
|
|
67
67
|
try {
|
|
68
68
|
classification = classifyTaskSecurity(repoRoot, taskId);
|
|
@@ -96,17 +96,12 @@ export function advanceStatus(
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const riskClass: 'low' | 'high' =
|
|
100
|
-
options.path === 'fast_lane' ? 'low'
|
|
101
|
-
: options.path === 'full_pipeline' ? 'high'
|
|
102
|
-
: (task.risk_class ?? 'low');
|
|
103
|
-
|
|
104
99
|
const workItemForValidator: SMTask = {
|
|
105
100
|
type: 'task',
|
|
106
101
|
id: task.id,
|
|
107
102
|
project: task.project,
|
|
108
103
|
status: task.status,
|
|
109
|
-
risk_class:
|
|
104
|
+
risk_class: task.risk_class ?? 'low',
|
|
110
105
|
security_class: task.security_class,
|
|
111
106
|
security_review_verdict: task.security_review_verdict,
|
|
112
107
|
context: { rfc: { project: task.project, id: task.id } },
|
|
@@ -114,12 +109,7 @@ export function advanceStatus(
|
|
|
114
109
|
acceptance_criteria: task.acceptance_criteria,
|
|
115
110
|
};
|
|
116
111
|
|
|
117
|
-
const
|
|
118
|
-
const proposed: TaskDoc = {
|
|
119
|
-
...task,
|
|
120
|
-
status: toStatus,
|
|
121
|
-
...(resetsVerdict ? { security_review_verdict: null } : {}),
|
|
122
|
-
};
|
|
112
|
+
const proposed: TaskDoc = { ...task, status: toStatus };
|
|
123
113
|
|
|
124
114
|
advanceWorkItemStatus({
|
|
125
115
|
repoRoot,
|
|
@@ -134,23 +124,7 @@ export function advanceStatus(
|
|
|
134
124
|
save: (p) => saveTask(repoRoot, p as TaskDoc),
|
|
135
125
|
proposed,
|
|
136
126
|
gate: options.gate,
|
|
137
|
-
path: options.path,
|
|
138
127
|
});
|
|
139
128
|
|
|
140
|
-
// After a successful status change + verdict reset, emit a single commit covering both.
|
|
141
|
-
if (resetsVerdict) {
|
|
142
|
-
const taskFilePath = join(tasksDir(repoRoot), `${taskId}.json`);
|
|
143
|
-
try {
|
|
144
|
-
execFileSync('git', ['-C', repoRoot, 'add', taskFilePath], { stdio: 'pipe' });
|
|
145
|
-
execFileSync(
|
|
146
|
-
'git',
|
|
147
|
-
['-C', repoRoot, 'commit', '-m', `cloverleaf: ${taskId} status ${from} → ${toStatus}; security_review_verdict → null (rework)`],
|
|
148
|
-
{ stdio: 'pipe' }
|
|
149
|
-
);
|
|
150
|
-
} catch {
|
|
151
|
-
// No-op: commit is best-effort when running outside a git repo (e.g., test environments).
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
129
|
return proposed;
|
|
156
130
|
}
|
package/lib/work-item.ts
CHANGED
|
@@ -26,7 +26,6 @@ export interface AdvanceWorkItemParams<T> {
|
|
|
26
26
|
save: (proposed: T & { status: string }) => void;
|
|
27
27
|
proposed: T;
|
|
28
28
|
gate?: string;
|
|
29
|
-
path?: 'fast_lane' | 'full_pipeline';
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
export interface AdvanceWorkItemResult {
|
|
@@ -35,9 +34,9 @@ export interface AdvanceWorkItemResult {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export function advanceWorkItemStatus<T>(params: AdvanceWorkItemParams<T>): AdvanceWorkItemResult {
|
|
38
|
-
const { repoRoot, workItemType, project, id, from, to, actor, stateMachine, validateFixture, save, gate
|
|
37
|
+
const { repoRoot, workItemType, project, id, from, to, actor, stateMachine, validateFixture, save, gate } = params;
|
|
39
38
|
|
|
40
|
-
const reason = formatReason({ gate
|
|
39
|
+
const reason = formatReason({ gate });
|
|
41
40
|
const event = {
|
|
42
41
|
event_id: randomUUID(),
|
|
43
42
|
event_type: 'status_transition' as const,
|
|
@@ -74,7 +73,6 @@ export function advanceWorkItemStatus<T>(params: AdvanceWorkItemParams<T>): Adva
|
|
|
74
73
|
to,
|
|
75
74
|
actor,
|
|
76
75
|
gate,
|
|
77
|
-
path,
|
|
78
76
|
});
|
|
79
77
|
|
|
80
78
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloverleaf/reference-impl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"prepublishOnly": "node scripts/check-standard-prepped.mjs && npm test && npm run build"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@cloverleaf/standard": "^0.
|
|
52
|
+
"@cloverleaf/standard": "^0.8.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@cloverleaf/standard": "^0.
|
|
55
|
+
"@cloverleaf/standard": "^0.8.0",
|
|
56
56
|
"ajv": "^8.17.1",
|
|
57
57
|
"ajv-formats": "^3.0.1",
|
|
58
58
|
"axe-core": "^4.10.0",
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"@types/node": "^22.0.0",
|
|
65
65
|
"@types/pixelmatch": "^5.2.6",
|
|
66
66
|
"@types/pngjs": "^6.0.5",
|
|
67
|
+
"@types/semver": "^7.8.0",
|
|
68
|
+
"semver": "^7.8.5",
|
|
67
69
|
"tsx": "^4.19.0",
|
|
68
70
|
"typescript": "^5.5.0",
|
|
69
71
|
"vitest": "^2.0.0"
|
package/prompts/documenter.md
CHANGED
|
@@ -36,7 +36,7 @@ Inspect the diff. For each category below that matches, update the listed docs:
|
|
|
36
36
|
|
|
37
37
|
| Diff touches | Docs to update |
|
|
38
38
|
|---|---|
|
|
39
|
-
| `standard/
|
|
39
|
+
| `standard/schemas/**`, `standard/state-machines/**`, `standard/validators/**`, `standard/agent-contracts/**`, `standard/conformance/**` | `standard/CHANGELOG.md` (Unreleased), relevant `standard/docs/*.md` sections if behavior/conformance changed |
|
|
40
40
|
| `reference-impl/lib/**`, `reference-impl/skills/**`, `reference-impl/prompts/**` | `reference-impl/CHANGELOG.md` (Unreleased), `reference-impl/README.md` if public surface changed (new skill, CLI command, exported lib symbol) |
|
|
41
41
|
| `site/src/**`, `site/public/**` | `site/CHANGELOG.md` ONLY if that file already exists; otherwise skip |
|
|
42
42
|
| Root-level package additions, version bumps | Root `README.md`, root `CHANGELOG.md` |
|
package/prompts/implementer.md
CHANGED
|
@@ -25,11 +25,17 @@ You are the Cloverleaf Implementer agent. Your job: take a Task and produce work
|
|
|
25
25
|
**Scope nudge.** Your declared scope is `task.scope.files_touched`. You may freely modify any file listed there. If you discover during implementation that you need to touch a file outside that list, you may do so only if no sibling task in the same Plan declares that file — the walker auto-extends your scope on merge. If a file you need is already declared by a sibling task, that is a contested modification: stop, surface the conflict to the human, and do not merge. The walker enforces this at merge time and will refuse contested merges; auto-resolution is never attempted.
|
|
26
26
|
|
|
27
27
|
2. If `feedback` is present, re-read each finding; plan how to address them. If the prior bounce came from a chair council (`.cloverleaf/runs/<task.id>/council/task.review.json` has `rule: "chair"`), prioritize the members listed in its `forward` array and the chair's `rationale`.
|
|
28
|
-
3.
|
|
28
|
+
3. Get onto the task's branch, `cloverleaf/<task.id>`:
|
|
29
|
+
- If it does **not** exist, create it from `base_branch`: `git checkout -b cloverleaf/<task.id>`.
|
|
30
|
+
- If it **does** exist, you are reworking after a council bounce. Check it out — `git checkout cloverleaf/<task.id>` — and re-implement in place. **Never recreate or reset the branch:** it already carries earlier work, including the Documenter's doc commits, and rebuilding it from `base_branch` silently destroys them.
|
|
29
31
|
4. Implement the code + tests needed to satisfy every acceptance criterion.
|
|
30
32
|
5. Run the project's tests. Your test rules are provided as `{{test_rules}}` — a JSON object `{ rules: [...] }` whose `rules` is a list of `{cwd, match, command}` entries; each `match` is a list of glob patterns. For each rule whose `match` covers a file you changed, run its `command` in its `cwd`. All must pass. (If no rule matches your changes, there is nothing to run.)
|
|
33
|
+
|
|
34
|
+
Run your verification to completion **before returning**. If a command may exceed the default tool timeout, pass an explicit longer timeout rather than backgrounding it and returning early — a prose "I'll wait" message is not a valid result, and the orchestrator has no defined recovery for a non-conforming response. (This project's own `reference-impl` suite takes roughly five minutes, which exceeds the default.)
|
|
35
|
+
|
|
36
|
+
**Capture suite results safely:** redirect output to a file and check the exit code — `<command> > /tmp/suite.log 2>&1; echo "EXIT=$?"` — and never pipe the run through `| tail` or `| head`. A pipe reports the *last* command's exit status, so a failing suite reads as success.
|
|
31
37
|
6. Stage and commit your changes with message `feat: <task.title> [<task.id>]`.
|
|
32
|
-
7. Return a structured JSON result
|
|
38
|
+
7. Return a structured JSON result as your final message — **exactly one JSON object and nothing else**, no prose before or after it, no markdown fence commentary:
|
|
33
39
|
|
|
34
40
|
```json
|
|
35
41
|
{
|
package/prompts/qa.md
CHANGED
|
@@ -43,18 +43,19 @@ The Standard's QA contract requires a `preview_uri`. You were passed the sentine
|
|
|
43
43
|
|
|
44
44
|
3. If no rules match (e.g., the diff only changes `.cloverleaf/**` or tests unrelated to any package), skip with a `pass` verdict — nothing testable in this diff:
|
|
45
45
|
```json
|
|
46
|
-
{"verdict": "pass", "summary": "No testable packages changed.
|
|
46
|
+
{"verdict": "pass", "summary": "No testable packages changed. Test counts: passed 0, failed 0, total 0.", "findings": []}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
4. For each queued command:
|
|
50
50
|
- Run it in `"$TMPDIR/<cwd>"`
|
|
51
51
|
- Capture stdout, stderr, exit code
|
|
52
|
+
- **Capture suite results safely:** redirect output to a file and check the exit code — `<command> > /tmp/suite.log 2>&1; echo "EXIT=$?"` — and never pipe the run through `| tail` or `| head`. A pipe reports the *last* command's exit status, so a failing suite reads as success.
|
|
52
53
|
- Parse test output to extract `passed`, `failed`, `total`:
|
|
53
54
|
- Exit code is the universal signal: exit 0 = the command's checks passed; non-zero = failed.
|
|
54
55
|
- When the output format is recognized, also extract counts, e.g. Vitest (`Tests N passed | M failed`), pytest (`N passed, M failed`), or a plain build/lint (exit 0 → `{passed: 1, failed: 0, total: 1}`).
|
|
55
56
|
- On failure, collect up to 10 failure names/messages as findings with `severity: "error"` and `rule: "qa.<suite>.<test-name>"`
|
|
56
57
|
|
|
57
|
-
5. Aggregate results: sum `passed`, `failed`, `total` across all runs
|
|
58
|
+
5. Aggregate results: sum `passed`, `failed`, `total` across all runs, and state them at the end of `summary`. Do not add a top-level `results` key — the feedback envelope schema forbids properties beyond `verdict`, `summary` and `findings`.
|
|
58
59
|
|
|
59
60
|
6. Compute verdict:
|
|
60
61
|
- `pass` — every command exited 0 AND aggregated `failed === 0`
|
|
@@ -94,6 +95,7 @@ The CLI creates the output directory.
|
|
|
94
95
|
|
|
95
96
|
In the feedback you emit, include the report as an attachment on a single info-level finding (or on whichever summary finding you already emit):
|
|
96
97
|
|
|
98
|
+
<!-- cloverleaf-schema: feedback.schema.json#/$defs/finding -->
|
|
97
99
|
```json
|
|
98
100
|
{
|
|
99
101
|
"severity": "info",
|
|
@@ -114,7 +116,7 @@ Respond with exactly one JSON object and nothing else:
|
|
|
114
116
|
```json
|
|
115
117
|
{
|
|
116
118
|
"verdict": "pass" | "bounce" | "escalate",
|
|
117
|
-
"summary": "<one-sentence summary>",
|
|
119
|
+
"summary": "<one-sentence summary, ending with the aggregate counts, e.g. 'Test counts: passed 153, failed 0, total 153.'>",
|
|
118
120
|
"findings": [
|
|
119
121
|
{
|
|
120
122
|
"severity": "error",
|
|
@@ -122,11 +124,6 @@ Respond with exactly one JSON object and nothing else:
|
|
|
122
124
|
"message": "<test failure message>",
|
|
123
125
|
"location": "<file:line if known>"
|
|
124
126
|
}
|
|
125
|
-
]
|
|
126
|
-
"results": {
|
|
127
|
-
"passed": <integer>,
|
|
128
|
-
"failed": <integer>,
|
|
129
|
-
"total": <integer>
|
|
130
|
-
}
|
|
127
|
+
]
|
|
131
128
|
}
|
|
132
129
|
```
|
package/prompts/reviewer.md
CHANGED
|
@@ -21,13 +21,13 @@ You are the Cloverleaf Reviewer agent. Your job: perform a fresh-eyes review of
|
|
|
21
21
|
Run this as the first executable step before anything else. Session B sessions may inherit an arbitrary `cwd` from the walker harness; this anchors you at the repo root.
|
|
22
22
|
|
|
23
23
|
1. Read the task's `acceptance_criteria` and `definition_of_done`.
|
|
24
|
-
2. Run `git diff <base_branch>..<branch> --stat` and `git diff <base_branch>..<branch
|
|
24
|
+
2. Run `git diff <base_branch>..<branch> --stat -- ':(exclude).cloverleaf/'` and `git diff <base_branch>..<branch> -- ':(exclude).cloverleaf/'` to see the change. Excluding `.cloverleaf/` matters: the orchestrator commits FSM state to `<base_branch>` while the feature branch stays behind, so an unfiltered two-dot diff shows the branch "deleting" event files and reverting the task's `status`. That is branch/base divergence, not implementer drift — never bounce a task for it.
|
|
25
25
|
3. For each acceptance criterion, determine whether the diff satisfies it. Note any unsatisfied criteria as findings.
|
|
26
26
|
4. Check for defects: missing tests, obvious logic errors, security issues, hygiene problems.
|
|
27
27
|
5. Decide verdict:
|
|
28
28
|
- `pass` if every acceptance criterion is satisfied and no blocking defects exist.
|
|
29
29
|
- `bounce` otherwise.
|
|
30
|
-
6. Return a feedback envelope (per `feedback.schema.json`)
|
|
30
|
+
6. Return a feedback envelope (per `feedback.schema.json`) as your final message — **exactly one JSON object and nothing else**:
|
|
31
31
|
|
|
32
32
|
```json
|
|
33
33
|
{
|
|
@@ -67,6 +67,8 @@ A `pass` verdict MAY have an empty `findings` array or omit it. A `bounce` verdi
|
|
|
67
67
|
Use `--detach` with a SHA rather than a branch name: when running inside a walker worktree, the feature branch (and main) may already be checked out in another worktree, causing `git worktree add` to fail with "fatal: branch … is already checked out". Detaching at a SHA bypasses this constraint entirely.
|
|
68
68
|
|
|
69
69
|
This keeps `.cloverleaf/` on main intact.
|
|
70
|
+
|
|
71
|
+
**Capture suite results safely:** redirect output to a file and check the exit code — `<command> > /tmp/suite.log 2>&1; echo "EXIT=$?"` — and never pipe the run through `| tail` or `| head`. A pipe reports the *last* command's exit status, so a failing suite reads as success.
|
|
70
72
|
- **Loading or running a module directly (TypeScript projects).** If your project is TypeScript, do not improvise `node -e "import('./lib/x.js')"` to spot-check a module — sources are `.ts` and the build emits `.mjs`, so a bare `.js` import resolves to neither. Use `npx tsx` instead (resolves `.ts` sources and `.js`-style import specifiers):
|
|
71
73
|
|
|
72
74
|
```bash
|
package/prompts/ui-reviewer.md
CHANGED
|
@@ -25,13 +25,53 @@ Run this as the first executable step before anything else. Session B sessions m
|
|
|
25
25
|
|
|
26
26
|
You operate in two filesystem locations — keep them straight:
|
|
27
27
|
|
|
28
|
-
- `<worktree>` — the ephemeral worktree at `$WT` (set up in step 2 of the Runtime procedure). You run the dev server here and
|
|
28
|
+
- `<worktree>` — the ephemeral worktree at `$WT` (set up in step 2 of the Runtime procedure). You run the dev server here and drive Playwright against it. A standalone `.mjs` driver may live wherever you find convenient — its imports are anchored at the plugin root, not at its own directory (see "Resolving your dependencies" below) — but you MUST delete any driver you write during teardown (step 13).
|
|
29
29
|
- `<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.
|
|
30
30
|
|
|
31
31
|
**All `compareVisual` paths MUST be rooted at `{{repo_root}}`, NOT at `$WT`.**
|
|
32
32
|
|
|
33
33
|
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.
|
|
34
34
|
|
|
35
|
+
## Resolving your dependencies
|
|
36
|
+
|
|
37
|
+
`playwright`, `axe-core`, `pixelmatch` and `pngjs` are runtime dependencies of `@cloverleaf/reference-impl`, so they are installed under the **plugin root** — never under the repository you are reviewing. The repo's UI directory declares none of them, and `prep-worktree` copies no `node_modules` into `$WT/site/`. **A bare specifier therefore cannot resolve, from any directory**: `import 'playwright'` and `import 'axe-core'` both fail with `ERR_MODULE_NOT_FOUND` regardless of where your driver sits.
|
|
38
|
+
|
|
39
|
+
Export the plugin root once, before you run anything:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export CLOVERLEAF_PLUGIN_ROOT="$(npx cloverleaf-cli plugin-root)"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then anchor every import at it. `createRequire` resolves each package by name from the plugin root, so you never hard-code a package's internal file layout:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
import { createRequire } from 'node:module';
|
|
49
|
+
|
|
50
|
+
const PLUGIN_ROOT = process.env.CLOVERLEAF_PLUGIN_ROOT;
|
|
51
|
+
const require = createRequire(PLUGIN_ROOT + '/');
|
|
52
|
+
|
|
53
|
+
const { chromium, firefox, webkit } = require('playwright');
|
|
54
|
+
const axe = require('axe-core');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Cloverleaf's own helpers are ESM, one `dist/<module>.mjs` per `lib/<module>.ts`. Import them by absolute path:
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const { compareVisual } = await import(`${PLUGIN_ROOT}/dist/visual-diff.mjs`);
|
|
61
|
+
const { slugifyRoute } = await import(`${PLUGIN_ROOT}/dist/route-slug.mjs`);
|
|
62
|
+
const { dedupeAxeFindings } = await import(`${PLUGIN_ROOT}/dist/axe-dedupe.mjs`);
|
|
63
|
+
const { applyMaxCombinationsCap, buildBrowserEscalationFinding } =
|
|
64
|
+
await import(`${PLUGIN_ROOT}/dist/ui-browser.mjs`);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If an import fails, fix the anchor. Do **not** relocate the driver, do **not** add packages to the reviewed repo's `package.json`, and do **not** `npm install` anything into a scratch directory — none of that changes what resolves, and editing the repo's manifest would pollute the diff under review.
|
|
68
|
+
|
|
69
|
+
Keep whatever path you pick for the driver in a shell variable — the steps below call it `$DRIVER` — so teardown can delete it:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
DRIVER="$WT/site/ui-review-driver.mjs" # any path works; this one is also swept up with the worktree
|
|
73
|
+
```
|
|
74
|
+
|
|
35
75
|
## Scope (v0.5)
|
|
36
76
|
|
|
37
77
|
- **Browsers**: the reviewer runs separate Playwright sessions for each engine listed in `{{ui_review_config}}.browsers` (e.g., `["chromium", "webkit", "firefox"]`). Browser is the **outermost** loop, wrapping the viewport × route loops.
|
|
@@ -61,7 +101,15 @@ The cap enforcement helper is available in `lib/ui-browser.ts` as `applyMaxCombi
|
|
|
61
101
|
|
|
62
102
|
## Playwright cache
|
|
63
103
|
|
|
64
|
-
|
|
104
|
+
Playwright keeps its engine binaries in a shared cache outside the worktree, so a review never re-downloads ~300 MB. Establish that location yourself — do not assume a caller set it for you:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This defers to a dispatcher or operator who already set the variable (a non-default cache directory is a supported override) and falls back to Playwright's own default otherwise. Write `$HOME`, not `~`: a tilde inside double quotes is not expanded, and `"~/.cache/ms-playwright"` would create a literal `~` directory.
|
|
111
|
+
|
|
112
|
+
Before launching each browser session, verify that the required engine binary exists in `$PLAYWRIGHT_BROWSERS_PATH`. If a browser binary is absent, return `verdict: "escalate"` with a synthetic finding per missing engine:
|
|
65
113
|
|
|
66
114
|
```
|
|
67
115
|
"Playwright {engine} not installed. Run 'npx playwright install webkit firefox' on this machine."
|
|
@@ -84,17 +132,34 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
|
|
|
84
132
|
npx cloverleaf-cli prep-worktree {{repo_root}} "$WT"
|
|
85
133
|
```
|
|
86
134
|
|
|
87
|
-
3. For this repo, UI lives in `site/` (or another directory if ui-paths.json scopes it elsewhere). Install dependencies and start the dev server
|
|
135
|
+
3. For this repo, UI lives in `site/` (or another directory if ui-paths.json scopes it elsewhere). Install dependencies and start the dev server.
|
|
136
|
+
|
|
137
|
+
**Capture command results safely:** redirect output to a file and check the exit code — `<command> > /tmp/step.log 2>&1; echo "EXIT=$?"` — and never pipe the run through `| tail` or `| head`. A pipe reports the *last* command's exit status, so a failing command reads as success.
|
|
138
|
+
|
|
139
|
+
**Disable dev-only overlay UI before the server starts.** A baseline must contain only what ships. Astro's dev server injects a dev toolbar over the bottom-centre of every page: it never reaches production, it occludes the exact region a bottom-of-page regression appears in, and it couples every baseline to the Astro version that drew it. It is in the DOM too, so the axe pass in step 8c would report the toolbar's own violations as the site's.
|
|
140
|
+
|
|
88
141
|
```bash
|
|
89
142
|
cd "$WT/site"
|
|
90
|
-
npm ci
|
|
143
|
+
npm ci > /tmp/ui-npm-ci.log 2>&1; echo "EXIT=$?"
|
|
144
|
+
# EXIT must be 0 before you go on. On any other value, read /tmp/ui-npm-ci.log
|
|
145
|
+
# to see why and return verdict `escalate` — do NOT continue with a broken install.
|
|
146
|
+
npx astro preferences disable devToolbar > /tmp/ui-devtoolbar.log 2>&1; echo "EXIT=$?"
|
|
147
|
+
# Non-zero EXIT means this UI directory is not an Astro project — see below.
|
|
91
148
|
npm run dev -- --port={{preview_port}} &
|
|
92
149
|
SERVER_PID=$!
|
|
93
150
|
```
|
|
94
151
|
|
|
95
|
-
|
|
152
|
+
`astro preferences disable devToolbar` is **project-scoped** by default: it writes into `$WT/site/.astro/`, which step 13 deletes along with the worktree, so it turns the toolbar off for this capture alone and nothing outside this run changes. Never pass `--global` — that writes to the operator's home directory and silently changes every other Astro project on the machine. Disable it before backgrounding the server, not after: Astro decides whether to inject the toolbar when the dev server boots.
|
|
153
|
+
|
|
154
|
+
If that command exits non-zero, this UI directory is **not an Astro project**. Disable that toolchain's own dev overlay instead. If it has none you can turn off, emit an `info` finding recording that the baseline may contain dev-only UI rather than capturing a contaminated one silently.
|
|
155
|
+
|
|
156
|
+
Apply the same capture rule to your driver run and to every other command whose success you judge:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
node "$DRIVER" > /tmp/ui-driver.log 2>&1; echo "EXIT=$?"
|
|
160
|
+
```
|
|
96
161
|
|
|
97
|
-
4. Wait up to 30s for `http://localhost:{{preview_port}}/` to respond 200. If the server fails to start in 30s, kill
|
|
162
|
+
4. Wait up to 30s for `http://localhost:{{preview_port}}/` to respond 200. If the server fails to start in 30s, run teardown (step 13) — `kill $SERVER_PID`, never a command-line pattern — and return verdict `escalate`.
|
|
98
163
|
|
|
99
164
|
5. Determine the site base path:
|
|
100
165
|
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.)
|
|
@@ -108,7 +173,7 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
|
|
|
108
173
|
- Use only the returned `routes` list for the browser passes below.
|
|
109
174
|
|
|
110
175
|
7. **Verify browser binaries** — before starting any browser session:
|
|
111
|
-
-
|
|
176
|
+
- Export `PLAYWRIGHT_BROWSERS_PATH` first if you have not already (see "Playwright cache"), then check each engine in `{{ui_review_config}}.browsers` against `$PLAYWRIGHT_BROWSERS_PATH`.
|
|
112
177
|
- Collect all missing engines.
|
|
113
178
|
- If any engine is missing, call `buildBrowserEscalationFinding(engine, process.platform)` for each, teardown the worktree (step 13), and return `verdict: "escalate"` with those findings.
|
|
114
179
|
|
|
@@ -118,7 +183,7 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
|
|
|
118
183
|
|
|
119
184
|
b. **Visual-diff pass (when `visualDiff.enabled` is true):**
|
|
120
185
|
|
|
121
|
-
**Visual diffing is ONLY `compareVisual` (pixelmatch). There is no ImageMagick.** Never shell out to `convert`, `compare`, `magick`, or any external image tool to diff or convert images — they are not installed and not a dependency. Screenshot via Playwright (`page.screenshot`) → pass the buffer to `compareVisual` (`lib/visual-diff.ts`). If `compareVisual` is hard to invoke, your driver
|
|
186
|
+
**Visual diffing is ONLY `compareVisual` (pixelmatch). There is no ImageMagick.** Never shell out to `convert`, `compare`, `magick`, or any external image tool to diff or convert images — they are not installed and not a dependency. Screenshot via Playwright (`page.screenshot`) → pass the buffer to `compareVisual` (`lib/visual-diff.ts`). If `compareVisual` is hard to invoke, your driver failed to import it — fix the import anchor (see "Resolving your dependencies"), do **not** substitute an external tool.
|
|
122
187
|
|
|
123
188
|
For each route in the (capped) route list × each viewport in `{{ui_review_config}}.viewports`:
|
|
124
189
|
- Set Playwright viewport to `{ width, height }` from the config.
|
|
@@ -147,10 +212,10 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
|
|
|
147
212
|
- Set Playwright viewport to `{ width, height }`.
|
|
148
213
|
- For each route in the (capped) route list:
|
|
149
214
|
- Navigate.
|
|
150
|
-
- Inject and run axe-core:
|
|
215
|
+
- Inject and run axe-core. `axe` is the plugin-root-anchored `require('axe-core')` from "Resolving your dependencies"; axe runs **inside the page**, so inject its source and evaluate there:
|
|
151
216
|
```javascript
|
|
152
|
-
|
|
153
|
-
const results = await axe.run(document);
|
|
217
|
+
await page.addScriptTag({ content: axe.source });
|
|
218
|
+
const results = await page.evaluate(async () => await window.axe.run(document));
|
|
154
219
|
```
|
|
155
220
|
- Collect each violation as a raw tuple: `{ viewport, ruleId, target, impact, message, helpUrl }` (from `axe.run` output).
|
|
156
221
|
|
|
@@ -172,11 +237,13 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
|
|
|
172
237
|
12. **Write ui-review state sidecar** — after all browser passes complete and before teardown, determine whether any `compareVisual` call returned `new-baseline` or `dimension-mismatch` across all routes, viewports, and browsers in this run.
|
|
173
238
|
|
|
174
239
|
- If **yes**: write `{{repo_root}}/.cloverleaf/runs/{{taskId}}/ui-review/state.json` containing:
|
|
240
|
+
<!-- cloverleaf-schema: none -->
|
|
175
241
|
```json
|
|
176
242
|
{"baselines_pending": true}
|
|
177
243
|
```
|
|
178
244
|
(Create intermediate directories as needed.)
|
|
179
245
|
- If **no**: write `{{repo_root}}/.cloverleaf/runs/{{taskId}}/ui-review/state.json` containing:
|
|
246
|
+
<!-- cloverleaf-schema: none -->
|
|
180
247
|
```json
|
|
181
248
|
{"baselines_pending": false}
|
|
182
249
|
```
|
|
@@ -186,10 +253,15 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
|
|
|
186
253
|
13. Teardown:
|
|
187
254
|
```bash
|
|
188
255
|
kill $SERVER_PID 2>/dev/null || true
|
|
256
|
+
rm -f "$DRIVER"
|
|
189
257
|
cd {{repo_root}}
|
|
190
258
|
git worktree remove --force "$WT"
|
|
191
259
|
```
|
|
192
260
|
|
|
261
|
+
Kill the server by the PID you captured in step 3, never by command-line pattern. `pkill -f "astro dev"` also matches the command line of the shell running it, so the shell kills itself: exit 144, and every command after it in the same compound statement — the `rm -f` and `git worktree remove` above — silently never runs.
|
|
262
|
+
|
|
263
|
+
Delete every driver script you wrote, wherever you put it. `git worktree remove --force` only clears what lives inside `$WT`; a driver written anywhere else survives the run and leaks into the next one.
|
|
264
|
+
|
|
193
265
|
## Tool constraints
|
|
194
266
|
|
|
195
267
|
- Read-only for source files and tests.
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloverleaf-approve-baselines
|
|
3
|
-
description: Human baseline-approval gate for the Cloverleaf
|
|
3
|
+
description: Human baseline-approval gate for the Cloverleaf delivery council. When the UI member captures new or resized visual baselines it sets baselines_pending=true in .cloverleaf/runs/{taskId}/ui-review/state.json, and the council runner holds the council pass until the baselines are approved. This skill is clear-only — it presents the new baseline images, records human approval, and clears baselines_pending (it drives no FSM transition). Re-run /cloverleaf-run <TASK-ID> afterwards so the delivery council can pass. Usage — /cloverleaf-approve-baselines <TASK-ID>.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cloverleaf — approve-baselines
|
|
7
7
|
|
|
8
8
|
## Trigger condition
|
|
9
9
|
|
|
10
|
-
This skill is invoked **only** when the `
|
|
10
|
+
This skill is invoked **only** when the delivery council's `ui` member reports that `baselines_pending` is `true` — i.e., the UI Reviewer captured at least one `new-baseline` or `dimension-mismatch` result during its run, meaning one or more baseline PNGs under `.cloverleaf/baselines/{browser}/` were created or replaced. The council runner (`/cloverleaf-run` §4.1) holds the council `pass` (which would advance `council → final-gate`) until `baselines_pending` is cleared.
|
|
11
11
|
|
|
12
|
-
Do not run this skill if
|
|
12
|
+
Do not run this skill if `state.json` already has `baselines_pending: false` (there is nothing to approve).
|
|
13
13
|
|
|
14
14
|
## Effect
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
2.
|
|
18
|
-
3. Commits the updated state
|
|
16
|
+
1. Presents the new baseline images and records human approval.
|
|
17
|
+
2. Writes `baselines_pending: false` to `.cloverleaf/runs/{taskId}/ui-review/state.json`.
|
|
18
|
+
3. Commits the updated state to the feature branch.
|
|
19
|
+
|
|
20
|
+
This skill is **clear-only**: it drives **no** FSM transition. Once `baselines_pending` is `false`, re-running `/cloverleaf-run <TASK-ID>` re-dispatches the delivery council; the `ui` member now passes and the council `pass` applies (`council → final-gate`).
|
|
19
21
|
|
|
20
22
|
---
|
|
21
23
|
|
|
@@ -33,19 +35,14 @@ Do not run this skill if the task is not in `ui-review` status or if `state.json
|
|
|
33
35
|
|
|
34
36
|
1. Capture the TASK-ID argument.
|
|
35
37
|
|
|
36
|
-
2. Load the task and
|
|
38
|
+
2. Load the task (context only) and read the ui-review state to confirm there is something to approve:
|
|
37
39
|
```bash
|
|
38
40
|
cloverleaf-cli load-task <repo_root> <TASK-ID>
|
|
39
|
-
```
|
|
40
|
-
Verify `status === "ui-review"`. If not, report and stop.
|
|
41
|
-
|
|
42
|
-
3. Read the current ui-review state:
|
|
43
|
-
```bash
|
|
44
41
|
cloverleaf-cli read-ui-review-state <repo_root> <TASK-ID>
|
|
45
42
|
```
|
|
46
|
-
|
|
43
|
+
Verify `baselines_pending === true`. If it is already `false` (or the state file is absent), report that no approval is needed and stop. (This skill gates on the `baselines_pending` flag, **not** on task `status` — under the collapsed council FSM the task sits at `council` while the UI member's baselines await approval.)
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
3. Present the new baseline images to the human for review. The baselines live at:
|
|
49
46
|
```
|
|
50
47
|
<repo_root>/.cloverleaf/baselines/{browser}/{slug}-{viewport}.png
|
|
51
48
|
```
|
|
@@ -55,25 +52,20 @@ Do not run this skill if the task is not in `ui-review` status or if `state.json
|
|
|
55
52
|
```
|
|
56
53
|
Display the list. Ask the human to confirm they have reviewed the images and approve the baselines before proceeding.
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
4. Once approved, clear the flag — write `baselines_pending: false`:
|
|
59
56
|
```bash
|
|
60
57
|
cloverleaf-cli write-ui-review-state <repo_root> <TASK-ID> false
|
|
61
58
|
```
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
cloverleaf-cli advance-status <repo_root> <TASK-ID> qa agent '' full_pipeline
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
7. Commit the changes to the feature branch:
|
|
60
|
+
5. Commit the changes to the feature branch:
|
|
69
61
|
```bash
|
|
70
62
|
cd <repo_root>
|
|
71
63
|
git add .cloverleaf/
|
|
72
|
-
git commit -m "cloverleaf: <TASK-ID> baselines approved
|
|
64
|
+
git commit -m "cloverleaf: <TASK-ID> baselines approved (baselines_pending cleared)"
|
|
73
65
|
```
|
|
74
66
|
|
|
75
|
-
|
|
76
|
-
> "✓ Baselines approved
|
|
67
|
+
6. Report:
|
|
68
|
+
> "✓ Baselines approved (baselines_pending cleared). Re-run `/cloverleaf-run <TASK-ID>` so the delivery council can now pass."
|
|
77
69
|
|
|
78
70
|
---
|
|
79
71
|
|
|
@@ -81,5 +73,5 @@ Do not run this skill if the task is not in `ui-review` status or if `state.json
|
|
|
81
73
|
|
|
82
74
|
- Never push.
|
|
83
75
|
- Do not modify source code or test files.
|
|
84
|
-
-
|
|
85
|
-
-
|
|
76
|
+
- This skill is **clear-only** — it drives **no** FSM transition (no `advance-status`). It only clears the `baselines_pending` flag; the council runner re-runs the council once the flag is `false`.
|
|
77
|
+
- Do not skip step 3 — the human must acknowledge the baseline images before approval is recorded.
|