@cloverleaf/reference-impl 0.8.1 → 0.8.3
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/VERSION +1 -1
- package/dist/cli.mjs +25 -4
- package/dist/scope-check.mjs +7 -2
- package/lib/cli.ts +31 -4
- package/lib/scope-check.ts +7 -2
- package/package.json +1 -1
- package/skills/cloverleaf-breakdown/SKILL.md +2 -0
- package/skills/cloverleaf-document/SKILL.md +2 -0
- package/skills/cloverleaf-draft-rfc/SKILL.md +2 -0
- package/skills/cloverleaf-implement/SKILL.md +2 -0
- package/skills/cloverleaf-qa/SKILL.md +2 -0
- package/skills/cloverleaf-review/SKILL.md +2 -0
- package/skills/cloverleaf-security-review/SKILL.md +3 -0
- package/skills/cloverleaf-spike/SKILL.md +2 -0
- package/skills/cloverleaf-ui-review/SKILL.md +2 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.8.
|
|
1
|
+
0.8.3
|
package/dist/cli.mjs
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import { readFileSync, mkdirSync, copyFileSync, appendFileSync } from 'node:fs';
|
|
46
46
|
import { dirname, join } from 'node:path';
|
|
47
|
-
import { execSync } from 'node:child_process';
|
|
47
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
48
48
|
import { loadTask, saveTask } from './task.mjs';
|
|
49
49
|
import { advanceStatus } from './task.mjs';
|
|
50
50
|
import { emitGateDecision } from './events.mjs';
|
|
@@ -66,7 +66,7 @@ import { buildBaselinePath } from './visual-diff.mjs';
|
|
|
66
66
|
import { computeReadyTasks, detectCycle } from './dag-walker.mjs';
|
|
67
67
|
import { readWalkState, writeWalkState, walkStatePath } from './walk-state.mjs';
|
|
68
68
|
import { loadWalkerConfig } from './walker-config.mjs';
|
|
69
|
-
import { classifyFiles } from './scope-check.mjs';
|
|
69
|
+
import { classifyFiles, normalizePath } from './scope-check.mjs';
|
|
70
70
|
import { computeRfcTasksView } from './rfc-tasks.mjs';
|
|
71
71
|
import { loadSecretPatternsConfig, scanSecrets } from './secret-scan.mjs';
|
|
72
72
|
import { classifyTaskSecurity } from './security-classify.mjs';
|
|
@@ -652,8 +652,29 @@ try {
|
|
|
652
652
|
catch {
|
|
653
653
|
// No diff is fine (empty branch)
|
|
654
654
|
}
|
|
655
|
-
// 4.
|
|
656
|
-
|
|
655
|
+
// 4. Compute sharedFiles via git check-attr (merge=union honors user's multi-writer intent)
|
|
656
|
+
let sharedFiles = new Set();
|
|
657
|
+
if (modifiedFiles.length > 0) {
|
|
658
|
+
try {
|
|
659
|
+
const out = execFileSync('git', ['-C', repoRoot, 'check-attr', '-z', 'merge', '--', ...modifiedFiles], { encoding: 'utf-8' });
|
|
660
|
+
// -z output: NUL-separated triplets: path\0attr\0value\0
|
|
661
|
+
const parts = out.split('\0');
|
|
662
|
+
for (let i = 0; i + 2 < parts.length; i += 3) {
|
|
663
|
+
const path = parts[i];
|
|
664
|
+
const attr = parts[i + 1];
|
|
665
|
+
const value = parts[i + 2];
|
|
666
|
+
if (attr === 'merge' && value === 'union') {
|
|
667
|
+
sharedFiles.add(normalizePath(path));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
process.stderr.write(`cloverleaf-cli check-scope: git check-attr failed (${err instanceof Error ? err.message : String(err)}); proceeding with no shared-file annotations.\n`);
|
|
673
|
+
sharedFiles = new Set();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// 5. Classify and output
|
|
677
|
+
const result = classifyFiles(taskDoc, modifiedFiles, siblingScopes, sharedFiles);
|
|
657
678
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
658
679
|
process.exit(0);
|
|
659
680
|
}
|
package/dist/scope-check.mjs
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* - Strip trailing `/` (preserving a non-empty result)
|
|
38
38
|
* - Return the resulting string (may be empty for degenerate inputs; caller filters)
|
|
39
39
|
*/
|
|
40
|
-
function normalizePath(p) {
|
|
40
|
+
export function normalizePath(p) {
|
|
41
41
|
let s = p.trim().replace(/\\/g, '/');
|
|
42
42
|
// Strip leading `./` repeatedly
|
|
43
43
|
while (s.startsWith('./')) {
|
|
@@ -71,7 +71,7 @@ function normalizePaths(paths) {
|
|
|
71
71
|
* @param siblingScopes - Other tasks in the same Plan with their declared files.
|
|
72
72
|
* @returns - { contested, own, extension } with arrays sorted by file path.
|
|
73
73
|
*/
|
|
74
|
-
export function classifyFiles(taskDoc, modifiedFiles, siblingScopes) {
|
|
74
|
+
export function classifyFiles(taskDoc, modifiedFiles, siblingScopes, sharedFiles) {
|
|
75
75
|
// 1. Normalize own files from taskDoc.scope.files_touched
|
|
76
76
|
const scope = taskDoc['scope'];
|
|
77
77
|
const rawOwn = Array.isArray(scope?.['files_touched'])
|
|
@@ -117,6 +117,11 @@ export function classifyFiles(taskDoc, modifiedFiles, siblingScopes) {
|
|
|
117
117
|
if (ownSet.has(f)) {
|
|
118
118
|
own.push(f);
|
|
119
119
|
}
|
|
120
|
+
else if (sharedFiles?.has(f)) {
|
|
121
|
+
// merge=union (or other shared-intent annotation): never contested.
|
|
122
|
+
// Falls into extension so post-merge auto-extend picks it up.
|
|
123
|
+
extension.push(f);
|
|
124
|
+
}
|
|
120
125
|
else if (siblingMap.has(f)) {
|
|
121
126
|
const owners = siblingMap.get(f);
|
|
122
127
|
contested.push({ file: f, owner: owners[0] }); // lex-smallest wins
|
package/lib/cli.ts
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
import { readFileSync, mkdirSync, copyFileSync, appendFileSync, existsSync } from 'node:fs';
|
|
47
47
|
import { dirname, join } from 'node:path';
|
|
48
|
-
import { execSync } from 'node:child_process';
|
|
48
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
49
49
|
import { loadTask, saveTask } from './task.js';
|
|
50
50
|
import { advanceStatus } from './task.js';
|
|
51
51
|
import { emitGateDecision } from './events.js';
|
|
@@ -68,7 +68,7 @@ import { buildBaselinePath } from './visual-diff.js';
|
|
|
68
68
|
import { computeReadyTasks, detectCycle } from './dag-walker.js';
|
|
69
69
|
import { readWalkState, writeWalkState, walkStatePath } from './walk-state.js';
|
|
70
70
|
import { loadWalkerConfig } from './walker-config.js';
|
|
71
|
-
import { classifyFiles } from './scope-check.js';
|
|
71
|
+
import { classifyFiles, normalizePath } from './scope-check.js';
|
|
72
72
|
import type { SiblingScope } from './scope-check.js';
|
|
73
73
|
import { computeRfcTasksView, type RfcTasksView } from './rfc-tasks.js';
|
|
74
74
|
import { loadSecretPatternsConfig, scanSecrets } from './secret-scan.js';
|
|
@@ -663,8 +663,35 @@ try {
|
|
|
663
663
|
// No diff is fine (empty branch)
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
-
// 4.
|
|
667
|
-
|
|
666
|
+
// 4. Compute sharedFiles via git check-attr (merge=union honors user's multi-writer intent)
|
|
667
|
+
let sharedFiles = new Set<string>();
|
|
668
|
+
if (modifiedFiles.length > 0) {
|
|
669
|
+
try {
|
|
670
|
+
const out = execFileSync(
|
|
671
|
+
'git',
|
|
672
|
+
['-C', repoRoot, 'check-attr', '-z', 'merge', '--', ...modifiedFiles],
|
|
673
|
+
{ encoding: 'utf-8' },
|
|
674
|
+
);
|
|
675
|
+
// -z output: NUL-separated triplets: path\0attr\0value\0
|
|
676
|
+
const parts = out.split('\0');
|
|
677
|
+
for (let i = 0; i + 2 < parts.length; i += 3) {
|
|
678
|
+
const path = parts[i];
|
|
679
|
+
const attr = parts[i + 1];
|
|
680
|
+
const value = parts[i + 2];
|
|
681
|
+
if (attr === 'merge' && value === 'union') {
|
|
682
|
+
sharedFiles.add(normalizePath(path));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
} catch (err) {
|
|
686
|
+
process.stderr.write(
|
|
687
|
+
`cloverleaf-cli check-scope: git check-attr failed (${err instanceof Error ? err.message : String(err)}); proceeding with no shared-file annotations.\n`,
|
|
688
|
+
);
|
|
689
|
+
sharedFiles = new Set();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// 5. Classify and output
|
|
694
|
+
const result = classifyFiles(taskDoc, modifiedFiles, siblingScopes, sharedFiles);
|
|
668
695
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
669
696
|
process.exit(0);
|
|
670
697
|
}
|
package/lib/scope-check.ts
CHANGED
|
@@ -56,7 +56,7 @@ export interface SiblingScope {
|
|
|
56
56
|
* - Strip trailing `/` (preserving a non-empty result)
|
|
57
57
|
* - Return the resulting string (may be empty for degenerate inputs; caller filters)
|
|
58
58
|
*/
|
|
59
|
-
function normalizePath(p: string): string {
|
|
59
|
+
export function normalizePath(p: string): string {
|
|
60
60
|
let s = p.trim().replace(/\\/g, '/');
|
|
61
61
|
// Strip leading `./` repeatedly
|
|
62
62
|
while (s.startsWith('./')) {
|
|
@@ -93,7 +93,8 @@ function normalizePaths(paths: string[]): string[] {
|
|
|
93
93
|
export function classifyFiles(
|
|
94
94
|
taskDoc: TaskDoc,
|
|
95
95
|
modifiedFiles: string[],
|
|
96
|
-
siblingScopes: SiblingScope[]
|
|
96
|
+
siblingScopes: SiblingScope[],
|
|
97
|
+
sharedFiles?: Set<string>
|
|
97
98
|
): ClassifyResult {
|
|
98
99
|
// 1. Normalize own files from taskDoc.scope.files_touched
|
|
99
100
|
const scope = taskDoc['scope'] as Record<string, unknown> | undefined;
|
|
@@ -138,6 +139,10 @@ export function classifyFiles(
|
|
|
138
139
|
for (const f of filteredModified) {
|
|
139
140
|
if (ownSet.has(f)) {
|
|
140
141
|
own.push(f);
|
|
142
|
+
} else if (sharedFiles?.has(f)) {
|
|
143
|
+
// merge=union (or other shared-intent annotation): never contested.
|
|
144
|
+
// Falls into extension so post-merge auto-extend picks it up.
|
|
145
|
+
extension.push(f);
|
|
141
146
|
} else if (siblingMap.has(f)) {
|
|
142
147
|
const owners = siblingMap.get(f)!;
|
|
143
148
|
contested.push({ file: f, owner: owners[0] }); // lex-smallest wins
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloverleaf/reference-impl",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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",
|
|
@@ -46,6 +46,8 @@ The user has invoked this skill with an RFC-ID (e.g., `CLV-009`).
|
|
|
46
46
|
|
|
47
47
|
In the subagent context, also supply a hint that `next_id_base === $PLAN_ID`, so task IDs in `tasks[]` start at `PLAN_ID + 1`. The Plan agent allocates its own ID as `next_id_base` and task IDs sequentially after.
|
|
48
48
|
|
|
49
|
+
**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.
|
|
50
|
+
|
|
49
51
|
7. Parse subagent response — expected JSON conforming to `plan.schema.json`. Required fields: `id, type: "plan", status: "drafting", owner, project, parent_rfc, task_dag (edge-based), tasks (inline Task docs, status=pending)`. 3-bounce budget per invocation.
|
|
50
52
|
|
|
51
53
|
8. Ensure output `plan.id === $PLAN_ID`, `project === $PROJECT_ID`, `parent_rfc === { project: <rfc.project>, id: $RFC_ID }`. If the subagent drifted, override these before save.
|
|
@@ -43,6 +43,8 @@ description: Run the Documenter agent on a task in the `implementing` state (ful
|
|
|
43
43
|
- `{{base_branch}}` → `main`
|
|
44
44
|
- `{{repo_root}}` → absolute path
|
|
45
45
|
|
|
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.
|
|
47
|
+
|
|
46
48
|
6. Parse the subagent's response. Expect JSON of the form `{"commits_added": N, "files_changed": [...], "summary": "..."}`.
|
|
47
49
|
|
|
48
50
|
7. On failure to parse or response with invalid shape: report the response and stop without advancing state.
|
|
@@ -41,6 +41,8 @@ The user has invoked this skill with an RFC-ID (e.g., `CLV-009`).
|
|
|
41
41
|
- `{{completed_spikes}}` → `COMPLETED_SPIKES` JSON array (or `[]`)
|
|
42
42
|
- `{{spike}}` → (unused for draftRfc; substitute `null`)
|
|
43
43
|
|
|
44
|
+
**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.
|
|
45
|
+
|
|
44
46
|
6. Parse subagent's response: expected JSON conforming to `rfc.schema.json`. Required fields: `id`, `type: "rfc"`, `status: "drafting"`, `owner`, `project`, `title`, `problem`, `solution`, `unknowns` (array of strings), `acceptance_criteria`, `out_of_scope`.
|
|
45
47
|
|
|
46
48
|
If output fails schema validation: bounce. Budget: 3 bounces per invocation. On budget exhaustion: report and stop without advancing state.
|
|
@@ -32,6 +32,8 @@ The user has invoked this skill with a TASK-ID (e.g., `DEMO-001`).
|
|
|
32
32
|
- `{{repo_root}}` → absolute path to the current repo
|
|
33
33
|
- `{{base_branch}}` → `main` (or the current default branch)
|
|
34
34
|
|
|
35
|
+
**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
|
+
|
|
35
37
|
5. Parse the subagent's response. Expect JSON of the form `{"status": "done", "branch": "...", "files_changed": [...], "summary": "..."}` or `{"status": "blocked", "reason": "..."}`.
|
|
36
38
|
|
|
37
39
|
6. On `blocked`: report the reason and stop. Do NOT advance status.
|
|
@@ -49,6 +49,8 @@ description: Run the QA agent on a task in the `qa` state (full pipeline only).
|
|
|
49
49
|
- `model`: `sonnet`
|
|
50
50
|
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/qa.md` with substitutions for `{{task}}`, `{{diff}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{qa_rules}}` (the JSON loaded in step 5).
|
|
51
51
|
|
|
52
|
+
**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.
|
|
53
|
+
|
|
52
54
|
8. Parse response: expect `{"verdict": "pass"|"bounce"|"escalate", "summary", "findings", "results"}`.
|
|
53
55
|
|
|
54
56
|
9. Branch on verdict:
|
|
@@ -42,6 +42,8 @@ description: Run the Reviewer agent on a task in the `review` state. Emits a fee
|
|
|
42
42
|
- `model`: `sonnet`
|
|
43
43
|
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/reviewer.md` with substitutions for `{{task}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{diff}}`.
|
|
44
44
|
|
|
45
|
+
**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
|
+
|
|
45
47
|
6. Parse the subagent's response. Expect a feedback envelope JSON of the form `{"verdict": "pass"|"bounce", "summary": "...", "findings": [...]}`. Validate shape: verdict must be `pass` or `bounce`; if `bounce`, findings must have at least one entry with `severity` (one of `blocker|error|warning|info`) and `message`.
|
|
46
48
|
|
|
47
49
|
7. Branch on verdict:
|
|
@@ -28,6 +28,9 @@ description: Run the Security Reviewer agent on a task in the `security-review`
|
|
|
28
28
|
- `subagent_type`: `general-purpose`
|
|
29
29
|
- `model`: `sonnet`
|
|
30
30
|
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/security-reviewer.md` with substitutions for `{{task}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{diff}}`.
|
|
31
|
+
|
|
32
|
+
**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.
|
|
33
|
+
|
|
31
34
|
Parse the subagent's feedback envelope (`verdict` + `findings[]`).
|
|
32
35
|
|
|
33
36
|
6. **Merge + derive verdict.** Concatenate Pass A findings + Pass B findings into one `findings[]`. Derive the final verdict from the max severity across ALL findings:
|
|
@@ -38,6 +38,8 @@ The user has invoked this skill with a SPIKE-ID (e.g., `CLV-010`).
|
|
|
38
38
|
- `{{brief}}` → `null` (unused for runSpike)
|
|
39
39
|
- `{{prior_rfc}}`, `{{completed_spikes}}` → `null`
|
|
40
40
|
|
|
41
|
+
**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.
|
|
42
|
+
|
|
41
43
|
6. Parse subagent response. Expected: the spike JSON with `status: "completed"`, `findings: string`, `recommendation: string`. Schema: `spike.schema.json` (validated by save-spike).
|
|
42
44
|
|
|
43
45
|
If output fails schema validation: bounce. Budget: 3 bounces. On exhaustion: report and stop without advancing to completed.
|
|
@@ -72,6 +72,8 @@ description: Run the UI Reviewer agent on a task in the `ui-review` state (full
|
|
|
72
72
|
- `{{affected_routes}}` → the value of `$AFFECTED` (verbatim — may be `"all"`, a JSON array, or `[]` but step 6 handled `[]` already)
|
|
73
73
|
- `{{ui_review_config}}` → JSON-stringified result of `cloverleaf-cli ui-review-config <repo_root>` (used by the subagent to scope viewport sizes, thresholds, and axe rule overrides)
|
|
74
74
|
|
|
75
|
+
**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.
|
|
76
|
+
|
|
75
77
|
11. Parse the subagent's response. Expect `{"verdict": "pass"|"bounce"|"escalate", "summary": "...", "findings": [...]}`.
|
|
76
78
|
|
|
77
79
|
12. **Read the baseline-approval sidecar** (after the subagent completes, regardless of verdict):
|