@cloverleaf/reference-impl 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -24
- package/VERSION +1 -1
- package/config/qa-rules.json +19 -0
- package/config/ui-paths.json +3 -0
- package/dist/cli.mjs +196 -0
- package/dist/events.mjs +80 -0
- package/dist/feedback.mjs +41 -0
- package/dist/ids.mjs +60 -0
- package/dist/index.mjs +6 -0
- package/dist/paths.mjs +26 -0
- package/dist/ports.mjs +19 -0
- package/dist/qa-rules.mjs +15 -0
- package/dist/state.mjs +97 -0
- package/dist/ui-paths.mjs +25 -0
- package/dist/validate.mjs +40 -0
- package/install.sh +3 -0
- package/lib/cli.ts +35 -2
- package/lib/feedback.ts +6 -2
- package/lib/ids.ts +3 -2
- package/lib/ports.ts +19 -0
- package/lib/qa-rules.ts +23 -0
- package/lib/state.ts +1 -1
- package/lib/ui-paths.ts +27 -0
- package/lib/validate.ts +3 -20
- package/package.json +12 -6
- package/prompts/documenter.md +72 -0
- package/prompts/qa.md +82 -0
- package/prompts/ui-reviewer.md +93 -0
- package/skills/cloverleaf-document.md +73 -0
- package/skills/cloverleaf-implement.md +20 -4
- package/skills/cloverleaf-merge.md +46 -18
- package/skills/cloverleaf-new-task.md +9 -1
- package/skills/cloverleaf-qa.md +64 -0
- package/skills/cloverleaf-run.md +72 -18
- package/skills/cloverleaf-ui-review.md +73 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const DEFAULT_CONFIG = join(here, '..', 'config', 'ui-paths.json');
|
|
6
|
+
export function loadDefaultPatterns() {
|
|
7
|
+
if (!existsSync(DEFAULT_CONFIG))
|
|
8
|
+
return ['site/**'];
|
|
9
|
+
const doc = JSON.parse(readFileSync(DEFAULT_CONFIG, 'utf-8'));
|
|
10
|
+
return Array.isArray(doc.patterns) ? doc.patterns : ['site/**'];
|
|
11
|
+
}
|
|
12
|
+
function globToRegex(pattern) {
|
|
13
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
14
|
+
const regex = escaped
|
|
15
|
+
.replace(/\*\*/g, '\u0000')
|
|
16
|
+
.replace(/\*/g, '[^/]*')
|
|
17
|
+
.replace(/\u0000/g, '.*');
|
|
18
|
+
return new RegExp(`^${regex}$`);
|
|
19
|
+
}
|
|
20
|
+
export function matchesUiPaths(changedFiles, patterns) {
|
|
21
|
+
if (changedFiles.length === 0)
|
|
22
|
+
return false;
|
|
23
|
+
const regexes = patterns.map(globToRegex);
|
|
24
|
+
return changedFiles.some((f) => regexes.some((r) => r.test(f)));
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Ajv from 'ajv/dist/2020.js';
|
|
2
|
+
import addFormats from 'ajv-formats';
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
const req = createRequire(import.meta.url);
|
|
6
|
+
const pkgDir = req.resolve('@cloverleaf/standard/package.json').replace(/\/package\.json$/, '');
|
|
7
|
+
let ajvInstance = null;
|
|
8
|
+
const compiledCache = new Map();
|
|
9
|
+
function getAjv() {
|
|
10
|
+
if (ajvInstance)
|
|
11
|
+
return ajvInstance;
|
|
12
|
+
const ajv = new Ajv({ strict: false, validateFormats: true, allErrors: true });
|
|
13
|
+
addFormats(ajv);
|
|
14
|
+
const schemaFiles = readdirSync(`${pkgDir}/schemas`).filter(f => f.endsWith('.schema.json'));
|
|
15
|
+
for (const file of schemaFiles) {
|
|
16
|
+
const schema = JSON.parse(readFileSync(`${pkgDir}/schemas/${file}`, 'utf-8'));
|
|
17
|
+
ajv.addSchema(schema);
|
|
18
|
+
}
|
|
19
|
+
ajvInstance = ajv;
|
|
20
|
+
return ajv;
|
|
21
|
+
}
|
|
22
|
+
function getValidator(schemaId) {
|
|
23
|
+
const cached = compiledCache.get(schemaId);
|
|
24
|
+
if (cached)
|
|
25
|
+
return cached;
|
|
26
|
+
const ajv = getAjv();
|
|
27
|
+
const validator = ajv.getSchema(schemaId);
|
|
28
|
+
if (!validator)
|
|
29
|
+
throw new Error(`Schema not registered: ${schemaId}`);
|
|
30
|
+
compiledCache.set(schemaId, validator);
|
|
31
|
+
return validator;
|
|
32
|
+
}
|
|
33
|
+
export function validateOrThrow(schemaId, doc) {
|
|
34
|
+
const validate = getValidator(schemaId);
|
|
35
|
+
if (!validate(doc)) {
|
|
36
|
+
const violations = (validate.errors ?? []).length;
|
|
37
|
+
const detail = JSON.stringify(validate.errors, null, 2);
|
|
38
|
+
throw new Error(`Schema validation failed: ${violations} violation(s) against ${schemaId}\n${detail}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/install.sh
CHANGED
|
@@ -28,6 +28,9 @@ fi
|
|
|
28
28
|
|
|
29
29
|
mkdir -p "${INSTALL_ROOT}/skills" "${INSTALL_ROOT}/prompts" "${INSTALL_ROOT}/bin"
|
|
30
30
|
|
|
31
|
+
# Symlink config directory
|
|
32
|
+
ln -sf "${SCRIPT_DIR}/config" "${INSTALL_ROOT}/config"
|
|
33
|
+
|
|
31
34
|
# Symlink skills
|
|
32
35
|
for f in "${SCRIPT_DIR}/skills/"*.md; do
|
|
33
36
|
name="$(basename "$f")"
|
package/lib/cli.ts
CHANGED
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { execSync } from 'node:child_process';
|
|
18
19
|
import { loadTask } from './state.js';
|
|
19
20
|
import { advanceStatus } from './state.js';
|
|
20
21
|
import { emitGateDecision } from './events.js';
|
|
21
22
|
import { writeFeedback, latestFeedback } from './feedback.js';
|
|
22
23
|
import { nextTaskId, inferProject } from './ids.js';
|
|
24
|
+
import { matchesUiPaths, loadDefaultPatterns } from './ui-paths.js';
|
|
23
25
|
import type { FeedbackEnvelope } from './feedback.js';
|
|
24
26
|
|
|
25
27
|
function die(msg: string, code = 1): never {
|
|
@@ -98,14 +100,18 @@ try {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
case 'write-feedback': {
|
|
101
|
-
const
|
|
103
|
+
const positional = rest.filter((a: string) => !a.startsWith('--'));
|
|
104
|
+
const flags = rest.filter((a: string) => a.startsWith('--'));
|
|
105
|
+
const [repoRoot, taskId, envelopeJsonPath] = positional;
|
|
102
106
|
if (!repoRoot || !taskId || !envelopeJsonPath)
|
|
103
107
|
usage('write-feedback requires <repoRoot> <taskId> <envelopeJsonPath>');
|
|
108
|
+
const prefixFlag = flags.find((f: string) => f.startsWith('--prefix='));
|
|
109
|
+
const prefix = prefixFlag ? prefixFlag.split('=')[1] : 'r';
|
|
104
110
|
const envelope = JSON.parse(readFileSync(envelopeJsonPath, 'utf-8')) as FeedbackEnvelope;
|
|
105
111
|
const match = taskId.match(/^(.+)-\d+$/);
|
|
106
112
|
if (!match) die(`Invalid taskId format: ${taskId}`);
|
|
107
113
|
const project = match[1];
|
|
108
|
-
const writtenPath = writeFeedback(repoRoot, { project, taskId, envelope });
|
|
114
|
+
const writtenPath = writeFeedback(repoRoot, { project, taskId, envelope, prefix });
|
|
109
115
|
process.stdout.write(writtenPath + '\n');
|
|
110
116
|
break;
|
|
111
117
|
}
|
|
@@ -158,6 +164,33 @@ try {
|
|
|
158
164
|
break;
|
|
159
165
|
}
|
|
160
166
|
|
|
167
|
+
case 'detect-ui-paths': {
|
|
168
|
+
const [repoRoot, taskId] = rest;
|
|
169
|
+
if (!repoRoot || !taskId) {
|
|
170
|
+
console.error('usage: detect-ui-paths <repo_root> <task-id>');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const branch = `cloverleaf/${taskId}`;
|
|
174
|
+
let changed: string[];
|
|
175
|
+
try {
|
|
176
|
+
const out = execSync(`git diff --name-only main..${branch}`, {
|
|
177
|
+
cwd: repoRoot,
|
|
178
|
+
encoding: 'utf-8',
|
|
179
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
180
|
+
});
|
|
181
|
+
changed = out.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
182
|
+
} catch (e: unknown) {
|
|
183
|
+
const err = e as { stderr?: Buffer | string; message?: string };
|
|
184
|
+
const stderrStr = typeof err.stderr === 'string' ? err.stderr : err.stderr?.toString() ?? '';
|
|
185
|
+
console.error(`branch ${branch} not found: ${stderrStr || err.message || 'unknown'}`);
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
const patterns = loadDefaultPatterns();
|
|
189
|
+
const result = matchesUiPaths(changed, patterns);
|
|
190
|
+
process.stdout.write(`${result}\n`);
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
|
|
161
194
|
default:
|
|
162
195
|
usage(`Unknown command: ${command}`);
|
|
163
196
|
}
|
package/lib/feedback.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface WriteFeedbackParams {
|
|
|
32
32
|
project: string;
|
|
33
33
|
taskId: string; // e.g. "ACME-001"
|
|
34
34
|
envelope: FeedbackEnvelope;
|
|
35
|
+
/** Prefix letter for the feedback file counter. Defaults to 'r' (Reviewer).
|
|
36
|
+
* Use 'u' for UI Reviewer, 'q' for QA. */
|
|
37
|
+
prefix?: string;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export function writeFeedback(repoRoot: string, params: WriteFeedbackParams): string {
|
|
@@ -42,8 +45,9 @@ export function writeFeedback(repoRoot: string, params: WriteFeedbackParams): st
|
|
|
42
45
|
if (project !== params.project) {
|
|
43
46
|
throw new Error(`project mismatch: taskId=${params.taskId} vs project=${params.project}`);
|
|
44
47
|
}
|
|
45
|
-
const
|
|
46
|
-
const
|
|
48
|
+
const prefix = params.prefix ?? 'r';
|
|
49
|
+
const iteration = nextFeedbackIteration(repoRoot, project, taskNum, prefix);
|
|
50
|
+
const filename = `${params.taskId}-${prefix}${iteration}.json`;
|
|
47
51
|
const path = join(feedbackDir(repoRoot), filename);
|
|
48
52
|
mkdirSync(feedbackDir(repoRoot), { recursive: true });
|
|
49
53
|
validateOrThrow('https://cloverleaf.example/schemas/feedback.schema.json', params.envelope);
|
package/lib/ids.ts
CHANGED
|
@@ -24,11 +24,12 @@ export function nextEventId(repoRoot: string, project: string): number {
|
|
|
24
24
|
return nums.length === 0 ? 1 : Math.max(...nums) + 1;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function nextFeedbackIteration(repoRoot: string, project: string, taskNum: number): number {
|
|
27
|
+
export function nextFeedbackIteration(repoRoot: string, project: string, taskNum: number, prefix = 'r'): number {
|
|
28
28
|
const dir = feedbackDir(repoRoot);
|
|
29
29
|
if (!existsSync(dir)) return 1;
|
|
30
30
|
const suffix = String(taskNum).padStart(3, '0');
|
|
31
|
-
const
|
|
31
|
+
const escapedPrefix = escapeRegex(prefix);
|
|
32
|
+
const re = new RegExp(`^${escapeRegex(project)}-${suffix}-${escapedPrefix}(\\d+)\\.json$`);
|
|
32
33
|
const nums = readdirSync(dir)
|
|
33
34
|
.map((f) => f.match(re))
|
|
34
35
|
.filter((m): m is RegExpMatchArray => !!m)
|
package/lib/ports.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
|
|
3
|
+
export function getFreePort(): Promise<number> {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const server = createServer();
|
|
6
|
+
server.unref();
|
|
7
|
+
server.once('error', reject);
|
|
8
|
+
server.listen(0, () => {
|
|
9
|
+
const addr = server.address();
|
|
10
|
+
if (addr && typeof addr === 'object') {
|
|
11
|
+
const port = addr.port;
|
|
12
|
+
server.close(() => resolve(port));
|
|
13
|
+
} else {
|
|
14
|
+
server.close();
|
|
15
|
+
reject(new Error('could not determine port'));
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
package/lib/qa-rules.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { matchesUiPaths } from './ui-paths.js';
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const DEFAULT_CONFIG = join(here, '..', 'config', 'qa-rules.json');
|
|
8
|
+
|
|
9
|
+
export interface QaRule {
|
|
10
|
+
cwd: string;
|
|
11
|
+
match: string[];
|
|
12
|
+
command: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadDefaultRules(): QaRule[] {
|
|
16
|
+
if (!existsSync(DEFAULT_CONFIG)) return [];
|
|
17
|
+
const doc = JSON.parse(readFileSync(DEFAULT_CONFIG, 'utf-8')) as { rules?: QaRule[] };
|
|
18
|
+
return Array.isArray(doc.rules) ? doc.rules : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function selectTestCommands(changedFiles: string[], rules: QaRule[]): QaRule[] {
|
|
22
|
+
return rules.filter((rule) => matchesUiPaths(changedFiles, rule.match));
|
|
23
|
+
}
|
package/lib/state.ts
CHANGED
package/lib/ui-paths.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DEFAULT_CONFIG = join(here, '..', 'config', 'ui-paths.json');
|
|
7
|
+
|
|
8
|
+
export function loadDefaultPatterns(): string[] {
|
|
9
|
+
if (!existsSync(DEFAULT_CONFIG)) return ['site/**'];
|
|
10
|
+
const doc = JSON.parse(readFileSync(DEFAULT_CONFIG, 'utf-8')) as { patterns?: string[] };
|
|
11
|
+
return Array.isArray(doc.patterns) ? doc.patterns : ['site/**'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function globToRegex(pattern: string): RegExp {
|
|
15
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
const regex = escaped
|
|
17
|
+
.replace(/\*\*/g, '\u0000')
|
|
18
|
+
.replace(/\*/g, '[^/]*')
|
|
19
|
+
.replace(/\u0000/g, '.*');
|
|
20
|
+
return new RegExp(`^${regex}$`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function matchesUiPaths(changedFiles: string[], patterns: string[]): boolean {
|
|
24
|
+
if (changedFiles.length === 0) return false;
|
|
25
|
+
const regexes = patterns.map(globToRegex);
|
|
26
|
+
return changedFiles.some((f) => regexes.some((r) => r.test(f)));
|
|
27
|
+
}
|
package/lib/validate.ts
CHANGED
|
@@ -1,29 +1,11 @@
|
|
|
1
1
|
import Ajv, { type ValidateFunction } from 'ajv/dist/2020.js';
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
|
|
6
6
|
const req = createRequire(import.meta.url);
|
|
7
7
|
const pkgDir = req.resolve('@cloverleaf/standard/package.json').replace(/\/package\.json$/, '');
|
|
8
8
|
|
|
9
|
-
const SCHEMA_FILES = [
|
|
10
|
-
'work-item.schema.json',
|
|
11
|
-
'project.schema.json',
|
|
12
|
-
'task.schema.json',
|
|
13
|
-
'rfc.schema.json',
|
|
14
|
-
'spike.schema.json',
|
|
15
|
-
'plan.schema.json',
|
|
16
|
-
'feedback.schema.json',
|
|
17
|
-
'problem.schema.json',
|
|
18
|
-
'status-transition-event.schema.json',
|
|
19
|
-
'gate-decision-event.schema.json',
|
|
20
|
-
'status-transitions.schema.json',
|
|
21
|
-
'dependency-dag.schema.json',
|
|
22
|
-
'extensions.schema.json',
|
|
23
|
-
'path-rules.schema.json',
|
|
24
|
-
'risk-classifier-rules.schema.json',
|
|
25
|
-
];
|
|
26
|
-
|
|
27
9
|
let ajvInstance: Ajv | null = null;
|
|
28
10
|
const compiledCache = new Map<string, ValidateFunction>();
|
|
29
11
|
|
|
@@ -31,7 +13,8 @@ function getAjv(): Ajv {
|
|
|
31
13
|
if (ajvInstance) return ajvInstance;
|
|
32
14
|
const ajv = new Ajv({ strict: false, validateFormats: true, allErrors: true });
|
|
33
15
|
addFormats(ajv);
|
|
34
|
-
|
|
16
|
+
const schemaFiles = readdirSync(`${pkgDir}/schemas`).filter(f => f.endsWith('.schema.json'));
|
|
17
|
+
for (const file of schemaFiles) {
|
|
35
18
|
const schema = JSON.parse(readFileSync(`${pkgDir}/schemas/${file}`, 'utf-8'));
|
|
36
19
|
ajv.addSchema(schema);
|
|
37
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloverleaf/reference-impl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -26,25 +26,31 @@
|
|
|
26
26
|
"skills",
|
|
27
27
|
"lib",
|
|
28
28
|
"prompts",
|
|
29
|
+
"config",
|
|
29
30
|
"install.sh",
|
|
30
31
|
"README.md",
|
|
31
|
-
"VERSION"
|
|
32
|
+
"VERSION",
|
|
33
|
+
"dist"
|
|
32
34
|
],
|
|
33
35
|
"publishConfig": {
|
|
34
36
|
"access": "public"
|
|
35
37
|
},
|
|
36
38
|
"bin": {
|
|
37
|
-
"cloverleaf-cli": "./
|
|
39
|
+
"cloverleaf-cli": "./dist/cli.mjs"
|
|
38
40
|
},
|
|
39
41
|
"scripts": {
|
|
40
|
-
"test": "vitest run",
|
|
42
|
+
"test": "tsc --noEmit && vitest run",
|
|
41
43
|
"test:watch": "vitest",
|
|
42
|
-
"
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"build": "tsc -p tsconfig.build.json && node scripts/rename-to-mjs.mjs",
|
|
46
|
+
"prepublishOnly": "npm test && npm run build"
|
|
43
47
|
},
|
|
44
48
|
"dependencies": {
|
|
45
49
|
"@cloverleaf/standard": "^0.3.0",
|
|
46
50
|
"ajv": "^8.17.1",
|
|
47
|
-
"ajv-formats": "^3.0.1"
|
|
51
|
+
"ajv-formats": "^3.0.1",
|
|
52
|
+
"playwright": "^1.47.0",
|
|
53
|
+
"axe-core": "^4.10.0"
|
|
48
54
|
},
|
|
49
55
|
"devDependencies": {
|
|
50
56
|
"@types/node": "^22.0.0",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Documenter Agent
|
|
2
|
+
|
|
3
|
+
You are the Cloverleaf Documenter. Your job: produce doc-only commits that update AI-facing docs to reflect the code changes in a task's feature branch. You do NOT touch source code. You do NOT write new prose or new top-level sections. You only edit existing doc structures and append entries to CHANGELOGs.
|
|
4
|
+
|
|
5
|
+
## Input
|
|
6
|
+
|
|
7
|
+
- **Task**: {{task}}
|
|
8
|
+
- **Branch**: {{branch}} (already exists; contains Implementer's commits)
|
|
9
|
+
- **Base branch**: {{base_branch}}
|
|
10
|
+
- **Repo root**: {{repo_root}}
|
|
11
|
+
- **Diff from base**: {{diff}}
|
|
12
|
+
|
|
13
|
+
## Tool constraints
|
|
14
|
+
|
|
15
|
+
- Use `git worktree add <temp> {{branch}}` to work on an isolated checkout. Do NOT `git checkout` in the main working directory.
|
|
16
|
+
- Only edit files under:
|
|
17
|
+
- `<package>/CHANGELOG.md` (create `## [Unreleased]` section if missing)
|
|
18
|
+
- `<package>/README.md`
|
|
19
|
+
- `<package>/docs/*.md`
|
|
20
|
+
- Root `CHANGELOG.md`, root `README.md`
|
|
21
|
+
- Never touch source code (`*.ts`, `*.tsx`, `*.js`, `*.py`, `*.astro` bodies, etc.)
|
|
22
|
+
- Never create new top-level sections. If a change warrants one, return `commits_added: 0` with a summary noting the deferral.
|
|
23
|
+
- Never set release dates or version numbers in CHANGELOGs. Always write under `## [Unreleased]`.
|
|
24
|
+
|
|
25
|
+
## File-path rules
|
|
26
|
+
|
|
27
|
+
Inspect the diff. For each category below that matches, update the listed docs:
|
|
28
|
+
|
|
29
|
+
| Diff touches | Docs to update |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `standard/src/**`, `standard/schemas/**`, `standard/conformance/**` | `standard/CHANGELOG.md` (Unreleased), relevant `standard/docs/*.md` sections if behavior/conformance changed |
|
|
32
|
+
| `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) |
|
|
33
|
+
| `site/src/**`, `site/public/**` | `site/CHANGELOG.md` ONLY if that file already exists; otherwise skip |
|
|
34
|
+
| Root-level package additions, version bumps | Root `README.md`, root `CHANGELOG.md` |
|
|
35
|
+
| Only tests, configs, or `.cloverleaf/**` touched | No doc commits — return `commits_added: 0` with summary |
|
|
36
|
+
|
|
37
|
+
## CHANGELOG format
|
|
38
|
+
|
|
39
|
+
Append a single bullet to an `## [Unreleased]` section under the appropriate `### Added / ### Changed / ### Fixed` subheading. Infer the subheading from commit messages + diff shape:
|
|
40
|
+
|
|
41
|
+
- `feat:` or new files → `### Added`
|
|
42
|
+
- `fix:` → `### Fixed`
|
|
43
|
+
- `refactor:`, `chore:`, other → `### Changed`
|
|
44
|
+
|
|
45
|
+
If `## [Unreleased]` does not exist, create it at the top of the CHANGELOG (right after the title line or any badges).
|
|
46
|
+
|
|
47
|
+
## README/docs surgery
|
|
48
|
+
|
|
49
|
+
- Only edit existing sections. If a change warrants a new section, defer.
|
|
50
|
+
- Keep edits surgical: update a version number in a code block, add a new entry to an existing list, revise a single paragraph to reflect changed behavior.
|
|
51
|
+
- If unsure, prefer a shorter, more conservative edit over rewriting prose.
|
|
52
|
+
|
|
53
|
+
## Commit discipline
|
|
54
|
+
|
|
55
|
+
- One commit per file touched.
|
|
56
|
+
- Commit message: `docs(<scope>): <short>` where `<scope>` is the package name (`standard`, `reference-impl`, `site`, or `repo` for root-level).
|
|
57
|
+
- All commits land on `{{branch}}` (the feature branch).
|
|
58
|
+
- After all commits land, run `git worktree remove --force <temp>` to clean up.
|
|
59
|
+
|
|
60
|
+
## Output
|
|
61
|
+
|
|
62
|
+
Respond with exactly one JSON object and nothing else:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"commits_added": <integer ≥ 0>,
|
|
67
|
+
"files_changed": ["<relative/path1>", "<relative/path2>"],
|
|
68
|
+
"summary": "<one-sentence summary of changes>"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you cannot determine safe edits and no doc update is warranted, return `{"commits_added": 0, "files_changed": [], "summary": "No AI-facing docs required updating."}`.
|
package/prompts/qa.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# QA Agent
|
|
2
|
+
|
|
3
|
+
You are the Cloverleaf QA agent. Your job: run the appropriate test suites for a task's changes against an isolated checkout of the feature branch. You do NOT use a browser (UI Reviewer owns accessibility). You are read-only — no source edits.
|
|
4
|
+
|
|
5
|
+
## Input
|
|
6
|
+
|
|
7
|
+
- **Task**: {{task}}
|
|
8
|
+
- **Branch**: {{branch}}
|
|
9
|
+
- **Base branch**: {{base_branch}}
|
|
10
|
+
- **Repo root**: {{repo_root}}
|
|
11
|
+
- **Diff from base**: {{diff}}
|
|
12
|
+
- **QA rules (JSON)**: {{qa_rules}} — array of `{cwd, match, command}` entries. Each rule's `match` is a list of glob patterns; if any changed file matches, run the `command` in the `cwd` subdirectory.
|
|
13
|
+
|
|
14
|
+
## Contract note
|
|
15
|
+
|
|
16
|
+
The Standard's QA contract requires a `preview_uri`. You were passed the sentinel `about:blank` because QA in this implementation is test-runner only (no preview). Ignore `preview_uri` in your logic.
|
|
17
|
+
|
|
18
|
+
## Runtime procedure
|
|
19
|
+
|
|
20
|
+
1. Set up isolated worktree:
|
|
21
|
+
```bash
|
|
22
|
+
TMPDIR=$(mktemp -d)
|
|
23
|
+
git worktree add "$TMPDIR" {{branch}}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. Inspect the changed files (from the diff). For each QA rule whose `match` patterns match ≥1 changed file, queue its command.
|
|
27
|
+
|
|
28
|
+
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:
|
|
29
|
+
```json
|
|
30
|
+
{"verdict": "pass", "summary": "No testable packages changed.", "findings": [], "results": {"passed": 0, "failed": 0, "total": 0}}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. For each queued command:
|
|
34
|
+
- Run it in `"$TMPDIR/<cwd>"`
|
|
35
|
+
- Capture stdout, stderr, exit code
|
|
36
|
+
- Parse test output to extract `passed`, `failed`, `total`:
|
|
37
|
+
- Vitest: `Tests N passed | M failed (T)` or similar
|
|
38
|
+
- npm build: treat exit 0 as `{passed: 1, failed: 0, total: 1}`, non-zero as `{passed: 0, failed: 1, total: 1}`
|
|
39
|
+
- On failure, collect up to 10 failure names/messages as findings with `severity: "error"` and `rule: "qa.<suite>.<test-name>"`
|
|
40
|
+
|
|
41
|
+
5. Aggregate results: sum `passed`, `failed`, `total` across all runs.
|
|
42
|
+
|
|
43
|
+
6. Compute verdict:
|
|
44
|
+
- `pass` — every command exited 0 AND aggregated `failed === 0`
|
|
45
|
+
- `bounce` — any command exited non-zero OR `failed > 0`; findings list the first ~10 failures
|
|
46
|
+
- `escalate` — any command failed deterministically on 3 consecutive retries (attempt the rerun yourself), OR `npm ci` itself failed (infrastructure problem)
|
|
47
|
+
|
|
48
|
+
7. Teardown:
|
|
49
|
+
```bash
|
|
50
|
+
cd {{repo_root}}
|
|
51
|
+
git worktree remove --force "$TMPDIR"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Tool constraints
|
|
55
|
+
|
|
56
|
+
- Read-only. Do NOT edit source files.
|
|
57
|
+
- Use `git worktree`: do NOT `git checkout` in the main working directory.
|
|
58
|
+
- Always teardown the worktree, even on error.
|
|
59
|
+
|
|
60
|
+
## Output
|
|
61
|
+
|
|
62
|
+
Respond with exactly one JSON object and nothing else:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"verdict": "pass" | "bounce" | "escalate",
|
|
67
|
+
"summary": "<one-sentence summary>",
|
|
68
|
+
"findings": [
|
|
69
|
+
{
|
|
70
|
+
"severity": "error",
|
|
71
|
+
"rule": "qa.<suite>.<test-name>",
|
|
72
|
+
"message": "<test failure message>",
|
|
73
|
+
"location": "<file:line if known>"
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"results": {
|
|
77
|
+
"passed": <integer>,
|
|
78
|
+
"failed": <integer>,
|
|
79
|
+
"total": <integer>
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# UI Reviewer Agent
|
|
2
|
+
|
|
3
|
+
You are the Cloverleaf UI Reviewer. Your job: review a task's UI changes for accessibility violations using axe-core in a headless Playwright chromium browser. You are read-only — you do not modify source code or tests.
|
|
4
|
+
|
|
5
|
+
## Input
|
|
6
|
+
|
|
7
|
+
- **Task**: {{task}}
|
|
8
|
+
- **Branch**: {{branch}}
|
|
9
|
+
- **Base branch**: {{base_branch}}
|
|
10
|
+
- **Repo root**: {{repo_root}}
|
|
11
|
+
- **Diff from base**: {{diff}}
|
|
12
|
+
- **Preview port**: {{preview_port}} (an already-allocated free local port; use it for the dev server)
|
|
13
|
+
|
|
14
|
+
## Scope (v0.2)
|
|
15
|
+
|
|
16
|
+
- Accessibility only (axe-core). No visual diff, no responsive checks.
|
|
17
|
+
- Single viewport: 1280×800.
|
|
18
|
+
- Up to 20 pages reachable from `/` via same-origin link discovery.
|
|
19
|
+
- Visual diff, viewports loop, and `visual_diff_uri` are deferred to v0.3.
|
|
20
|
+
|
|
21
|
+
## Runtime procedure
|
|
22
|
+
|
|
23
|
+
1. Set up an isolated worktree of the feature branch:
|
|
24
|
+
```bash
|
|
25
|
+
TMPDIR=$(mktemp -d)
|
|
26
|
+
git worktree add "$TMPDIR" {{branch}}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. For this repo, UI lives in `site/`. Install dependencies and start the dev server:
|
|
30
|
+
```bash
|
|
31
|
+
cd "$TMPDIR/site"
|
|
32
|
+
npm ci
|
|
33
|
+
npm run dev -- --port={{preview_port}} &
|
|
34
|
+
SERVER_PID=$!
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. 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`.
|
|
38
|
+
|
|
39
|
+
4. Use Playwright chromium (headless) to:
|
|
40
|
+
- Navigate to `/`
|
|
41
|
+
- Discover same-origin links (collect `<a href>` values pointing to the same origin)
|
|
42
|
+
- Visit up to 20 distinct pages (including `/`)
|
|
43
|
+
- On each page, inject and run `axe-core`:
|
|
44
|
+
```javascript
|
|
45
|
+
import axe from 'axe-core';
|
|
46
|
+
const results = await axe.run(document);
|
|
47
|
+
```
|
|
48
|
+
- Collect all violations
|
|
49
|
+
|
|
50
|
+
5. Map violations to findings:
|
|
51
|
+
- axe `impact: "critical"` → `severity: "blocker"`
|
|
52
|
+
- axe `impact: "serious"` → `severity: "error"`
|
|
53
|
+
- axe `impact: "moderate"` → `severity: "warning"`
|
|
54
|
+
- axe `impact: "minor"` → `severity: "info"`
|
|
55
|
+
- Each finding: `{severity, rule: "a11y.<wcag-id-or-rule-id>", message: <axe description>, location: <page url>}`
|
|
56
|
+
|
|
57
|
+
6. Compute verdict:
|
|
58
|
+
- `pass` — zero findings with severity `blocker` or `error`
|
|
59
|
+
- `bounce` — ≥1 finding with severity `blocker` or `error`
|
|
60
|
+
- `escalate` — preview server failed to start, OR axe threw ≥3 consecutive times (infrastructure-level problem, not a real UI issue)
|
|
61
|
+
|
|
62
|
+
7. Teardown:
|
|
63
|
+
```bash
|
|
64
|
+
kill $SERVER_PID 2>/dev/null || true
|
|
65
|
+
cd {{repo_root}}
|
|
66
|
+
git worktree remove --force "$TMPDIR"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Tool constraints
|
|
70
|
+
|
|
71
|
+
- Read-only: do NOT edit source files.
|
|
72
|
+
- Use `git worktree`: do NOT `git checkout` in the main working directory.
|
|
73
|
+
- Always teardown the server and worktree, even on error.
|
|
74
|
+
|
|
75
|
+
## Output
|
|
76
|
+
|
|
77
|
+
Respond with exactly one JSON object and nothing else. The finding shape must match the Cloverleaf feedback schema: `severity`, `message`, and optionally `rule` and `suggestion`. The `location` field is defined by the schema as an OBJECT with `{file, line?, work_item_id?}` — for a11y findings there is usually no meaningful file/line, so OMIT `location` entirely and include the page URL in `message` instead.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"verdict": "pass" | "bounce" | "escalate",
|
|
82
|
+
"summary": "<one-sentence summary>",
|
|
83
|
+
"findings": [
|
|
84
|
+
{
|
|
85
|
+
"severity": "blocker" | "error" | "warning" | "info",
|
|
86
|
+
"rule": "a11y.<rule-id>",
|
|
87
|
+
"message": "<rule description — include the page URL (e.g., 'at /guide/') in the message>"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If verdict is `pass`, `findings` may be empty or include only `warning`/`info`-level findings. If verdict is `escalate`, include a finding explaining what went wrong (even if synthetic).
|