@cloverleaf/reference-impl 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @cloverleaf/reference-impl
2
+
3
+ Reference implementation of the Cloverleaf methodology as a set of Claude Code skills. This package implements the **Tight Loop** — Implementer + Reviewer — letting a user drive a Task from `pending` to `merged` with state, events, and feedback recorded in the repo under `.cloverleaf/`.
4
+
5
+ Implements [Cloverleaf Standard](../standard/) v0.3.0 at L2 (Exchange) conformance.
6
+
7
+ ## Install
8
+
9
+ From this directory:
10
+
11
+ ```bash
12
+ npm install # pulls @cloverleaf/standard + deps
13
+ ./install.sh # symlinks skills into ~/.claude/plugins/cloverleaf/
14
+ # or:
15
+ ./install.sh --project # local install into ./.claude/plugins/cloverleaf/
16
+ ```
17
+
18
+ ## Skills provided
19
+
20
+ | Slash command | Role |
21
+ |---|---|
22
+ | `/cloverleaf-new-task "<brief>"` | Scaffold a new Task JSON from a prose brief. |
23
+ | `/cloverleaf-implement <TASK-ID>` | Dispatch Implementer subagent; advances pending → review. |
24
+ | `/cloverleaf-review <TASK-ID>` | Dispatch Reviewer subagent; pass → automated-gates, bounce → implementing. |
25
+ | `/cloverleaf-merge <TASK-ID>` | Human gate; confirm and transition automated-gates → merged. |
26
+ | `/cloverleaf-run <TASK-ID>` | Orchestrator: loops implement → review with up to 3 bounces, pauses at the human merge gate. |
27
+
28
+ ## Quick start — toy repo
29
+
30
+ ```bash
31
+ cd examples/toy-repo
32
+ npm install
33
+ ../../install.sh --project
34
+ ```
35
+
36
+ In a Claude Code session in that directory:
37
+
38
+ ```
39
+ /cloverleaf-run DEMO-001
40
+ ```
41
+
42
+ Watch it walk the state machine, produce a branch `cloverleaf/DEMO-001` with a `multiply` function + tests, and pause at the merge gate.
43
+
44
+ ## What's in v0.1.0
45
+
46
+ - Implementer + Reviewer agents (wired as subagents).
47
+ - Full state machine transitions from `pending` to `merged` (fast-lane path), with stub transitions through `documenting` and `automated-gates`.
48
+ - Per-project committed audit trail under `.cloverleaf/`.
49
+ - Max 3 bounces before `escalated`.
50
+
51
+ ## What's NOT in v0.1.0
52
+
53
+ - Researcher, Plan, Documenter, UI Reviewer, QA agents (stub transitions only).
54
+ - HTTP endpoints for the agent OpenAPI contracts — skills are markdown-driven, not servers. L3 conformance deferred.
55
+ - `git push` / PR creation — branch stays local; user handles remote.
56
+ - Path-rules or risk-classifier-rules enforcement — fast-lane is hardcoded.
57
+
58
+ See [upcoming releases](../CHANGELOG.md) for the roadmap.
59
+
60
+ ## Branch topology
61
+
62
+ State commits (`.cloverleaf/**`) always land on `main`. Code commits land on a per-task feature branch named `cloverleaf/<task-id>`.
63
+
64
+ - `main`: canonical audit trail. Every status-transition event, gate decision, and feedback envelope is committed here, in order.
65
+ - `cloverleaf/<task-id>`: code for one task. Branched from main; the Implementer agent lives here.
66
+
67
+ The skills handle the branch switching for you. After `/cloverleaf-implement <TASK-ID>` runs, you are back on main with the state updates committed; the `cloverleaf/<task-id>` branch holds the code ready for review. After `/cloverleaf-merge`, the audit trail reflects the merged state, and you push the code branch manually.
68
+
69
+ The Reviewer never switches branches. It reads files via `git show` and runs tests in a `git worktree add` sidecar to avoid clobbering main's `.cloverleaf/`.
70
+
71
+ ## Package layout
72
+
73
+ - `lib/` — TypeScript library used by the CLI. State, events, feedback, IDs, paths.
74
+ - `skills/` — Claude Code skill markdown files.
75
+ - `prompts/` — Implementer/Reviewer subagent system prompts.
76
+ - `examples/toy-repo/` — standalone demo repo.
77
+ - `tests/` — Vitest suite.
78
+
79
+ ## Development
80
+
81
+ ```bash
82
+ npm test # run the Vitest suite
83
+ npm run test:watch
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT — see [../LICENSE](../LICENSE).
package/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
package/install.sh ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Cloverleaf Reference Impl installer.
5
+ # Default: installs skills + CLI shim into ~/.claude/plugins/cloverleaf/.
6
+ # --project: installs locally into ./.claude/plugins/cloverleaf/.
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ MODE="user"
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --project) MODE="project"; shift ;;
14
+ --help|-h)
15
+ echo "Usage: ./install.sh [--project]"
16
+ echo " --project: install locally in .claude/plugins/cloverleaf/"
17
+ echo " (default): install at ~/.claude/plugins/cloverleaf/"
18
+ exit 0 ;;
19
+ *) echo "Unknown arg: $1"; exit 2 ;;
20
+ esac
21
+ done
22
+
23
+ if [[ "$MODE" == "user" ]]; then
24
+ INSTALL_ROOT="${HOME}/.claude/plugins/cloverleaf"
25
+ else
26
+ INSTALL_ROOT="$(pwd)/.claude/plugins/cloverleaf"
27
+ fi
28
+
29
+ mkdir -p "${INSTALL_ROOT}/skills" "${INSTALL_ROOT}/prompts" "${INSTALL_ROOT}/bin"
30
+
31
+ # Symlink skills
32
+ for f in "${SCRIPT_DIR}/skills/"*.md; do
33
+ name="$(basename "$f")"
34
+ ln -sf "$f" "${INSTALL_ROOT}/skills/${name}"
35
+ done
36
+
37
+ # Symlink prompts
38
+ for f in "${SCRIPT_DIR}/prompts/"*.md; do
39
+ name="$(basename "$f")"
40
+ ln -sf "$f" "${INSTALL_ROOT}/prompts/${name}"
41
+ done
42
+
43
+ # Write the CLI shim
44
+ cat > "${INSTALL_ROOT}/bin/cloverleaf-cli" <<EOF
45
+ #!/usr/bin/env bash
46
+ exec npx --yes tsx "${SCRIPT_DIR}/lib/cli.ts" "\$@"
47
+ EOF
48
+ chmod +x "${INSTALL_ROOT}/bin/cloverleaf-cli"
49
+
50
+ echo "Cloverleaf reference impl installed at: ${INSTALL_ROOT}"
51
+ echo "Skills available: $(ls "${INSTALL_ROOT}/skills" | wc -l | tr -d ' ')"
52
+ echo ""
53
+ echo "Add ${INSTALL_ROOT}/bin to your PATH if you want to invoke cloverleaf-cli directly,"
54
+ echo "or reference it by absolute path from your skill calls."
package/lib/cli.ts ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * Cloverleaf CLI dispatcher.
4
+ *
5
+ * Usage: cli.ts <command> [args...]
6
+ *
7
+ * Commands:
8
+ * load-task <repoRoot> <taskId>
9
+ * infer-project <repoRoot>
10
+ * next-task-id <repoRoot> [--project=<p>]
11
+ * advance-status <repoRoot> <taskId> <toStatus> <actor> [gate] [path]
12
+ * write-feedback <repoRoot> <taskId> <envelopeJsonPath>
13
+ * latest-feedback <repoRoot> <taskId>
14
+ * emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]
15
+ */
16
+
17
+ import { readFileSync } from 'node:fs';
18
+ import { loadTask } from './state.js';
19
+ import { advanceStatus } from './state.js';
20
+ import { emitGateDecision } from './events.js';
21
+ import { writeFeedback, latestFeedback } from './feedback.js';
22
+ import { nextTaskId, inferProject } from './ids.js';
23
+ import type { FeedbackEnvelope } from './feedback.js';
24
+
25
+ function die(msg: string, code = 1): never {
26
+ process.stderr.write(msg + '\n');
27
+ process.exit(code);
28
+ }
29
+
30
+ function usage(msg?: string): never {
31
+ if (msg) process.stderr.write(msg + '\n');
32
+ process.stderr.write(
33
+ 'Usage: cli.ts <command> [args...]\n' +
34
+ 'Commands:\n' +
35
+ ' load-task <repoRoot> <taskId>\n' +
36
+ ' infer-project <repoRoot>\n' +
37
+ ' next-task-id <repoRoot> [--project=<p>]\n' +
38
+ ' advance-status <repoRoot> <taskId> <toStatus> <actor> [gate] [path]\n' +
39
+ ' write-feedback <repoRoot> <taskId> <envelopeJsonPath>\n' +
40
+ ' latest-feedback <repoRoot> <taskId>\n' +
41
+ ' emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]\n'
42
+ );
43
+ process.exit(2);
44
+ }
45
+
46
+ const [, , command, ...rest] = process.argv;
47
+
48
+ if (!command) {
49
+ usage('Error: no command given');
50
+ }
51
+
52
+ try {
53
+ switch (command) {
54
+ case 'load-task': {
55
+ const [repoRoot, taskId] = rest;
56
+ if (!repoRoot || !taskId) usage('load-task requires <repoRoot> <taskId>');
57
+ const task = loadTask(repoRoot, taskId);
58
+ process.stdout.write(JSON.stringify(task, null, 2) + '\n');
59
+ break;
60
+ }
61
+
62
+ case 'infer-project': {
63
+ const [repoRoot] = rest;
64
+ if (!repoRoot) usage('infer-project requires <repoRoot>');
65
+ const project = inferProject(repoRoot);
66
+ process.stdout.write(project + '\n');
67
+ break;
68
+ }
69
+
70
+ case 'next-task-id': {
71
+ // rest may contain --project=<p> flag among positional args
72
+ const positional = rest.filter((a) => !a.startsWith('--'));
73
+ const flags = rest.filter((a) => a.startsWith('--'));
74
+ const [repoRoot] = positional;
75
+ if (!repoRoot) usage('next-task-id requires <repoRoot>');
76
+ const projectFlag = flags.find((f) => f.startsWith('--project='));
77
+ const explicitProject = projectFlag ? projectFlag.replace('--project=', '') : undefined;
78
+ const project = inferProject(repoRoot, explicitProject);
79
+ const id = nextTaskId(repoRoot, project);
80
+ process.stdout.write(id + '\n');
81
+ break;
82
+ }
83
+
84
+ case 'advance-status': {
85
+ const [repoRoot, taskId, toStatus, actorArg, gate, path] = rest;
86
+ if (!repoRoot || !taskId || !toStatus || !actorArg)
87
+ usage('advance-status requires <repoRoot> <taskId> <toStatus> <actor> [gate] [path]');
88
+ if (actorArg !== 'agent' && actorArg !== 'human') {
89
+ die(`actor must be 'agent' or 'human' (got '${actorArg}')`, 2);
90
+ }
91
+ const actor: 'agent' | 'human' = actorArg;
92
+ const opts: { gate?: string; path?: 'fast_lane' | 'full_pipeline' } = {};
93
+ if (gate) opts.gate = gate;
94
+ if (path === 'fast_lane' || path === 'full_pipeline') opts.path = path;
95
+ const updated = advanceStatus(repoRoot, taskId, toStatus, actor, opts);
96
+ process.stdout.write(updated.status + '\n');
97
+ break;
98
+ }
99
+
100
+ case 'write-feedback': {
101
+ const [repoRoot, taskId, envelopeJsonPath] = rest;
102
+ if (!repoRoot || !taskId || !envelopeJsonPath)
103
+ usage('write-feedback requires <repoRoot> <taskId> <envelopeJsonPath>');
104
+ const envelope = JSON.parse(readFileSync(envelopeJsonPath, 'utf-8')) as FeedbackEnvelope;
105
+ const match = taskId.match(/^(.+)-\d+$/);
106
+ if (!match) die(`Invalid taskId format: ${taskId}`);
107
+ const project = match[1];
108
+ const writtenPath = writeFeedback(repoRoot, { project, taskId, envelope });
109
+ process.stdout.write(writtenPath + '\n');
110
+ break;
111
+ }
112
+
113
+ case 'latest-feedback': {
114
+ const [repoRoot, taskId] = rest;
115
+ if (!repoRoot || !taskId) usage('latest-feedback requires <repoRoot> <taskId>');
116
+ const envelope = latestFeedback(repoRoot, taskId);
117
+ if (envelope === null) {
118
+ process.stdout.write('');
119
+ } else {
120
+ process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
121
+ }
122
+ break;
123
+ }
124
+
125
+ case 'emit-gate-decision': {
126
+ const positional = rest.filter((a) => !a.startsWith('--'));
127
+ const flags = rest.filter((a) => a.startsWith('--'));
128
+ const [repoRoot, workItemId, gate, decision, actorArg] = positional;
129
+ if (!repoRoot || !workItemId || !gate || !decision || !actorArg)
130
+ usage(
131
+ 'emit-gate-decision requires <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]'
132
+ );
133
+ const validDecisions = ['approve', 'reject', 'revise', 'split', 'abandon', 'escalate'];
134
+ if (!validDecisions.includes(decision)) {
135
+ die(`decision must be one of: ${validDecisions.join(', ')}, got: ${decision}`);
136
+ }
137
+ if (actorArg !== 'agent' && actorArg !== 'human' && actorArg !== 'system') {
138
+ die(`actor must be "agent", "human", or "system", got: ${actorArg}`);
139
+ }
140
+ const commentFlag = flags.find((f) => f.startsWith('--comment='));
141
+ const comment = commentFlag ? commentFlag.replace('--comment=', '') : undefined;
142
+
143
+ // Derive project from workItemId (e.g. "DEMO-001" → "DEMO")
144
+ const wiMatch = workItemId.match(/^(.+)-\d+$/);
145
+ if (!wiMatch) die(`Cannot derive project from workItemId: ${workItemId}`);
146
+ const project = wiMatch[1];
147
+
148
+ const writtenPath = emitGateDecision(repoRoot, {
149
+ project,
150
+ workItemType: 'task',
151
+ workItemId,
152
+ gate,
153
+ decision: decision as 'approve' | 'reject' | 'revise' | 'split' | 'abandon' | 'escalate',
154
+ actor: actorArg as 'agent' | 'human' | 'system',
155
+ reasoning: comment,
156
+ });
157
+ process.stdout.write(writtenPath + '\n');
158
+ break;
159
+ }
160
+
161
+ default:
162
+ usage(`Unknown command: ${command}`);
163
+ }
164
+ } catch (err: unknown) {
165
+ const msg = err instanceof Error ? err.message : String(err);
166
+ // Surface "illegal transition" errors with the right language
167
+ const lower = msg.toLowerCase();
168
+ if (lower.includes('illegal') || lower.includes('not allowed')) {
169
+ die(`Illegal transition: ${msg}`);
170
+ }
171
+ die(`Error: ${msg}`);
172
+ }
package/lib/events.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { eventsDir } from './paths.js';
5
+ import { nextEventId } from './ids.js';
6
+ import { validateOrThrow } from './validate.js';
7
+
8
+ export interface StatusTransitionParams {
9
+ project: string;
10
+ workItemType: 'task' | 'rfc' | 'spike' | 'plan';
11
+ workItemId: string;
12
+ from: string;
13
+ to: string;
14
+ actor: 'agent' | 'human' | 'system';
15
+ gate?: string;
16
+ path?: 'fast_lane' | 'full_pipeline';
17
+ }
18
+
19
+ export interface GateDecisionParams {
20
+ project: string;
21
+ workItemType: 'task' | 'rfc' | 'spike' | 'plan';
22
+ workItemId: string;
23
+ gate: string;
24
+ decision: 'approve' | 'reject' | 'revise' | 'split' | 'abandon' | 'escalate';
25
+ actor: 'agent' | 'human' | 'system';
26
+ reasoning?: string;
27
+ }
28
+
29
+ function actorObject(kind: 'agent' | 'human' | 'system'): { kind: string; id: string } {
30
+ const id = kind === 'agent' ? 'implementer' : kind === 'human' ? 'local-user' : 'system';
31
+ return { kind, id };
32
+ }
33
+
34
+ export function formatReason(opts: { gate?: string; path?: string }): string | undefined {
35
+ const parts: string[] = [];
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;
39
+ }
40
+
41
+ /**
42
+ * Emits a status-transition event to `.cloverleaf/events/`.
43
+ * File name: `<PROJECT>-<NNN>-status.json` where NNN is the next per-project
44
+ * sequential number derived from existing event files.
45
+ *
46
+ * Returns the absolute path of the written file.
47
+ */
48
+ export function emitStatusTransition(repoRoot: string, params: StatusTransitionParams): string {
49
+ const { project, workItemType, workItemId, from, to, actor } = params;
50
+ const seq = nextEventId(repoRoot, project);
51
+ const seqStr = String(seq).padStart(3, '0');
52
+ const filename = `${project}-${seqStr}-status.json`;
53
+ const filePath = join(eventsDir(repoRoot), filename);
54
+
55
+ // Build reason from gate and/or path if provided (schema only allows reason, not gate/path at top level).
56
+ const reason = formatReason({ gate: params.gate, path: params.path });
57
+
58
+ const doc: Record<string, unknown> = {
59
+ event_id: randomUUID(),
60
+ event_type: 'status_transition',
61
+ occurred_at: new Date().toISOString(),
62
+ work_item_id: { project, id: workItemId },
63
+ work_item_type: workItemType,
64
+ from_status: from,
65
+ to_status: to,
66
+ actor: actorObject(actor),
67
+ };
68
+ if (reason !== undefined) {
69
+ doc.reason = reason;
70
+ }
71
+
72
+ mkdirSync(eventsDir(repoRoot), { recursive: true });
73
+ validateOrThrow('https://cloverleaf.example/schemas/status-transition-event.schema.json', doc);
74
+ writeFileSync(filePath, JSON.stringify(doc, null, 2) + '\n');
75
+ return filePath;
76
+ }
77
+
78
+ /**
79
+ * Emits a gate-decision event to `.cloverleaf/events/`.
80
+ * File name: `<PROJECT>-<NNN>-gate.json`.
81
+ *
82
+ * Returns the absolute path of the written file.
83
+ */
84
+ export function emitGateDecision(repoRoot: string, params: GateDecisionParams): string {
85
+ const { project, workItemId, gate, decision, actor, reasoning } = params;
86
+ const seq = nextEventId(repoRoot, project);
87
+ const seqStr = String(seq).padStart(3, '0');
88
+ const filename = `${project}-${seqStr}-gate.json`;
89
+ const filePath = join(eventsDir(repoRoot), filename);
90
+
91
+ const doc: Record<string, unknown> = {
92
+ event_id: randomUUID(),
93
+ event_type: 'gate_decision',
94
+ occurred_at: new Date().toISOString(),
95
+ gate,
96
+ work_item_id: { project, id: workItemId },
97
+ decision,
98
+ approver: actorObject(actor),
99
+ };
100
+ if (reasoning !== undefined) {
101
+ doc.comment = reasoning;
102
+ }
103
+
104
+ mkdirSync(eventsDir(repoRoot), { recursive: true });
105
+ validateOrThrow('https://cloverleaf.example/schemas/gate-decision-event.schema.json', doc);
106
+ writeFileSync(filePath, JSON.stringify(doc, null, 2) + '\n');
107
+ return filePath;
108
+ }
@@ -0,0 +1,74 @@
1
+ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { feedbackDir } from './paths.js';
4
+ import { nextFeedbackIteration } from './ids.js';
5
+ import { validateOrThrow } from './validate.js';
6
+
7
+ export type Verdict = 'pass' | 'bounce' | 'escalate';
8
+ export type FindingSeverity = 'info' | 'warning' | 'error' | 'blocker';
9
+
10
+ export interface FindingLocation {
11
+ file?: string;
12
+ line?: number;
13
+ work_item_id?: { project: string; id: string };
14
+ }
15
+
16
+ export interface Finding {
17
+ severity: FindingSeverity;
18
+ message: string;
19
+ location?: FindingLocation;
20
+ suggestion?: string;
21
+ rule?: string;
22
+ }
23
+
24
+ export interface FeedbackEnvelope {
25
+ verdict: Verdict;
26
+ summary?: string;
27
+ findings?: Finding[];
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ export interface WriteFeedbackParams {
32
+ project: string;
33
+ taskId: string; // e.g. "ACME-001"
34
+ envelope: FeedbackEnvelope;
35
+ }
36
+
37
+ export function writeFeedback(repoRoot: string, params: WriteFeedbackParams): string {
38
+ const match = params.taskId.match(/^(.+)-(\d+)$/);
39
+ if (!match) throw new Error(`Invalid taskId: ${params.taskId}`);
40
+ const project = match[1];
41
+ const taskNum = parseInt(match[2], 10);
42
+ if (project !== params.project) {
43
+ throw new Error(`project mismatch: taskId=${params.taskId} vs project=${params.project}`);
44
+ }
45
+ const iteration = nextFeedbackIteration(repoRoot, project, taskNum);
46
+ const filename = `${params.taskId}-r${iteration}.json`;
47
+ const path = join(feedbackDir(repoRoot), filename);
48
+ mkdirSync(feedbackDir(repoRoot), { recursive: true });
49
+ validateOrThrow('https://cloverleaf.example/schemas/feedback.schema.json', params.envelope);
50
+ writeFileSync(path, JSON.stringify(params.envelope, null, 2) + '\n');
51
+ return path;
52
+ }
53
+
54
+ export function latestFeedback(repoRoot: string, taskId: string): FeedbackEnvelope | null {
55
+ const items = allFeedback(repoRoot, taskId);
56
+ return items.length === 0 ? null : items[items.length - 1];
57
+ }
58
+
59
+ export function allFeedback(repoRoot: string, taskId: string): FeedbackEnvelope[] {
60
+ const dir = feedbackDir(repoRoot);
61
+ if (!existsSync(dir)) return [];
62
+ const re = new RegExp(`^${escapeRegex(taskId)}-r(\\d+)\\.json$`);
63
+ const entries = readdirSync(dir)
64
+ .map((f) => ({ f, m: f.match(re) }))
65
+ .filter((x): x is { f: string; m: RegExpMatchArray } => !!x.m)
66
+ .sort((a, b) => parseInt(a.m[1], 10) - parseInt(b.m[1], 10));
67
+ return entries.map(({ f }) =>
68
+ JSON.parse(readFileSync(join(dir, f), 'utf-8')) as FeedbackEnvelope
69
+ );
70
+ }
71
+
72
+ function escapeRegex(s: string): string {
73
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
74
+ }
package/lib/ids.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { readdirSync, existsSync } from 'node:fs';
2
+ import { tasksDir, eventsDir, feedbackDir, projectsDir } from './paths.js';
3
+
4
+ export function nextTaskId(repoRoot: string, project: string): string {
5
+ const dir = tasksDir(repoRoot);
6
+ if (!existsSync(dir)) return `${project}-001`;
7
+ const re = new RegExp(`^${escapeRegex(project)}-(\\d+)\\.json$`);
8
+ const nums = readdirSync(dir)
9
+ .map((f) => f.match(re))
10
+ .filter((m): m is RegExpMatchArray => !!m)
11
+ .map((m) => parseInt(m[1], 10));
12
+ const next = nums.length === 0 ? 1 : Math.max(...nums) + 1;
13
+ return `${project}-${String(next).padStart(3, '0')}`;
14
+ }
15
+
16
+ export function nextEventId(repoRoot: string, project: string): number {
17
+ const dir = eventsDir(repoRoot);
18
+ if (!existsSync(dir)) return 1;
19
+ const re = new RegExp(`^${escapeRegex(project)}-(\\d+)-(status|gate)\\.json$`);
20
+ const nums = readdirSync(dir)
21
+ .map((f) => f.match(re))
22
+ .filter((m): m is RegExpMatchArray => !!m)
23
+ .map((m) => parseInt(m[1], 10));
24
+ return nums.length === 0 ? 1 : Math.max(...nums) + 1;
25
+ }
26
+
27
+ export function nextFeedbackIteration(repoRoot: string, project: string, taskNum: number): number {
28
+ const dir = feedbackDir(repoRoot);
29
+ if (!existsSync(dir)) return 1;
30
+ const suffix = String(taskNum).padStart(3, '0');
31
+ const re = new RegExp(`^${escapeRegex(project)}-${suffix}-r(\\d+)\\.json$`);
32
+ const nums = readdirSync(dir)
33
+ .map((f) => f.match(re))
34
+ .filter((m): m is RegExpMatchArray => !!m)
35
+ .map((m) => parseInt(m[1], 10));
36
+ return nums.length === 0 ? 1 : Math.max(...nums) + 1;
37
+ }
38
+
39
+ export function listProjects(repoRoot: string): string[] {
40
+ const dir = projectsDir(repoRoot);
41
+ if (!existsSync(dir)) return [];
42
+ return readdirSync(dir)
43
+ .filter((f) => f.endsWith('.json') && !f.endsWith('.meta.json'))
44
+ .map((f) => f.replace(/\.json$/, ''));
45
+ }
46
+
47
+ export function inferProject(repoRoot: string, explicit?: string): string {
48
+ if (explicit) return explicit;
49
+ const projects = listProjects(repoRoot);
50
+ if (projects.length === 0) throw new Error('No projects found under .cloverleaf/projects/');
51
+ if (projects.length > 1) {
52
+ throw new Error(`Multiple projects found (${projects.join(', ')}); specify one explicitly.`);
53
+ }
54
+ return projects[0];
55
+ }
56
+
57
+ function escapeRegex(s: string): string {
58
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './paths.js';
2
+ export * from './ids.js';
3
+ export * from './state.js';
4
+ export * from './events.js';
5
+ export * from './feedback.js';
6
+ export * from './validate.js';
package/lib/paths.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { resolve } from 'node:path';
2
+
3
+ const CLOVERLEAF = '.cloverleaf';
4
+
5
+ export function cloverleafDir(repoRoot: string): string {
6
+ return resolve(repoRoot, CLOVERLEAF);
7
+ }
8
+
9
+ export function projectsDir(repoRoot: string): string {
10
+ return resolve(cloverleafDir(repoRoot), 'projects');
11
+ }
12
+
13
+ export function tasksDir(repoRoot: string): string {
14
+ return resolve(cloverleafDir(repoRoot), 'tasks');
15
+ }
16
+
17
+ export function eventsDir(repoRoot: string): string {
18
+ return resolve(cloverleafDir(repoRoot), 'events');
19
+ }
20
+
21
+ export function feedbackDir(repoRoot: string): string {
22
+ return resolve(cloverleafDir(repoRoot), 'feedback');
23
+ }
24
+
25
+ export function rfcsDir(repoRoot: string): string {
26
+ return resolve(cloverleafDir(repoRoot), 'rfcs');
27
+ }
28
+
29
+ export function spikesDir(repoRoot: string): string {
30
+ return resolve(cloverleafDir(repoRoot), 'spikes');
31
+ }
32
+
33
+ export function plansDir(repoRoot: string): string {
34
+ return resolve(cloverleafDir(repoRoot), 'plans');
35
+ }
package/lib/state.ts ADDED
@@ -0,0 +1,137 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { tasksDir, projectsDir } from './paths.js';
6
+ import { emitStatusTransition, formatReason } from './events.js';
7
+
8
+ // Import validator from @cloverleaf/standard.
9
+ // The standard package ships TypeScript source only with no exports map.
10
+ // Vitest (via vite-node) resolves .js → .ts for workspace symlinked packages,
11
+ // so the .js convention works here. If it ever fails with "module not found",
12
+ // switch the specifier to '@cloverleaf/standard/validators/index.ts'.
13
+ import { validateStatusTransitionLegality } from '@cloverleaf/standard/validators/index.js';
14
+ import type { StatusTransitions, Task as SMTask } from '@cloverleaf/standard/validators/index.js';
15
+ import { validateOrThrow } from './validate.js';
16
+
17
+ const req = createRequire(import.meta.url);
18
+
19
+ export interface TaskDoc {
20
+ type: 'task';
21
+ project: string;
22
+ id: string;
23
+ title: string;
24
+ status: string;
25
+ risk_class: 'low' | 'high';
26
+ owner: { kind: 'agent' | 'human' | 'system'; id: string };
27
+ acceptance_criteria: string[];
28
+ definition_of_done: string[];
29
+ context: Record<string, unknown>;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ export interface ProjectDoc {
34
+ key: string;
35
+ name?: string;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export function loadTask(repoRoot: string, taskId: string): TaskDoc {
40
+ const path = join(tasksDir(repoRoot), `${taskId}.json`);
41
+ if (!existsSync(path)) throw new Error(`Task ${taskId} not found at ${path}`);
42
+ return JSON.parse(readFileSync(path, 'utf-8')) as TaskDoc;
43
+ }
44
+
45
+ export function saveTask(repoRoot: string, task: TaskDoc): void {
46
+ validateOrThrow('https://cloverleaf.example/schemas/task.schema.json', task);
47
+ const path = join(tasksDir(repoRoot), `${task.id}.json`);
48
+ writeFileSync(path, JSON.stringify(task, null, 2) + '\n');
49
+ }
50
+
51
+ export function loadProject(repoRoot: string, projectId: string): ProjectDoc {
52
+ const path = join(projectsDir(repoRoot), `${projectId}.json`);
53
+ if (!existsSync(path)) throw new Error(`Project ${projectId} not found at ${path}`);
54
+ return JSON.parse(readFileSync(path, 'utf-8')) as ProjectDoc;
55
+ }
56
+
57
+ function loadTaskStateMachine(): StatusTransitions {
58
+ // state-machines/task.json is a static JSON asset. Navigate from standard's
59
+ // package.json — no exports map support needed.
60
+ const pkgPath = req.resolve('@cloverleaf/standard/package.json');
61
+ const pkgDir = pkgPath.replace(/\/package\.json$/, '');
62
+ return JSON.parse(readFileSync(`${pkgDir}/state-machines/task.json`, 'utf-8')) as StatusTransitions;
63
+ }
64
+
65
+ export function advanceStatus(
66
+ repoRoot: string,
67
+ taskId: string,
68
+ toStatus: string,
69
+ actor: 'agent' | 'human',
70
+ options: { gate?: string; path?: 'fast_lane' | 'full_pipeline' } = {}
71
+ ): TaskDoc {
72
+ const task = loadTask(repoRoot, taskId);
73
+ const from = task.status;
74
+ const sm = loadTaskStateMachine();
75
+
76
+ // Read risk_class directly from the task (defaulting to 'low' if absent).
77
+ // The validator derives itemPath from workItem.risk_class: low → fast_lane, else full_pipeline.
78
+ // If caller passed options.path, translate it back to risk_class for the validator.
79
+ const riskClass: 'low' | 'high' =
80
+ options.path === 'fast_lane' ? 'low'
81
+ : options.path === 'full_pipeline' ? 'high'
82
+ : (task.risk_class ?? 'low');
83
+
84
+ // Build a minimal Task-shaped object so the validator can resolve path-tagged transitions.
85
+ const workItemForValidator: SMTask = {
86
+ type: 'task',
87
+ id: task.id,
88
+ project: task.project,
89
+ status: task.status,
90
+ risk_class: riskClass,
91
+ context: { rfc: { project: task.project, id: task.id } },
92
+ definition_of_done: task.definition_of_done,
93
+ acceptance_criteria: task.acceptance_criteria,
94
+ };
95
+
96
+ const reason = formatReason({ gate: options.gate, path: options.path });
97
+ const event = {
98
+ event_id: randomUUID(),
99
+ event_type: 'status_transition' as const,
100
+ occurred_at: new Date().toISOString(),
101
+ work_item_id: { project: task.project, id: task.id },
102
+ work_item_type: 'task' as const,
103
+ from_status: from,
104
+ to_status: toStatus,
105
+ actor: { kind: actor, id: actor },
106
+ ...(reason ? { reason } : {}),
107
+ };
108
+
109
+ const result = validateStatusTransitionLegality(event, sm, workItemForValidator);
110
+ if (!result.ok) {
111
+ const msgs = result.violations.map((v) => v.message).join('; ');
112
+ throw new Error(`Illegal transition ${from} → ${toStatus}: ${msgs}`);
113
+ }
114
+
115
+ // NEW: emit first, save second. validateStatusTransitionLegality stays above.
116
+ const emittedPath = emitStatusTransition(repoRoot, {
117
+ project: task.project,
118
+ workItemType: 'task',
119
+ workItemId: task.id,
120
+ from,
121
+ to: toStatus,
122
+ actor,
123
+ gate: options.gate,
124
+ path: options.path,
125
+ });
126
+
127
+ const proposed = { ...task, status: toStatus };
128
+ try {
129
+ saveTask(repoRoot, proposed);
130
+ } catch (err) {
131
+ const inner = err instanceof Error ? err.message : String(err);
132
+ throw new Error(
133
+ `orphan event written to ${emittedPath} but task save failed: ${inner}`
134
+ );
135
+ }
136
+ return proposed;
137
+ }
@@ -0,0 +1,61 @@
1
+ import Ajv, { type ValidateFunction } from 'ajv/dist/2020.js';
2
+ import addFormats from 'ajv-formats';
3
+ import { readFileSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+
6
+ const req = createRequire(import.meta.url);
7
+ const pkgDir = req.resolve('@cloverleaf/standard/package.json').replace(/\/package\.json$/, '');
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
+ let ajvInstance: Ajv | null = null;
28
+ const compiledCache = new Map<string, ValidateFunction>();
29
+
30
+ function getAjv(): Ajv {
31
+ if (ajvInstance) return ajvInstance;
32
+ const ajv = new Ajv({ strict: false, validateFormats: true, allErrors: true });
33
+ addFormats(ajv);
34
+ for (const file of SCHEMA_FILES) {
35
+ const schema = JSON.parse(readFileSync(`${pkgDir}/schemas/${file}`, 'utf-8'));
36
+ ajv.addSchema(schema);
37
+ }
38
+ ajvInstance = ajv;
39
+ return ajv;
40
+ }
41
+
42
+ function getValidator(schemaId: string): ValidateFunction {
43
+ const cached = compiledCache.get(schemaId);
44
+ if (cached) return cached;
45
+ const ajv = getAjv();
46
+ const validator = ajv.getSchema(schemaId);
47
+ if (!validator) throw new Error(`Schema not registered: ${schemaId}`);
48
+ compiledCache.set(schemaId, validator);
49
+ return validator;
50
+ }
51
+
52
+ export function validateOrThrow(schemaId: string, doc: unknown): void {
53
+ const validate = getValidator(schemaId);
54
+ if (!validate(doc)) {
55
+ const violations = (validate.errors ?? []).length;
56
+ const detail = JSON.stringify(validate.errors, null, 2);
57
+ throw new Error(
58
+ `Schema validation failed: ${violations} violation(s) against ${schemaId}\n${detail}`
59
+ );
60
+ }
61
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@cloverleaf/reference-impl",
3
+ "version": "0.1.1",
4
+ "description": "Reference implementation of the Cloverleaf methodology as Claude Code skills. Implements the Tight Loop (Implementer + Reviewer).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Renato D'Arrigo <renato.darrigo@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/cloverleaf-org/cloverleaf.git",
11
+ "directory": "reference-impl"
12
+ },
13
+ "homepage": "https://github.com/cloverleaf-org/cloverleaf#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/cloverleaf-org/cloverleaf/issues"
16
+ },
17
+ "keywords": [
18
+ "cloverleaf",
19
+ "methodology",
20
+ "ai-first",
21
+ "claude-code",
22
+ "agent",
23
+ "reference-implementation"
24
+ ],
25
+ "files": [
26
+ "skills",
27
+ "lib",
28
+ "prompts",
29
+ "install.sh",
30
+ "README.md",
31
+ "VERSION"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "bin": {
37
+ "cloverleaf-cli": "./lib/cli.ts"
38
+ },
39
+ "scripts": {
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "prepublishOnly": "npm test"
43
+ },
44
+ "dependencies": {
45
+ "@cloverleaf/standard": "^0.3.0",
46
+ "ajv": "^8.17.1",
47
+ "ajv-formats": "^3.0.1"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.0.0",
51
+ "tsx": "^4.19.0",
52
+ "typescript": "^5.5.0",
53
+ "vitest": "^2.0.0"
54
+ }
55
+ }
@@ -0,0 +1,47 @@
1
+ # Implementer Subagent
2
+
3
+ You are the Cloverleaf Implementer agent. Your job: take a Task and produce working code that satisfies its Definition of Done and Acceptance Criteria.
4
+
5
+ ## Inputs
6
+
7
+ - `task`: a Cloverleaf Task document (JSON).
8
+ - `feedback`: optional — the most recent feedback envelope from a prior Reviewer bounce. If present, address every finding before re-submitting.
9
+ - `repo_root`: absolute path to the consumer repo.
10
+ - `base_branch`: the branch to branch off (default: `main`).
11
+
12
+ ## Your process
13
+
14
+ 1. Read the task's `title`, `acceptance_criteria`, `definition_of_done`, and `context`. Read any referenced files.
15
+ 2. If `feedback` is present, re-read each finding; plan how to address them.
16
+ 3. Create a new branch named `cloverleaf/<task.id>` from `base_branch` using `git checkout -b cloverleaf/<task.id>`.
17
+ 4. Implement the code + tests needed to satisfy every acceptance criterion.
18
+ 5. Run the project's test command (typically `npm test` — check package.json `scripts.test`). All tests must pass.
19
+ 6. Stage and commit your changes with message `feat: <task.title> [<task.id>]`.
20
+ 7. Return a structured JSON result to stdout:
21
+
22
+ ```json
23
+ {
24
+ "status": "done",
25
+ "branch": "cloverleaf/<task.id>",
26
+ "files_changed": ["path/to/file1.ts", "tests/path/to/file1.test.ts"],
27
+ "summary": "One-sentence summary of the implementation."
28
+ }
29
+ ```
30
+
31
+ If you cannot complete the task:
32
+
33
+ ```json
34
+ {
35
+ "status": "blocked",
36
+ "reason": "Concise description of what's blocking you."
37
+ }
38
+ ```
39
+
40
+ ## Rules
41
+
42
+ - Do NOT push the branch to a remote. The human will handle that post-merge.
43
+ - Do NOT open a PR.
44
+ - Do NOT modify `.cloverleaf/` — state transitions are the skill's job.
45
+ - Do NOT skip tests or write placeholder tests. Every acceptance criterion must be covered by a real, meaningful test.
46
+ - Work within the existing project patterns. If the repo has a tsconfig, package.json scripts, or test conventions, follow them.
47
+ - Small, focused commits are preferred but a single well-scoped commit is acceptable for this task.
@@ -0,0 +1,56 @@
1
+ # Reviewer Subagent
2
+
3
+ You are the Cloverleaf Reviewer agent. Your job: perform a fresh-eyes review of an Implementer's branch against the task's Acceptance Criteria and emit a structured feedback envelope.
4
+
5
+ ## Inputs
6
+
7
+ - `task`: the Cloverleaf Task document (JSON).
8
+ - `branch`: the branch name the Implementer produced (e.g., `cloverleaf/DEMO-001`).
9
+ - `base_branch`: the branch to diff against (default: `main`).
10
+ - `repo_root`: absolute path to the consumer repo.
11
+
12
+ ## Your process
13
+
14
+ 1. Read the task's `acceptance_criteria` and `definition_of_done`.
15
+ 2. Run `git diff <base_branch>..<branch> --stat` and `git diff <base_branch>..<branch>` to see the change.
16
+ 3. For each acceptance criterion, determine whether the diff satisfies it. Note any unsatisfied criteria as findings.
17
+ 4. Check for defects: missing tests, obvious logic errors, security issues, hygiene problems.
18
+ 5. Decide verdict:
19
+ - `pass` if every acceptance criterion is satisfied and no blocking defects exist.
20
+ - `bounce` otherwise.
21
+ 6. Return a feedback envelope (per `feedback.schema.json`) to stdout as JSON:
22
+
23
+ ```json
24
+ {
25
+ "verdict": "pass" | "bounce",
26
+ "summary": "One or two sentences.",
27
+ "findings": [
28
+ {
29
+ "severity": "blocker" | "error" | "warning" | "info",
30
+ "message": "Concise description of the issue.",
31
+ "location": { "file": "path/to/file.ts", "line": 42 }
32
+ }
33
+ ]
34
+ }
35
+ ```
36
+
37
+ A `pass` verdict MAY have an empty `findings` array or omit it. A `bounce` verdict MUST have at least one finding AND a `summary`.
38
+
39
+ ## Rules
40
+
41
+ - You are a fresh pair of eyes. Do not rubber-stamp. If you have substantive doubts, bounce.
42
+ - Check that tests actually cover the AC; a passing test suite with no AC coverage is a bounce.
43
+ - Do NOT modify any files. You are read-only.
44
+ - 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:
45
+
46
+ ```bash
47
+ git worktree add /tmp/cl-review-<task-id> cloverleaf/<task-id>
48
+ cd /tmp/cl-review-<task-id>
49
+ npm install && npm test
50
+ cd -
51
+ git worktree remove /tmp/cl-review-<task-id>
52
+ ```
53
+
54
+ This keeps `.cloverleaf/` on main intact.
55
+ - 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.
56
+ - If a criterion is subjective, lean toward pass — the task author chose those words deliberately.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: cloverleaf-implement
3
+ description: Run the Implementer agent on a task. Dispatches a subagent to produce code + tests on a new branch, then advances state pending → tactical-plan → implementing → documenting → review. Usage — /cloverleaf-implement <TASK-ID>.
4
+ ---
5
+
6
+ # Cloverleaf — implement
7
+
8
+ The user has invoked this skill with a TASK-ID (e.g., `DEMO-001`).
9
+
10
+ ## Steps
11
+
12
+ 1. Capture the argument. If no TASK-ID was provided, report usage and stop.
13
+
14
+ 2. Load the task:
15
+ ```
16
+ ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli load-task <repo_root> <TASK-ID>
17
+ ```
18
+ Parse the JSON. Verify `status === "pending"` OR `status === "implementing"` (the second case is a re-run after a Reviewer bounce). If neither, report the current status and ask the user to use the correct command for that state.
19
+
20
+ 3. Load any outstanding feedback:
21
+ ```
22
+ ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli latest-feedback <repo_root> <TASK-ID>
23
+ ```
24
+ Capture the output. If present and the latest verdict is `bounce`, pass it into the subagent.
25
+
26
+ 4. Dispatch the Implementer subagent via the Task tool:
27
+ - `subagent_type`: `general-purpose`
28
+ - `model`: `sonnet`
29
+ - Prompt: the contents of `~/.claude/plugins/cloverleaf/prompts/implementer.md`, with placeholders substituted:
30
+ - `{{task}}` → the full task JSON (pretty-printed)
31
+ - `{{feedback}}` → the feedback JSON if present, else the literal string `null`
32
+ - `{{repo_root}}` → absolute path to the current repo
33
+ - `{{base_branch}}` → `main` (or the current default branch)
34
+
35
+ 5. Parse the subagent's response. Expect JSON of the form `{"status": "done", "branch": "...", "files_changed": [...], "summary": "..."}` or `{"status": "blocked", "reason": "..."}`.
36
+
37
+ 6. On `blocked`: report the reason and stop. Do NOT advance status.
38
+
39
+ 7. On `done`: walk the state machine. First, switch back to main:
40
+
41
+ ```bash
42
+ cd <repo_root>
43
+ git checkout main
44
+ ```
45
+
46
+ If this fails (uncommitted changes on main, detached HEAD, etc.), report the error and stop without advancing state.
47
+
48
+ 8. Run each of these CLI calls in sequence:
49
+
50
+ If the current task status is `pending`:
51
+ ```
52
+ cloverleaf-cli advance-status <repo_root> <TASK-ID> tactical-plan agent
53
+ cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent
54
+ cloverleaf-cli advance-status <repo_root> <TASK-ID> documenting agent
55
+ cloverleaf-cli advance-status <repo_root> <TASK-ID> review agent
56
+ ```
57
+
58
+ If the current task status was `implementing` (loop-back after bounce):
59
+ ```
60
+ cloverleaf-cli advance-status <repo_root> <TASK-ID> documenting agent
61
+ cloverleaf-cli advance-status <repo_root> <TASK-ID> review agent
62
+ ```
63
+
64
+ 9. Commit the state changes:
65
+ ```
66
+ cd <repo_root>
67
+ git add .cloverleaf/
68
+ git commit -m "cloverleaf: <TASK-ID> → review"
69
+ ```
70
+
71
+ 10. Report:
72
+ - "✓ Implementer done. Branch `<branch>`. State → review."
73
+ - "Files changed: <comma-separated>."
74
+ - "Currently on: `main`."
75
+ - "Next: `/cloverleaf-review <TASK-ID>`."
76
+
77
+ ## Rules
78
+
79
+ - Never push the branch or modify remote state.
80
+ - If any `advance-status` call fails (illegal transition), stop and report.
81
+ - The skill's working directory is the consumer's repo root.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: cloverleaf-merge
3
+ description: Human gate for merging a Cloverleaf task. Advances automated-gates → merged via the human_merge gate, requires explicit user confirmation. Usage — /cloverleaf-merge <TASK-ID>.
4
+ ---
5
+
6
+ # Cloverleaf — merge
7
+
8
+ ## Steps
9
+
10
+ 0. Ensure you are on `main`. State is authoritative on main. Run:
11
+
12
+ ```bash
13
+ cd <repo_root>
14
+ current=$(git rev-parse --abbrev-ref HEAD)
15
+ if [ "$current" != "main" ]; then git checkout main; fi
16
+ ```
17
+
18
+ If main has uncommitted changes, stop and report — the user must clean up first.
19
+
20
+ 1. Capture the TASK-ID.
21
+
22
+ 2. Load the task: `cloverleaf-cli load-task <repo_root> <TASK-ID>`. Verify `status === "automated-gates"`. If not, report and stop.
23
+
24
+ 3. Confirm with the user. Print:
25
+ > "About to merge `<TASK-ID>`. Branch `cloverleaf/<TASK-ID>` has been reviewed and passed. Confirm merge? (y/N)"
26
+
27
+ Wait for the user's reply. Only proceed on explicit `y`, `Y`, `yes`, or `YES`. Anything else: abort without state change.
28
+
29
+ 4. On confirmation:
30
+ - Emit the gate decision: `cloverleaf-cli emit-gate-decision <repo_root> <TASK-ID> human_merge approve human`. Note: decision is `approve` (not `approved` — per the gate-decision schema's enum).
31
+ - Advance: `cloverleaf-cli advance-status <repo_root> <TASK-ID> merged human human_merge fast_lane`.
32
+ - Commit state: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> merged"`.
33
+
34
+ 5. Report:
35
+ - "✓ Merged `<TASK-ID>`. Branch `cloverleaf/<TASK-ID>` is ready for you to push and open a PR."
36
+ - "Suggested: `git push origin cloverleaf/<TASK-ID>` then open a PR against `main`."
37
+
38
+ ## Rules
39
+
40
+ - The skill does NOT push the branch or open a PR. That remains explicit to the user.
41
+ - The `merged` status in `.cloverleaf/` is the Cloverleaf-level record; the actual GitHub merge is the user's responsibility.
42
+ - If the user declines, no state change and no commit.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: cloverleaf-new-task
3
+ description: Scaffold a new Cloverleaf Task from a prose brief. Allocates next task ID, writes .cloverleaf/tasks/<PROJECT>-<NNN>.json. Usage — /cloverleaf-new-task "<brief>".
4
+ ---
5
+
6
+ # Cloverleaf — new task
7
+
8
+ The user has invoked this skill with a brief. Your job: turn the brief into a structured Cloverleaf Task and write it to `.cloverleaf/tasks/`.
9
+
10
+ ## Steps
11
+
12
+ 1. Determine the active project. Run:
13
+ ```
14
+ ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli infer-project <repo_root>
15
+ ```
16
+ where `<repo_root>` is the current working directory. On failure (no projects, or multiple projects), report the error and ask the user to specify `--project=<id>` or to create a project config first.
17
+
18
+ 2. Allocate the next task ID:
19
+ ```
20
+ ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli next-task-id <repo_root> --project=<project>
21
+ ```
22
+ Capture the output (e.g., `DEMO-002`).
23
+
24
+ 3. Read the user's brief (the text passed as the skill argument).
25
+
26
+ 4. Construct a Task JSON document with this shape (matches task.schema.json):
27
+ ```json
28
+ {
29
+ "id": "<allocated-id>",
30
+ "type": "task",
31
+ "status": "pending",
32
+ "owner": { "kind": "agent", "id": "implementer" },
33
+ "project": "<project>",
34
+ "title": "<concise title derived from brief>",
35
+ "context": {},
36
+ "acceptance_criteria": ["<criterion 1>", "<criterion 2>", "..."],
37
+ "definition_of_done": ["<terminal statement of completion>"],
38
+ "risk_class": "low"
39
+ }
40
+ ```
41
+
42
+ Derive 2-5 acceptance criteria from the brief. Each must be verifiable. Derive one or more Definition of Done strings as an array.
43
+
44
+ 5. Write the file to `<repo_root>/.cloverleaf/tasks/<allocated-id>.json`.
45
+
46
+ 6. Commit: `git add .cloverleaf/tasks/<allocated-id>.json && git commit -m "cloverleaf: task <allocated-id>"`.
47
+
48
+ 7. Report:
49
+ - "Created `<allocated-id>` at `.cloverleaf/tasks/<allocated-id>.json`."
50
+ - Show the generated acceptance criteria.
51
+ - Suggest: "Review and edit the task if needed, then run `/cloverleaf-run <allocated-id>`."
52
+
53
+ ## Rules
54
+
55
+ - Do not guess at acceptance criteria. If the brief is too vague (e.g., "make it faster" with no target), ask the user a clarifying question before writing the file.
56
+ - If the user's brief hints at complex/risky work (UI changes, breaking API, cross-project), set `risk_class: "high"` and mention it. Default is `"low"` for simple tasks.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: cloverleaf-review
3
+ description: Run the Reviewer agent on a task in the `review` state. Emits a feedback envelope; advances to `automated-gates` on pass or loops back to `implementing` on bounce. Usage — /cloverleaf-review <TASK-ID>.
4
+ ---
5
+
6
+ # Cloverleaf — review
7
+
8
+ ## Steps
9
+
10
+ 0. Ensure you are on `main`. State is authoritative on main. Run:
11
+
12
+ ```bash
13
+ cd <repo_root>
14
+ current=$(git rev-parse --abbrev-ref HEAD)
15
+ if [ "$current" != "main" ]; then git checkout main; fi
16
+ ```
17
+
18
+ If main has uncommitted changes, stop and report — the user must clean up first.
19
+
20
+ 1. Capture the TASK-ID argument.
21
+
22
+ 2. Load the task:
23
+ ```
24
+ ~/.claude/plugins/cloverleaf/bin/cloverleaf-cli load-task <repo_root> <TASK-ID>
25
+ ```
26
+ Verify `status === "review"`. If not, report the current status and stop.
27
+
28
+ 3. The Implementer's branch is `cloverleaf/<TASK-ID>`. Confirm it exists: `git rev-parse --verify cloverleaf/<TASK-ID>`. If missing, report the discrepancy and stop. Do NOT check out this branch; stay on main.
29
+
30
+ 4. Compute the diff without checking out:
31
+ ```bash
32
+ git diff main..cloverleaf/<TASK-ID>
33
+ ```
34
+ Capture this output for the subagent.
35
+
36
+ 5. Dispatch the Reviewer subagent via the Task tool:
37
+ - `subagent_type`: `general-purpose`
38
+ - `model`: `sonnet`
39
+ - Prompt: contents of `~/.claude/plugins/cloverleaf/prompts/reviewer.md` with substitutions for `{{task}}`, `{{branch}}`, `{{base_branch}}`, `{{repo_root}}`, `{{diff}}`.
40
+
41
+ 6. Parse the subagent's response. Expect a feedback envelope JSON of the form `{"verdict": "pass"|"bounce", "summary": "...", "findings": [...]}`. Validate shape: verdict must be `pass` or `bounce`; if `bounce`, findings must have at least one entry with `severity` (one of `blocker|error|warning|info`) and `message`.
42
+
43
+ 7. Branch on verdict:
44
+
45
+ **Pass:**
46
+ ```
47
+ cloverleaf-cli advance-status <repo_root> <TASK-ID> automated-gates agent
48
+ ```
49
+ Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> review passed → automated-gates"`.
50
+ Report: "✓ Review passed. State → automated-gates. Next: `/cloverleaf-merge <TASK-ID>`."
51
+
52
+ **Bounce:**
53
+ 1. Write the feedback envelope to a temp file: `echo '<envelope-json>' > /tmp/cloverleaf-fb.json`.
54
+ 2. `cloverleaf-cli write-feedback <repo_root> <TASK-ID> /tmp/cloverleaf-fb.json` — captures the path like `.cloverleaf/feedback/<TASK-ID>-r<N>.json`.
55
+ 3. `cloverleaf-cli advance-status <repo_root> <TASK-ID> implementing agent` — loops back.
56
+ 4. Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> review bounced → implementing"`.
57
+ 5. Report: "✗ Review bounced. Findings: <summarize findings by severity>. State → implementing. Next: `/cloverleaf-implement <TASK-ID>`."
58
+
59
+ ## Rules
60
+
61
+ - Never push.
62
+ - Do not modify source code — the reviewer is read-only.
63
+ - On illegal state transition, report and stop without partial commits.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: cloverleaf-run
3
+ description: End-to-end orchestrator. Loops implement → review → (bounce ↻ implement) → pause at human merge gate. Max 3 bounces before escalation. Usage — /cloverleaf-run <TASK-ID>.
4
+ ---
5
+
6
+ # Cloverleaf — run (orchestrator)
7
+
8
+ ## Branch discipline
9
+
10
+ The orchestrator runs each sub-skill with the assumption that the working tree starts on `main`. Between steps, confirm the branch is main before proceeding. The Implementer step leaves the user on main after its internal `git checkout main`; the Reviewer and Merge steps start on main.
11
+
12
+ ## Steps
13
+
14
+ 1. Capture the TASK-ID.
15
+
16
+ 2. Load the task: `cloverleaf-cli load-task <repo_root> <TASK-ID>`. Verify `status === "pending"`. If not, report and stop — suggest the user run individual skills instead.
17
+
18
+ 3. Loop, up to `MAX_BOUNCES = 3`:
19
+
20
+ a. Invoke `/cloverleaf-implement <TASK-ID>` (inline the steps from that skill — do not actually call the slash command recursively).
21
+
22
+ b. Invoke `/cloverleaf-review <TASK-ID>`.
23
+
24
+ c. Re-load the task. If `status === "automated-gates"` — pass! Break out of the loop.
25
+
26
+ d. Else if `status === "implementing"` — bounce. Increment bounce counter. If counter == MAX_BOUNCES, escalate (step 5) and stop.
27
+
28
+ e. Else — unexpected state. Report and stop.
29
+
30
+ 4. On pass: invoke `/cloverleaf-merge <TASK-ID>`, which prompts the user for confirmation.
31
+
32
+ 5. On max bounces exceeded:
33
+ - `cloverleaf-cli advance-status <repo_root> <TASK-ID> escalated agent`.
34
+ - Commit: `git add .cloverleaf/ && git commit -m "cloverleaf: <TASK-ID> escalated after 3 bounces"`.
35
+ - Report: "✗ Escalated `<TASK-ID>` after 3 bounces. Review the feedback files under `.cloverleaf/feedback/` and either refine the task or take over manually."
36
+
37
+ ## Rules
38
+
39
+ - MAX_BOUNCES is hardcoded to 3 for v0.1.0.
40
+ - The orchestrator does not skip the human_merge gate; the user's confirmation is still required at merge time.
41
+ - If ANY individual skill reports an error or stops, the orchestrator also stops with a clear message.