@cloverleaf/reference-impl 0.4.1 → 0.5.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/.claude-plugin/plugin.json +1 -1
- package/VERSION +1 -1
- package/config/discovery.json +5 -0
- package/dist/cli.mjs +130 -4
- package/dist/discovery-config.mjs +26 -0
- package/dist/ids.mjs +26 -1
- package/dist/index.mjs +1 -1
- package/dist/plan.mjs +116 -0
- package/dist/rfc.mjs +39 -0
- package/dist/spike.mjs +38 -0
- package/dist/task.mjs +58 -0
- package/dist/work-item.mjs +49 -0
- package/lib/cli.ts +127 -4
- package/lib/discovery-config.ts +35 -0
- package/lib/ids.ts +25 -1
- package/lib/index.ts +1 -1
- package/lib/plan.ts +148 -0
- package/lib/rfc.ts +63 -0
- package/lib/spike.ts +61 -0
- package/lib/task.ts +91 -0
- package/lib/work-item.ts +78 -0
- package/package.json +1 -1
- package/prompts/plan.md +63 -0
- package/prompts/researcher.md +74 -0
- package/skills/cloverleaf-breakdown/SKILL.md +74 -0
- package/skills/cloverleaf-discover/SKILL.md +140 -0
- package/skills/cloverleaf-draft-rfc/SKILL.md +99 -0
- package/skills/cloverleaf-gate/SKILL.md +106 -0
- package/skills/cloverleaf-new-rfc/SKILL.md +76 -0
- package/skills/cloverleaf-spike/SKILL.md +66 -0
- package/lib/state.ts +0 -137
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cloverleaf-spike
|
|
3
|
+
description: Run a single Spike via the Researcher agent (operation=runSpike). Advances pending → running → completed with findings + recommendation. Usage — /cloverleaf-spike <SPIKE-ID>.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cloverleaf — run Spike
|
|
7
|
+
|
|
8
|
+
The user has invoked this skill with a SPIKE-ID (e.g., `CLV-010`).
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. Capture `<SPIKE-ID>` as `$SPIKE_ID`. If missing, report usage and stop.
|
|
13
|
+
|
|
14
|
+
2. Load the spike:
|
|
15
|
+
```
|
|
16
|
+
cloverleaf-cli load-spike <repo_root> <SPIKE-ID>
|
|
17
|
+
```
|
|
18
|
+
Verify `status === "pending"`. If not, report and stop.
|
|
19
|
+
|
|
20
|
+
3. Transition pending → running:
|
|
21
|
+
```
|
|
22
|
+
cloverleaf-cli advance-spike <repo_root> <SPIKE-ID> running agent
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
4. Load discovery config:
|
|
26
|
+
```bash
|
|
27
|
+
DOC_CTX=$(cloverleaf-cli discovery-config --repo-root <repo_root> | jq -r .docContextUri)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
5. Dispatch the Researcher subagent via the Task tool:
|
|
31
|
+
- `subagent_type`: `general-purpose`
|
|
32
|
+
- `model`: `sonnet`
|
|
33
|
+
- Prompt: contents of `$(cloverleaf-cli plugin-root)/prompts/researcher.md`, with placeholders:
|
|
34
|
+
- `{{operation}}` → `runSpike`
|
|
35
|
+
- `{{spike}}` → the full spike JSON (from step 2, with status now `running`)
|
|
36
|
+
- `{{doc_context_uri}}` → `$DOC_CTX`
|
|
37
|
+
- `{{repo_root}}` → absolute path to the current repo
|
|
38
|
+
- `{{brief}}` → `null` (unused for runSpike)
|
|
39
|
+
- `{{prior_rfc}}`, `{{completed_spikes}}` → `null`
|
|
40
|
+
|
|
41
|
+
6. Parse subagent response. Expected: the spike JSON with `status: "completed"`, `findings: string`, `recommendation: string`. Schema: `spike.schema.json` (validated by save-spike).
|
|
42
|
+
|
|
43
|
+
If output fails schema validation: bounce. Budget: 3 bounces. On exhaustion: report and stop without advancing to completed.
|
|
44
|
+
|
|
45
|
+
7. Save the completed spike:
|
|
46
|
+
```
|
|
47
|
+
cloverleaf-cli save-spike <repo_root> /tmp/spike-$SPIKE_ID.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
8. Transition running → completed:
|
|
51
|
+
```
|
|
52
|
+
cloverleaf-cli advance-spike <repo_root> <SPIKE-ID> completed agent
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
9. Commit:
|
|
56
|
+
```bash
|
|
57
|
+
git add .cloverleaf/spikes/ .cloverleaf/events/
|
|
58
|
+
git commit -m "cloverleaf: spike $SPIKE_ID completed"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
10. Report: spike findings summary.
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
|
|
65
|
+
- Orchestrator (`/cloverleaf-discover`) loops this for every spike in the RFC's `unknowns[]` (materialised as Spike work items by `/cloverleaf-draft-rfc`) before re-drafting the RFC.
|
|
66
|
+
- If `method === "prototype"` or `method === "benchmark"`: the Researcher agent describes what to prototype/benchmark, not implement it. v0.5 does not build prototypes — that's Delivery's job.
|
package/lib/state.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
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
|
-
}
|