@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.
@@ -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
- }