@cloverleaf/reference-impl 0.9.0 → 0.10.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/VERSION +1 -1
- package/config/discovery.json +2 -1
- package/dist/cli.mjs +10 -0
- package/dist/discovery-config.mjs +2 -0
- package/dist/prep-worktree.mjs +51 -36
- package/dist/qa-report.mjs +13 -0
- package/dist/rfc-tasks.mjs +1 -2
- package/lib/cli.ts +10 -0
- package/lib/discovery-config.ts +5 -0
- package/lib/prep-worktree.ts +54 -38
- package/lib/qa-report.ts +15 -0
- package/lib/rfc-tasks.ts +1 -2
- package/package.json +1 -1
- package/prompts/implementer.md +3 -2
- package/prompts/qa.md +18 -10
- package/prompts/reviewer.md +8 -4
- package/skills/cloverleaf-implement/SKILL.md +11 -0
- package/skills/cloverleaf-review/SKILL.md +11 -1
|
@@ -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, release).",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Renato D'Arrigo",
|
|
7
7
|
"email": "renato.darrigo@gmail.com"
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.10.0
|
package/config/discovery.json
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* next-work-item-id <repoRoot> <project>
|
|
32
32
|
* discovery-config --repo-root <repoRoot>
|
|
33
33
|
* prep-worktree <mainRoot> <worktreePath>
|
|
34
|
+
* qa-report <runs.json> <out.html>
|
|
34
35
|
* dag-ready-tasks <repoRoot> <planId> <maxConcurrent>
|
|
35
36
|
* dag-detect-cycle <repoRoot> <planId>
|
|
36
37
|
* walk-state-read <repoRoot> <planId>
|
|
@@ -64,6 +65,7 @@ import { loadSpike, saveSpike, advanceSpikeStatus } from './spike.mjs';
|
|
|
64
65
|
import { loadPlan, savePlan, advancePlanStatus, materialiseTasksFromPlan } from './plan.mjs';
|
|
65
66
|
import { loadDiscoveryConfig } from './discovery-config.mjs';
|
|
66
67
|
import { prepWorktree } from './prep-worktree.mjs';
|
|
68
|
+
import { writeQaReportFromFile } from './qa-report.mjs';
|
|
67
69
|
import { readUiReviewState, writeUiReviewState } from './ui-review-state.mjs';
|
|
68
70
|
import { buildBaselinePath } from './visual-diff.mjs';
|
|
69
71
|
import { computeReadyTasks, detectCycle } from './dag-walker.mjs';
|
|
@@ -110,6 +112,7 @@ function usage(msg) {
|
|
|
110
112
|
' next-work-item-id <repoRoot> <project>\n' +
|
|
111
113
|
' discovery-config --repo-root <repoRoot>\n' +
|
|
112
114
|
' prep-worktree <mainRoot> <worktreePath>\n' +
|
|
115
|
+
' qa-report <runs.json> <out.html>\n' +
|
|
113
116
|
' dag-ready-tasks <repoRoot> <planId> <maxConcurrent>\n' +
|
|
114
117
|
' dag-detect-cycle <repoRoot> <planId>\n' +
|
|
115
118
|
' walk-state-read <repoRoot> <planId>\n' +
|
|
@@ -493,6 +496,13 @@ try {
|
|
|
493
496
|
prepWorktree(mainRoot, worktreePath);
|
|
494
497
|
break;
|
|
495
498
|
}
|
|
499
|
+
case 'qa-report': {
|
|
500
|
+
const [runsJsonPath, outHtmlPath] = rest;
|
|
501
|
+
if (!runsJsonPath || !outHtmlPath)
|
|
502
|
+
usage('qa-report requires <runs.json> <out.html>');
|
|
503
|
+
writeQaReportFromFile(runsJsonPath, outHtmlPath);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
496
506
|
case 'dag-ready-tasks': {
|
|
497
507
|
const [repoRoot, planId, maxConcurrentStr] = rest;
|
|
498
508
|
if (!repoRoot || !planId || !maxConcurrentStr)
|
|
@@ -13,6 +13,7 @@ export function loadDiscoveryConfig(repoRoot) {
|
|
|
13
13
|
prep_copy_dirs: Array.isArray(rawFallback.prep_copy_dirs)
|
|
14
14
|
? rawFallback.prep_copy_dirs.filter((p) => typeof p === 'string')
|
|
15
15
|
: [],
|
|
16
|
+
worktree_setup_command: typeof rawFallback.worktree_setup_command === 'string' ? rawFallback.worktree_setup_command : '',
|
|
16
17
|
};
|
|
17
18
|
if (existsSync(override)) {
|
|
18
19
|
try {
|
|
@@ -33,5 +34,6 @@ function normalise(doc, fallback) {
|
|
|
33
34
|
prep_copy_dirs: Array.isArray(doc.prep_copy_dirs)
|
|
34
35
|
? doc.prep_copy_dirs.filter((p) => typeof p === 'string')
|
|
35
36
|
: fallback.prep_copy_dirs,
|
|
37
|
+
worktree_setup_command: typeof doc.worktree_setup_command === 'string' ? doc.worktree_setup_command : fallback.worktree_setup_command,
|
|
36
38
|
};
|
|
37
39
|
}
|
package/dist/prep-worktree.mjs
CHANGED
|
@@ -87,42 +87,61 @@ function buildMissingNodeModulesError(mainRoot) {
|
|
|
87
87
|
return new Error(`main missing standard/node_modules at ${mainStandardNm} — run \`npm ci\` in main's standard/ first`);
|
|
88
88
|
}
|
|
89
89
|
export function prepWorktree(mainRoot, worktreePath) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
const embedded = existsSync(join(worktreePath, 'standard', 'package.json')) &&
|
|
91
|
+
existsSync(join(worktreePath, 'reference-impl', 'package.json'));
|
|
92
|
+
// configRoot is where .cloverleaf/config/discovery.json is read from. Embedded mode
|
|
93
|
+
// walks up to the primary repo (which holds node_modules + the config); a non-monorepo
|
|
94
|
+
// consumer uses mainRoot directly.
|
|
95
|
+
let configRoot;
|
|
96
|
+
if (embedded) {
|
|
97
|
+
const resolvedMain = findPrimaryRoot(mainRoot);
|
|
98
|
+
if (resolvedMain === null) {
|
|
99
|
+
throw buildMissingNodeModulesError(mainRoot);
|
|
100
|
+
}
|
|
101
|
+
configRoot = resolvedMain;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
configRoot = mainRoot;
|
|
105
|
+
}
|
|
106
|
+
const config = loadDiscoveryConfig(configRoot);
|
|
107
|
+
if (embedded) {
|
|
108
|
+
// Embedded / monorepo mode: prime the cloverleaf TS tooling (unchanged behavior).
|
|
109
|
+
copyEmbeddedArtifacts(configRoot, worktreePath);
|
|
94
110
|
}
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
// Both modes: copy any gitignored dirs the consumer's tests/briefs reference.
|
|
112
|
+
copyPrepDirs(configRoot, config.prep_copy_dirs, worktreePath);
|
|
113
|
+
if (embedded) {
|
|
114
|
+
// Build standard/ fresh from the worktree's own sources.
|
|
115
|
+
execSync('npm run build', {
|
|
116
|
+
cwd: join(worktreePath, 'standard'),
|
|
117
|
+
stdio: 'pipe',
|
|
118
|
+
});
|
|
97
119
|
}
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
// Both modes: run the consumer's worktree setup command, if configured.
|
|
121
|
+
if (config.worktree_setup_command.trim() !== '') {
|
|
122
|
+
execSync(config.worktree_setup_command, {
|
|
123
|
+
cwd: worktreePath,
|
|
124
|
+
stdio: 'pipe',
|
|
125
|
+
});
|
|
103
126
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// leave partial state; we must not trip on it.
|
|
114
|
-
// 2. cpSync with verbatimSymlinks: true does not reliably overwrite an existing
|
|
115
|
-
// symlink at the destination even with force: true (CLV-20 Reviewer repro was
|
|
116
|
-
// EEXIST on vite/node_modules/.bin on second invocation).
|
|
117
|
-
primeCopy(mainStandardNm, wtStandardNm);
|
|
118
|
-
primeCopy(mainRefImplNm, wtRefImplNm);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Embedded / monorepo mode: copy the primary repo's installed cloverleaf TS deps + built
|
|
130
|
+
* dist into the worktree, preserving the @cloverleaf/standard relative symlink. (See the
|
|
131
|
+
* file header for the CLV-16/17/37/52 history.)
|
|
132
|
+
*/
|
|
133
|
+
function copyEmbeddedArtifacts(resolvedMain, worktreePath) {
|
|
134
|
+
primeCopy(join(resolvedMain, 'standard', 'node_modules'), join(worktreePath, 'standard', 'node_modules'));
|
|
135
|
+
primeCopy(join(resolvedMain, 'reference-impl', 'node_modules'), join(worktreePath, 'reference-impl', 'node_modules'));
|
|
119
136
|
primeCopy(join(resolvedMain, 'reference-impl', 'dist'), join(worktreePath, 'reference-impl', 'dist'));
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Honor discovery_config.prep_copy_dirs: copy each listed gitignored directory from
|
|
140
|
+
* configRoot into the worktree. Missing entries warn and are skipped.
|
|
141
|
+
*/
|
|
142
|
+
function copyPrepDirs(configRoot, dirs, worktreePath) {
|
|
143
|
+
for (const dir of dirs) {
|
|
144
|
+
const srcPath = join(configRoot, dir);
|
|
126
145
|
const dstPath = join(worktreePath, dir);
|
|
127
146
|
if (!existsSync(srcPath)) {
|
|
128
147
|
process.stderr.write(`prep-worktree: prep_copy_dirs entry '${dir}' not found at ${srcPath} — skipping.\n`);
|
|
@@ -130,10 +149,6 @@ export function prepWorktree(mainRoot, worktreePath) {
|
|
|
130
149
|
}
|
|
131
150
|
primeCopy(srcPath, dstPath);
|
|
132
151
|
}
|
|
133
|
-
execSync('npm run build', {
|
|
134
|
-
cwd: join(worktreePath, 'standard'),
|
|
135
|
-
stdio: 'pipe',
|
|
136
|
-
});
|
|
137
152
|
}
|
|
138
153
|
function primeCopy(src, dst) {
|
|
139
154
|
if (existsSync(dst)) {
|
package/dist/qa-report.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
1
3
|
function escape(s) {
|
|
2
4
|
if (s === undefined || s === null)
|
|
3
5
|
return '';
|
|
@@ -28,6 +30,17 @@ function renderRow(r) {
|
|
|
28
30
|
</tr>
|
|
29
31
|
`;
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Read a QA runs JSON file (array of QaRunResult), render the HTML report, and write it to
|
|
35
|
+
* outHtmlPath, creating the parent directory if needed. Lets QA write its report via the
|
|
36
|
+
* cloverleaf-cli bin instead of importing a monorepo dist path.
|
|
37
|
+
*/
|
|
38
|
+
export function writeQaReportFromFile(runsJsonPath, outHtmlPath) {
|
|
39
|
+
const runs = JSON.parse(readFileSync(runsJsonPath, 'utf-8'));
|
|
40
|
+
const html = renderQaReport(runs);
|
|
41
|
+
mkdirSync(dirname(outHtmlPath), { recursive: true });
|
|
42
|
+
writeFileSync(outHtmlPath, html, 'utf-8');
|
|
43
|
+
}
|
|
31
44
|
export function renderQaReport(runs) {
|
|
32
45
|
const empty = runs.length === 0
|
|
33
46
|
? `<p class="empty">No runs / results.</p>`
|
package/dist/rfc-tasks.mjs
CHANGED
|
@@ -3,8 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { plansDir, tasksDir, rfcsDir } from './paths.mjs';
|
|
4
4
|
/**
|
|
5
5
|
* A task is "standalone" (RFC-direct) iff it has no parent (absent or null)
|
|
6
|
-
* AND it has a non-empty context.rfc.id.
|
|
7
|
-
* docs/superpowers/specs/2026-05-12-rfc-direct-tasks-design.md §"Discriminator".
|
|
6
|
+
* AND it has a non-empty context.rfc.id.
|
|
8
7
|
*/
|
|
9
8
|
export function isStandaloneTask(task) {
|
|
10
9
|
const parent = task.parent;
|
package/lib/cli.ts
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* next-work-item-id <repoRoot> <project>
|
|
32
32
|
* discovery-config --repo-root <repoRoot>
|
|
33
33
|
* prep-worktree <mainRoot> <worktreePath>
|
|
34
|
+
* qa-report <runs.json> <out.html>
|
|
34
35
|
* dag-ready-tasks <repoRoot> <planId> <maxConcurrent>
|
|
35
36
|
* dag-detect-cycle <repoRoot> <planId>
|
|
36
37
|
* walk-state-read <repoRoot> <planId>
|
|
@@ -66,6 +67,7 @@ import { loadSpike, saveSpike, advanceSpikeStatus, type SpikeDoc } from './spike
|
|
|
66
67
|
import { loadPlan, savePlan, advancePlanStatus, materialiseTasksFromPlan, type PlanDoc } from './plan.js';
|
|
67
68
|
import { loadDiscoveryConfig } from './discovery-config.js';
|
|
68
69
|
import { prepWorktree } from './prep-worktree.js';
|
|
70
|
+
import { writeQaReportFromFile } from './qa-report.js';
|
|
69
71
|
import { readUiReviewState, writeUiReviewState } from './ui-review-state.js';
|
|
70
72
|
import { buildBaselinePath } from './visual-diff.js';
|
|
71
73
|
import { computeReadyTasks, detectCycle } from './dag-walker.js';
|
|
@@ -115,6 +117,7 @@ function usage(msg?: string): never {
|
|
|
115
117
|
' next-work-item-id <repoRoot> <project>\n' +
|
|
116
118
|
' discovery-config --repo-root <repoRoot>\n' +
|
|
117
119
|
' prep-worktree <mainRoot> <worktreePath>\n' +
|
|
120
|
+
' qa-report <runs.json> <out.html>\n' +
|
|
118
121
|
' dag-ready-tasks <repoRoot> <planId> <maxConcurrent>\n' +
|
|
119
122
|
' dag-detect-cycle <repoRoot> <planId>\n' +
|
|
120
123
|
' walk-state-read <repoRoot> <planId>\n' +
|
|
@@ -506,6 +509,13 @@ try {
|
|
|
506
509
|
break;
|
|
507
510
|
}
|
|
508
511
|
|
|
512
|
+
case 'qa-report': {
|
|
513
|
+
const [runsJsonPath, outHtmlPath] = rest;
|
|
514
|
+
if (!runsJsonPath || !outHtmlPath) usage('qa-report requires <runs.json> <out.html>');
|
|
515
|
+
writeQaReportFromFile(runsJsonPath, outHtmlPath);
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
|
|
509
519
|
case 'dag-ready-tasks': {
|
|
510
520
|
const [repoRoot, planId, maxConcurrentStr] = rest;
|
|
511
521
|
if (!repoRoot || !planId || !maxConcurrentStr)
|
package/lib/discovery-config.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface DiscoveryConfig {
|
|
|
10
10
|
projectId: string;
|
|
11
11
|
idStart: number;
|
|
12
12
|
prep_copy_dirs: string[];
|
|
13
|
+
worktree_setup_command: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function loadDiscoveryConfig(repoRoot: string): DiscoveryConfig {
|
|
@@ -22,6 +23,8 @@ export function loadDiscoveryConfig(repoRoot: string): DiscoveryConfig {
|
|
|
22
23
|
prep_copy_dirs: Array.isArray(rawFallback.prep_copy_dirs)
|
|
23
24
|
? (rawFallback.prep_copy_dirs as unknown[]).filter((p): p is string => typeof p === 'string')
|
|
24
25
|
: [],
|
|
26
|
+
worktree_setup_command:
|
|
27
|
+
typeof rawFallback.worktree_setup_command === 'string' ? rawFallback.worktree_setup_command : '',
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
if (existsSync(override)) {
|
|
@@ -43,5 +46,7 @@ function normalise(doc: Partial<DiscoveryConfig>, fallback: DiscoveryConfig): Di
|
|
|
43
46
|
prep_copy_dirs: Array.isArray(doc.prep_copy_dirs)
|
|
44
47
|
? (doc.prep_copy_dirs as unknown[]).filter((p): p is string => typeof p === 'string')
|
|
45
48
|
: fallback.prep_copy_dirs,
|
|
49
|
+
worktree_setup_command:
|
|
50
|
+
typeof doc.worktree_setup_command === 'string' ? doc.worktree_setup_command : fallback.worktree_setup_command,
|
|
46
51
|
};
|
|
47
52
|
}
|
package/lib/prep-worktree.ts
CHANGED
|
@@ -95,48 +95,69 @@ function buildMissingNodeModulesError(mainRoot: string): Error {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export function prepWorktree(mainRoot: string, worktreePath: string): void {
|
|
98
|
-
const
|
|
99
|
-
|
|
98
|
+
const embedded =
|
|
99
|
+
existsSync(join(worktreePath, 'standard', 'package.json')) &&
|
|
100
|
+
existsSync(join(worktreePath, 'reference-impl', 'package.json'));
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
// configRoot is where .cloverleaf/config/discovery.json is read from. Embedded mode
|
|
103
|
+
// walks up to the primary repo (which holds node_modules + the config); a non-monorepo
|
|
104
|
+
// consumer uses mainRoot directly.
|
|
105
|
+
let configRoot: string;
|
|
106
|
+
if (embedded) {
|
|
107
|
+
const resolvedMain = findPrimaryRoot(mainRoot);
|
|
108
|
+
if (resolvedMain === null) {
|
|
109
|
+
throw buildMissingNodeModulesError(mainRoot);
|
|
110
|
+
}
|
|
111
|
+
configRoot = resolvedMain;
|
|
112
|
+
} else {
|
|
113
|
+
configRoot = mainRoot;
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
const config = loadDiscoveryConfig(configRoot);
|
|
117
|
+
|
|
118
|
+
if (embedded) {
|
|
119
|
+
// Embedded / monorepo mode: prime the cloverleaf TS tooling (unchanged behavior).
|
|
120
|
+
copyEmbeddedArtifacts(configRoot, worktreePath);
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
// Both modes: copy any gitignored dirs the consumer's tests/briefs reference.
|
|
124
|
+
copyPrepDirs(configRoot, config.prep_copy_dirs, worktreePath);
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
if (embedded) {
|
|
127
|
+
// Build standard/ fresh from the worktree's own sources.
|
|
128
|
+
execSync('npm run build', {
|
|
129
|
+
cwd: join(worktreePath, 'standard'),
|
|
130
|
+
stdio: 'pipe',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Both modes: run the consumer's worktree setup command, if configured.
|
|
135
|
+
if (config.worktree_setup_command.trim() !== '') {
|
|
136
|
+
execSync(config.worktree_setup_command, {
|
|
137
|
+
cwd: worktreePath,
|
|
138
|
+
stdio: 'pipe',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// EEXIST on vite/node_modules/.bin on second invocation).
|
|
130
|
-
primeCopy(mainStandardNm, wtStandardNm);
|
|
131
|
-
primeCopy(mainRefImplNm, wtRefImplNm);
|
|
143
|
+
/**
|
|
144
|
+
* Embedded / monorepo mode: copy the primary repo's installed cloverleaf TS deps + built
|
|
145
|
+
* dist into the worktree, preserving the @cloverleaf/standard relative symlink. (See the
|
|
146
|
+
* file header for the CLV-16/17/37/52 history.)
|
|
147
|
+
*/
|
|
148
|
+
function copyEmbeddedArtifacts(resolvedMain: string, worktreePath: string): void {
|
|
149
|
+
primeCopy(join(resolvedMain, 'standard', 'node_modules'), join(worktreePath, 'standard', 'node_modules'));
|
|
150
|
+
primeCopy(join(resolvedMain, 'reference-impl', 'node_modules'), join(worktreePath, 'reference-impl', 'node_modules'));
|
|
132
151
|
primeCopy(join(resolvedMain, 'reference-impl', 'dist'), join(worktreePath, 'reference-impl', 'dist'));
|
|
152
|
+
}
|
|
133
153
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Honor discovery_config.prep_copy_dirs: copy each listed gitignored directory from
|
|
156
|
+
* configRoot into the worktree. Missing entries warn and are skipped.
|
|
157
|
+
*/
|
|
158
|
+
function copyPrepDirs(configRoot: string, dirs: string[], worktreePath: string): void {
|
|
159
|
+
for (const dir of dirs) {
|
|
160
|
+
const srcPath = join(configRoot, dir);
|
|
140
161
|
const dstPath = join(worktreePath, dir);
|
|
141
162
|
if (!existsSync(srcPath)) {
|
|
142
163
|
process.stderr.write(`prep-worktree: prep_copy_dirs entry '${dir}' not found at ${srcPath} — skipping.\n`);
|
|
@@ -144,11 +165,6 @@ export function prepWorktree(mainRoot: string, worktreePath: string): void {
|
|
|
144
165
|
}
|
|
145
166
|
primeCopy(srcPath, dstPath);
|
|
146
167
|
}
|
|
147
|
-
|
|
148
|
-
execSync('npm run build', {
|
|
149
|
-
cwd: join(worktreePath, 'standard'),
|
|
150
|
-
stdio: 'pipe',
|
|
151
|
-
});
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
function primeCopy(src: string, dst: string): void {
|
package/lib/qa-report.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
1
4
|
export interface QaRunResult {
|
|
2
5
|
ruleId: string;
|
|
3
6
|
command: string;
|
|
@@ -39,6 +42,18 @@ function renderRow(r: QaRunResult): string {
|
|
|
39
42
|
`;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Read a QA runs JSON file (array of QaRunResult), render the HTML report, and write it to
|
|
47
|
+
* outHtmlPath, creating the parent directory if needed. Lets QA write its report via the
|
|
48
|
+
* cloverleaf-cli bin instead of importing a monorepo dist path.
|
|
49
|
+
*/
|
|
50
|
+
export function writeQaReportFromFile(runsJsonPath: string, outHtmlPath: string): void {
|
|
51
|
+
const runs = JSON.parse(readFileSync(runsJsonPath, 'utf-8')) as QaRunResult[];
|
|
52
|
+
const html = renderQaReport(runs);
|
|
53
|
+
mkdirSync(dirname(outHtmlPath), { recursive: true });
|
|
54
|
+
writeFileSync(outHtmlPath, html, 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
export function renderQaReport(runs: QaRunResult[]): string {
|
|
43
58
|
const empty = runs.length === 0
|
|
44
59
|
? `<p class="empty">No runs / results.</p>`
|
package/lib/rfc-tasks.ts
CHANGED
|
@@ -6,8 +6,7 @@ import type { PlanDoc } from './plan.js';
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A task is "standalone" (RFC-direct) iff it has no parent (absent or null)
|
|
9
|
-
* AND it has a non-empty context.rfc.id.
|
|
10
|
-
* docs/superpowers/specs/2026-05-12-rfc-direct-tasks-design.md §"Discriminator".
|
|
9
|
+
* AND it has a non-empty context.rfc.id.
|
|
11
10
|
*/
|
|
12
11
|
export function isStandaloneTask(task: TaskDoc): boolean {
|
|
13
12
|
const parent = (task as Record<string, unknown>).parent;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloverleaf/reference-impl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Reference implementation of the Cloverleaf methodology as Claude Code skills. Implements the Tight Loop (Implementer + Reviewer).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/prompts/implementer.md
CHANGED
|
@@ -8,6 +8,7 @@ You are the Cloverleaf Implementer agent. Your job: take a Task and produce work
|
|
|
8
8
|
- `feedback`: optional — the most recent feedback envelope from a prior Reviewer bounce. If present, address every finding before re-submitting.
|
|
9
9
|
- `repo_root`: absolute path to the consumer repo.
|
|
10
10
|
- `base_branch`: the branch to branch off (default: `main`).
|
|
11
|
+
- `test_rules`: a JSON object `{ rules: [...] }` whose `rules` is a list of `{cwd, match, command}` entries (from `qa-rules.json`).
|
|
11
12
|
|
|
12
13
|
## Your process
|
|
13
14
|
|
|
@@ -26,7 +27,7 @@ You are the Cloverleaf Implementer agent. Your job: take a Task and produce work
|
|
|
26
27
|
2. If `feedback` is present, re-read each finding; plan how to address them.
|
|
27
28
|
3. Create a new branch named `cloverleaf/<task.id>` from `base_branch` using `git checkout -b cloverleaf/<task.id>`.
|
|
28
29
|
4. Implement the code + tests needed to satisfy every acceptance criterion.
|
|
29
|
-
5. Run the project's test
|
|
30
|
+
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.)
|
|
30
31
|
6. Stage and commit your changes with message `feat: <task.title> [<task.id>]`.
|
|
31
32
|
7. Return a structured JSON result to stdout:
|
|
32
33
|
|
|
@@ -54,5 +55,5 @@ If you cannot complete the task:
|
|
|
54
55
|
- Do NOT open a PR.
|
|
55
56
|
- Do NOT modify `.cloverleaf/` — state transitions are the skill's job.
|
|
56
57
|
- Do NOT skip tests or write placeholder tests. Every acceptance criterion must be covered by a real, meaningful test.
|
|
57
|
-
- Work within the existing project patterns.
|
|
58
|
+
- Work within the existing project patterns. Follow the repo's existing conventions — its configuration, scripts, and test layout.
|
|
58
59
|
- Small, focused commits are preferred but a single well-scoped commit is acceptable for this task.
|
package/prompts/qa.md
CHANGED
|
@@ -25,11 +25,11 @@ The Standard's QA contract requires a `preview_uri`. You were passed the sentine
|
|
|
25
25
|
|
|
26
26
|
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.
|
|
27
27
|
|
|
28
|
-
1. Set up isolated worktree and
|
|
29
|
-
helper
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
1. Set up an isolated worktree and prime it so the project's tests can run. The
|
|
29
|
+
`prep-worktree` helper prepares the worktree's dependencies (for a TypeScript monorepo it
|
|
30
|
+
copies the built tooling; for other projects it runs the configured `worktree_setup_command`
|
|
31
|
+
and copies any `prep_copy_dirs`). Without it, a fresh worktree may lack the dependencies the
|
|
32
|
+
tests need.
|
|
33
33
|
```bash
|
|
34
34
|
TMPDIR=$(mktemp -d)
|
|
35
35
|
SHA=$(git rev-parse {{branch}})
|
|
@@ -50,8 +50,8 @@ The Standard's QA contract requires a `preview_uri`. You were passed the sentine
|
|
|
50
50
|
- Run it in `"$TMPDIR/<cwd>"`
|
|
51
51
|
- Capture stdout, stderr, exit code
|
|
52
52
|
- Parse test output to extract `passed`, `failed`, `total`:
|
|
53
|
-
-
|
|
54
|
-
-
|
|
53
|
+
- Exit code is the universal signal: exit 0 = the command's checks passed; non-zero = failed.
|
|
54
|
+
- 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
55
|
- On failure, collect up to 10 failure names/messages as findings with `severity: "error"` and `rule: "qa.<suite>.<test-name>"`
|
|
56
56
|
|
|
57
57
|
5. Aggregate results: sum `passed`, `failed`, `total` across all runs.
|
|
@@ -59,7 +59,7 @@ The Standard's QA contract requires a `preview_uri`. You were passed the sentine
|
|
|
59
59
|
6. Compute verdict:
|
|
60
60
|
- `pass` — every command exited 0 AND aggregated `failed === 0`
|
|
61
61
|
- `bounce` — any command exited non-zero OR `failed > 0`; findings list the first ~10 failures
|
|
62
|
-
- `escalate` — any command failed deterministically on 3 consecutive retries (attempt the rerun yourself), OR
|
|
62
|
+
- `escalate` — any command failed deterministically on 3 consecutive retries (attempt the rerun yourself), OR the worktree setup itself failed (infrastructure problem)
|
|
63
63
|
|
|
64
64
|
7. Teardown:
|
|
65
65
|
```bash
|
|
@@ -72,17 +72,25 @@ The Standard's QA contract requires a `preview_uri`. You were passed the sentine
|
|
|
72
72
|
- Read-only. Do NOT edit source files.
|
|
73
73
|
- Use `git worktree`: do NOT `git checkout` in the main working directory.
|
|
74
74
|
- Always teardown the worktree, even on error.
|
|
75
|
-
- **Loading or running a module directly.**
|
|
75
|
+
- **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):
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
npx tsx -e "import('./lib/<module>.js').then(m => console.log(Object.keys(m)))"
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
For other ecosystems, use your language's module-load or REPL equivalent. For anything the test suite already covers, prefer running the tests.
|
|
82
|
+
|
|
81
83
|
## QA Report (v0.4)
|
|
82
84
|
|
|
83
85
|
After executing all matched QA rules, write an HTML report summarizing each run to `<repoRoot>/.cloverleaf/runs/{taskId}/qa/report.html` (substitute `{taskId}` with the `id` field from the task input, e.g., `{{task.id}}`).
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
Write the runs array (one `{ruleId, command, cwd, durationMs, passed, stdoutTail, stderrTail}` object per executed command) to a temp JSON file, then generate the report via the CLI:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cloverleaf-cli qa-report /tmp/cl-qa-runs-{taskId}.json "<repoRoot>/.cloverleaf/runs/{taskId}/qa/report.html"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The CLI creates the output directory.
|
|
86
94
|
|
|
87
95
|
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):
|
|
88
96
|
|
package/prompts/reviewer.md
CHANGED
|
@@ -8,6 +8,7 @@ You are the Cloverleaf Reviewer agent. Your job: perform a fresh-eyes review of
|
|
|
8
8
|
- `branch`: the branch name the Implementer produced (e.g., `cloverleaf/DEMO-001`).
|
|
9
9
|
- `base_branch`: the branch to diff against (default: `main`).
|
|
10
10
|
- `repo_root`: absolute path to the consumer repo.
|
|
11
|
+
- `test_rules`: a JSON object `{ rules: [...] }` whose `rules` is a list of `{cwd, match, command}` entries (from `qa-rules.json`).
|
|
11
12
|
|
|
12
13
|
## Your process
|
|
13
14
|
|
|
@@ -49,15 +50,16 @@ A `pass` verdict MAY have an empty `findings` array or omit it. A `bounce` verdi
|
|
|
49
50
|
- You are a fresh pair of eyes. Do not rubber-stamp. If you have substantive doubts, bounce.
|
|
50
51
|
- Check that tests actually cover the AC; a passing test suite with no AC coverage is a bounce.
|
|
51
52
|
- Do NOT modify any files. You are read-only.
|
|
52
|
-
- Do NOT use `git checkout` or `git switch`. Read files via `git show <branch>:<path>`. If you need a live checkout to run tests, use a worktree and prime it with `cloverleaf-cli prep-worktree` (
|
|
53
|
+
- Do NOT use `git checkout` or `git switch`. Read files via `git show <branch>:<path>`. If you need a live checkout to run tests, use a worktree and prime it with `cloverleaf-cli prep-worktree` (prepares the worktree so the project's tests can run):
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
56
|
MAIN=$(pwd)
|
|
56
57
|
SHA=$(git rev-parse cloverleaf/<task-id>)
|
|
57
58
|
git worktree add --detach /tmp/cl-review-<task-id> "$SHA"
|
|
58
59
|
cloverleaf-cli prep-worktree "$MAIN" /tmp/cl-review-<task-id>
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
# Run the project's tests. Your rules are in {{test_rules}} (JSON object { rules: [{cwd, match, command}, ...] }).
|
|
61
|
+
# For each rule whose match globs cover a changed file, run its command in
|
|
62
|
+
# /tmp/cl-review-<task-id>/<cwd>.
|
|
61
63
|
cd -
|
|
62
64
|
git worktree remove /tmp/cl-review-<task-id>
|
|
63
65
|
```
|
|
@@ -65,10 +67,12 @@ A `pass` verdict MAY have an empty `findings` array or omit it. A `bounce` verdi
|
|
|
65
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.
|
|
66
68
|
|
|
67
69
|
This keeps `.cloverleaf/` on main intact.
|
|
68
|
-
- **Loading or running a module directly.**
|
|
70
|
+
- **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):
|
|
69
71
|
|
|
70
72
|
```bash
|
|
71
73
|
npx tsx -e "import('./lib/<module>.js').then(m => console.log(Object.keys(m)))"
|
|
72
74
|
```
|
|
75
|
+
|
|
76
|
+
For other ecosystems, use your language's module-load or REPL equivalent. For anything the test suite already covers, prefer running the tests.
|
|
73
77
|
- Severities (per the Cloverleaf feedback schema): `blocker` = wrong behavior / missing AC / broken tests; `error` = notable defect that should be fixed but doesn't break AC; `warning` = should fix; `info` = nit / style. Use `blocker` and `error` for bounces.
|
|
74
78
|
- If a criterion is subjective, lean toward pass — the task author chose those words deliberately.
|
|
@@ -23,6 +23,16 @@ The user has invoked this skill with a TASK-ID (e.g., `DEMO-001`).
|
|
|
23
23
|
```
|
|
24
24
|
Capture the output. If present and the latest verdict is `bounce`, pass it into the subagent.
|
|
25
25
|
|
|
26
|
+
3b. Load the project's test rules (consumer override or shipped default):
|
|
27
|
+
```bash
|
|
28
|
+
if [ -f "<repo_root>/.cloverleaf/config/qa-rules.json" ]; then
|
|
29
|
+
cat "<repo_root>/.cloverleaf/config/qa-rules.json"
|
|
30
|
+
else
|
|
31
|
+
cat "$(cloverleaf-cli plugin-root)/config/qa-rules.json"
|
|
32
|
+
fi
|
|
33
|
+
```
|
|
34
|
+
Capture the output as `test_rules`.
|
|
35
|
+
|
|
26
36
|
4. Dispatch the Implementer subagent via the Task tool:
|
|
27
37
|
- `subagent_type`: `general-purpose`
|
|
28
38
|
- `model`: `sonnet`
|
|
@@ -31,6 +41,7 @@ The user has invoked this skill with a TASK-ID (e.g., `DEMO-001`).
|
|
|
31
41
|
- `{{feedback}}` → the feedback JSON if present, else the literal string `null`
|
|
32
42
|
- `{{repo_root}}` → absolute path to the current repo
|
|
33
43
|
- `{{base_branch}}` → `main` (or the current default branch)
|
|
44
|
+
- `{{test_rules}}` → the test rules JSON captured in step 3b
|
|
34
45
|
|
|
35
46
|
**Dispatch conventions:** invoke the Task tool in foreground mode (its default — do NOT pass `run_in_background: true`). The Task tool returns the subagent's final message as a string in the result. Do NOT use Bash `sleep` to poll an output file — the harness blocks foreground `sleep`, and background dispatch is unnecessary here because the foreground Task tool already blocks until the subagent finishes.
|
|
36
47
|
|
|
@@ -37,10 +37,20 @@ description: Run the Reviewer agent on a task in the `review` state. Emits a fee
|
|
|
37
37
|
```
|
|
38
38
|
Capture this output for the subagent.
|
|
39
39
|
|
|
40
|
+
4b. Load the project's test rules (consumer override or shipped default):
|
|
41
|
+
```bash
|
|
42
|
+
if [ -f "<repo_root>/.cloverleaf/config/qa-rules.json" ]; then
|
|
43
|
+
cat "<repo_root>/.cloverleaf/config/qa-rules.json"
|
|
44
|
+
else
|
|
45
|
+
cat "$(cloverleaf-cli plugin-root)/config/qa-rules.json"
|
|
46
|
+
fi
|
|
47
|
+
```
|
|
48
|
+
Capture the output as `test_rules`.
|
|
49
|
+
|
|
40
50
|
5. Dispatch the Reviewer subagent via the Task tool:
|
|
41
51
|
- `subagent_type`: `general-purpose`
|
|
42
52
|
- `model`: `sonnet`
|
|
43
|
-
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/reviewer.md` with substitutions for `{{task}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{diff}}`.
|
|
53
|
+
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/reviewer.md` with substitutions for `{{task}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{diff}}`, `{{test_rules}}`.
|
|
44
54
|
|
|
45
55
|
**Dispatch conventions:** invoke the Task tool in foreground mode (its default — do NOT pass `run_in_background: true`). The Task tool returns the subagent's final message as a string in the result. Do NOT use Bash `sleep` to poll an output file — the harness blocks foreground `sleep`, and background dispatch is unnecessary here because the foreground Task tool already blocks until the subagent finishes.
|
|
46
56
|
|