@cloverleaf/reference-impl 0.11.1 → 0.13.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.
@@ -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.11.0",
4
+ "version": "0.13.0",
5
5
  "author": {
6
6
  "name": "Renato D'Arrigo",
7
7
  "email": "renato.darrigo@gmail.com"
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Reference implementation of the Cloverleaf methodology as a set of Claude Code skills. Lets a user drive a Task from `pending` to `merged` with state, events, and feedback recorded in the repo under `.cloverleaf/`.
4
4
 
5
- Implements [Cloverleaf Standard](../standard/) v0.3.0 at L2 (Exchange) conformance.
5
+ Implements [Cloverleaf Standard](../standard/) v0.8.0 at L2 (Exchange) conformance.
6
6
 
7
7
  ## Install
8
8
 
@@ -15,58 +15,124 @@ npm install # pulls @cloverleaf/standard + deps
15
15
  ./install.sh --project # local install into ./.claude/plugins/cloverleaf/
16
16
  ```
17
17
 
18
- ## Scope (v0.3)
18
+ ## How delivery works
19
19
 
20
- v0.2 implements both paths of the Delivery track:
20
+ Every Task walks one path, whatever its risk:
21
21
 
22
- - **Fast Lane** (`risk_class: "low"`): Implementer → Reviewer → Human Merge
23
- - **Full Pipeline** (`risk_class: "high"`): Implementer DocumenterReviewer(UI Reviewer if `site/**` changed) QAFinal Approval
22
+ ```
23
+ pending tactical-planimplementingdocumentingcouncilfinal-gatemerged
24
+ ```
25
+
26
+ `council` is a single parameterized phase. Which reviewers sit on it is configuration, not control flow: `config/council.json` defines the profiles, and the Task's `risk_class` selects one.
27
+
28
+ | `risk_class` | Profile | Round 1 | Round 2 |
29
+ |---|---|---|---|
30
+ | `low` | `delivery-fast` | Reviewer | Security, if `security_class` is `high` |
31
+ | `high` | `delivery-full` | Reviewer | Security (if `security_class` is `high`), UI (if the diff touches UI paths), and QA — dispatched concurrently |
32
+
33
+ Both shipped profiles aggregate with `any-veto`: one blocking member is enough to stop the round.
34
+
35
+ The council has three exits:
36
+
37
+ - **pass** → `final-gate`, where a human gives the single approval that merges the Task.
38
+ - **bounce** → `implementing`, carrying every member's feedback in one batch.
39
+ - **escalate** → `escalated`. Escalation is un-lowerable: no later member, chair, or aggregation rule can turn it back into a pass.
24
40
 
25
41
  ### Agents
26
42
 
27
43
  | Agent | Status | Mechanism |
28
44
  |---|---|---|
45
+ | Researcher | Real | Subagent; drafts an RFC body from its brief and runs Spikes against its unknowns |
46
+ | Plan | Real | Subagent; decomposes an approved RFC into a Plan with a `task_dag` |
29
47
  | Implementer | Real | Subagent, code + tests on feature branch |
30
- | Documenter | Real (v0.2) | Subagent, doc-only commits per file-path rules |
48
+ | Documenter | Real | Subagent, doc-only commits per file-path rules |
31
49
  | Reviewer | Real | Subagent, read-only review of diff |
32
- | UI Reviewer | Real (v0.5) | Playwright + axe-core + pixelmatch; multi-browser outer loop (chromium/webkit/firefox); axe-core runs on `axe.browser` engine only (default chromium); maxCombinations cap with per-route warnings |
33
- | QA | Real (v0.2) | Per-package test runner via `git worktree` |
34
- | Plan | Stub | Deferred to v0.3 |
35
- | Researcher | Stub | Deferred to v0.3 |
50
+ | Security Reviewer | Real | Deterministic secret scan + LLM vulnerability judgment |
51
+ | UI Reviewer | Real | Playwright + axe-core + pixelmatch; multi-browser outer loop (chromium/webkit/firefox), though `ui-review.json` ships chromium-only; axe-core runs on the `axe.browser` engine only; `maxCombinations` cap with per-route warnings |
52
+ | QA | Real | Per-package test runner via `git worktree` |
53
+ | Chair | Real | Deliberative judge; dispatched only when a council bounces under `aggregation: "chair"` |
36
54
 
37
55
  ### Skills
38
56
 
57
+ **Discovery** — RFC → Spikes → Plan → Tasks:
58
+
59
+ - `/cloverleaf-new-rfc` — scaffold an RFC work item from a brief file
60
+ - `/cloverleaf-draft-rfc` — Researcher populates the RFC body; emits a Spike per unknown
61
+ - `/cloverleaf-spike` — run one Spike to findings + recommendation
62
+ - `/cloverleaf-breakdown` — Plan agent decomposes the RFC into a Plan with a `task_dag`
63
+ - `/cloverleaf-gate` — human decision on an RFC or Plan sitting at its gate
64
+ - `/cloverleaf-discover` — end-to-end Discovery orchestrator over all of the above
65
+
66
+ **Delivery** — one Task from `pending` to `merged`:
67
+
39
68
  - `/cloverleaf-new-task` — scaffold a Task (auto-sets `risk_class`)
40
- - `/cloverleaf-implement` — run Implementer
41
- - `/cloverleaf-document` — run Documenter *(new in v0.2)*
42
- - `/cloverleaf-review` — run Reviewer
43
- - `/cloverleaf-ui-review` — run UI Reviewer *(new in v0.2)*
44
- - `/cloverleaf-approve-baselines` — human baseline-approval gate; clears `baselines_pending` and advances `ui-review → qa` *(new in CLV-19)*
45
- - `/cloverleaf-qa` — run QA *(new in v0.2)*
46
- - `/cloverleaf-security-review` — Security Reviewer: deterministic secret scan + LLM vulnerability judgment; runs when `security_class` is `high`
47
- - `/cloverleaf-merge` — human gate (branches on state)
48
- - `/cloverleaf-run` — orchestrator (dispatches by `risk_class`)
49
- - `/cloverleaf-release` — publish a new `@cloverleaf/reference-impl` release; runs pre-flight checks then executes `git tag -a` / `git push origin main` / `git push origin <tag>` / `npm publish` / `gh release create`; accepts `[--dry-run] [--yes]` *(new in CLV-63)*
69
+ - `/cloverleaf-implement` — Implementer; produces the feature branch
70
+ - `/cloverleaf-document` — Documenter; doc-only commits
71
+ - `/cloverleaf-run` — orchestrator; drives the whole walk, selecting the council profile by `risk_class`
72
+ - `/cloverleaf-run-plan` — DAG walker; drives every Task in an approved Plan, several at a time
50
73
 
51
- ### Configuration
74
+ **Council members, run standalone** — each produces a verdict and drives no state transition, so you can get one member's opinion without running a council:
52
75
 
53
- Two JSON config files in `config/` (overridable per consumer project):
76
+ - `/cloverleaf-review` Reviewer
77
+ - `/cloverleaf-security-review` — Security Reviewer
78
+ - `/cloverleaf-ui-review` — UI Reviewer
79
+ - `/cloverleaf-qa` — QA
54
80
 
55
- - `config/ui-paths.json` — glob patterns that trigger UI Reviewer (default: `site/**`)
56
- - `config/qa-rules.json` — per-package test commands
81
+ **Human gates:**
57
82
 
58
- ### Customizing for your repo
83
+ - `/cloverleaf-approve-baselines` approve new or resized visual baselines and clear `baselines_pending`. Clear-only: it drives no transition. Re-run `/cloverleaf-run` afterwards so the held council pass can land.
84
+ - `/cloverleaf-merge` — the `final-gate` approval; performs the real `git merge --no-ff` into main
59
85
 
60
- The package ships with cloverleaf-flavored defaults in `config/`. Your repo overrides any of them by placing a file of the same name at `<repoRoot>/.cloverleaf/config/<name>.json`. Consumer override is a **full replacement** — your file becomes the complete source of truth for that config.
86
+ ### Configuration
87
+
88
+ The package ships defaults for eight config files in `config/`. Your repo overrides any of them by placing a file of the same name at `<repoRoot>/.cloverleaf/config/<name>.json`. Consumer override is a **full replacement** — your file becomes the complete source of truth for that config.
61
89
 
62
- Available overrides:
90
+ | Config file | Purpose |
91
+ |---|---|
92
+ | `council.json` | Council profiles and their per-gate bindings: which reviewers run, in which rounds, under which aggregation rule |
93
+ | `qa-rules.json` | Per-package test commands for the QA member |
94
+ | `ui-paths.json` | Glob patterns marking a diff as UI-touching (default: `site/**`). Feeds the `ui_changes` predicate that admits the UI member to a council round |
95
+ | `affected-routes.json` | Rules for mapping a diff to the site routes the UI member visits; includes `contentRoutes` for content-collection mapping |
96
+ | `ui-review.json` | UI Reviewer runtime settings — browser engines, viewports, visual-diff thresholds, axe scope, `maxCombinations` |
97
+ | `security-paths.json` | Sensitive paths and keywords that infer `security_class: high` |
98
+ | `secret-patterns.json` | Secret regexes and placeholder excludes for the deterministic scan |
99
+ | `discovery.json` | Discovery-track settings, including `worktree_setup_command` for non-TypeScript consumers |
100
+
101
+ One further override has no shipped default, because it only makes sense per-repo:
63
102
 
64
103
  | Override file | Purpose |
65
104
  |---|---|
66
- | `.cloverleaf/config/ui-paths.json` | Controls `detect-ui-paths` — which diffs trigger the `ui-review` state |
67
- | `.cloverleaf/config/qa-rules.json` | Per-package test commands for the QA agent |
68
- | `.cloverleaf/config/affected-routes.json` | Rules for mapping diffs to site routes (UI Reviewer scope); includes `contentRoutes` for content-collection mapping |
69
- | `.cloverleaf/config/astro-base.json` | Explicit Astro `base` path — avoids best-effort parsing of `astro.config.*` |
105
+ | `.cloverleaf/config/astro-base.json` | Explicit Astro `base` path the UI Reviewer reads it instead of best-effort parsing `astro.config.*` |
106
+
107
+ All overrides are read fresh on every skill invocation; no caching. Edit and the next `/cloverleaf-run` picks it up.
108
+
109
+ #### Council profiles
110
+
111
+ `council.json` decides who reviews your code. A profile is a list of rounds; each round is a list of members dispatched concurrently. A `when` clause gates a member on a predicate, so a round can adapt to the Task in front of it:
112
+
113
+ ```json
114
+ {
115
+ "profiles": {
116
+ "delivery-full": {
117
+ "rounds": [
118
+ [ { "member": "reviewer" } ],
119
+ [
120
+ { "member": "security", "when": "security_class:high" },
121
+ { "member": "ui", "when": "ui_changes" },
122
+ { "member": "qa" }
123
+ ]
124
+ ],
125
+ "aggregation": "any-veto",
126
+ "on_round_bounce": "stop"
127
+ }
128
+ },
129
+ "gates": {
130
+ "task.review": { "by": "risk_class", "map": { "low": "delivery-fast", "high": "delivery-full" } }
131
+ }
132
+ }
133
+ ```
134
+
135
+ A member can also be a custom role: give it a `prompt` naming a file under `<repoRoot>/.cloverleaf/prompts/`, and the council dispatches your reviewer alongside the built-ins. The path is exist-checked when the plan resolves, so a typo fails loudly rather than silently skipping a reviewer.
70
136
 
71
137
  Example `affected-routes.json` override for a Next.js project:
72
138
 
@@ -85,8 +151,6 @@ Example `astro-base.json`:
85
151
  { "base": "/my-docs" }
86
152
  ```
87
153
 
88
- All overrides are read fresh on every skill invocation; no caching. Edit and the next `/cloverleaf-run` picks it up.
89
-
90
154
  ### Known limitations
91
155
 
92
156
  - Concurrent `/cloverleaf-run` on the same repo may race on preview ports.
@@ -201,17 +265,17 @@ Skipping the Plan = skipping `task_batch_gate`. That's the right tradeoff for ho
201
265
 
202
266
  ## Security review
203
267
 
204
- The **Security Reviewer** (8th agent) runs when a task's effective `security_class` is `high`, off the `automated-gates` hub in both lanes — so it covers fast-lane backend work, not just full-pipeline UI work.
268
+ The **Security Reviewer** runs as a blocking council member whenever a Task's effective `security_class` is `high`. Both shipped profiles include it, so it covers fast-profile backend work, not only full-profile UI work.
205
269
 
206
- **What triggers it.** `security_class` (`low`/`high`, independent of the UI-keyed `risk_class`) is inferred at task creation from sensitive markers (keywords + paths) and re-checked against the actual diff at review time (defense in depth a task whose brief never says "credential" but whose diff touches `engine/exchange.py` is caught). Override at creation with `--security=high|low`.
270
+ **What triggers it.** `security_class` (`low`/`high`, independent of the UI-keyed `risk_class`) is inferred at task creation from sensitive markers (keywords + paths) and re-checked against the actual diff at council entry defense in depth, so a task whose brief never says "credential" but whose diff touches `engine/exchange.py` is still caught. Override at creation with `--security=high|low`.
207
271
 
208
272
  **Two passes.** (A) a deterministic secret scan (`cloverleaf-cli secret-scan`) over the diff's added lines — cloud keys, tokens, PEM headers, credentialed connection strings; (B) an LLM judgment pass reasoning about injection, broken authz, unsafe deserialization, SSRF, missing input validation, weak crypto.
209
273
 
210
- **Routing.** Findings merge into one feedback envelope; the max severity sets the verdict any `blocker` (e.g. a leaked credential) `escalated` (a human must review); `error`/`warning` `implementing` (the Implementer fixes); clean `automated-gates` onward.
274
+ **Routing.** Findings merge into one feedback envelope and the maximum severity sets the member's verdict. Any `blocker` a leaked credential, say — produces `escalate`, which is un-lowerable, so the Task lands in `escalated` for a human. An `error` or `warning` produces `bounce`, and under `any-veto` the council sends the Task back to `implementing` with the round's feedback batched. Clean produces `pass` and the council carries on.
211
275
 
212
276
  **Customizing.** Both pattern sets are consumer-overridable: `.cloverleaf/config/security-paths.json` (sensitive paths + keywords) and `.cloverleaf/config/secret-patterns.json` (secret regexes + placeholder excludes).
213
277
 
214
- **Mechanical enforcement (v0.8.1+).** As of v0.8.1, the security-review state is mechanically enforced via the `security_gate` annotation on the Standard 0.7.1 state machine. `advance-status` re-runs `classify-security` on every guarded transition and refuses bypass attempts with exit code 2, naming the required recovery action (advance to `security-review`, run the Security Reviewer, write a `pass` verdict, then retry). This is a belt-and-suspenders complement to the orchestrator prose — the CLI enforces the invariant even if the driving LLM omits the bookkeeping step.
278
+ **Mechanical enforcement (v0.8.1+).** A high-security Task cannot reach `merged` without a passing security review. Standard 0.8.0 retired the `security_gate` state-machine annotation that used to carry this, so the guarantee now rests on two mechanisms. First, the `security` member is **blocking** under `any-veto` its bounce or escalate stops the council outright. Second, `apply-council-verdict` records `security_review_verdict='pass'` on the `council final-gate` transition for high-security Tasks, and writes a `security` block into the council audit artifact naming the member's verdict, the gating verdict it set, and the basis for bothincluding the case where no security member ran at all. The audit record is what makes the guarantee inspectable after the fact, rather than only enforced in the moment.
215
279
 
216
280
  ## License
217
281
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.11.1
1
+ 0.13.0
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "profiles": {
3
- "default": {
3
+ "delivery-fast": {
4
+ "rounds": [
5
+ [ { "member": "reviewer" } ],
6
+ [ { "member": "security", "when": "security_class:high" } ]
7
+ ],
8
+ "aggregation": "any-veto",
9
+ "on_round_bounce": "stop"
10
+ },
11
+ "delivery-full": {
4
12
  "rounds": [
5
13
  [ { "member": "reviewer" } ],
6
14
  [
@@ -14,6 +22,6 @@
14
22
  }
15
23
  },
16
24
  "gates": {
17
- "task.review": "default"
25
+ "task.review": { "by": "risk_class", "map": { "low": "delivery-fast", "high": "delivery-full" } }
18
26
  }
19
27
  }
package/dist/cli.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  * load-task <repoRoot> <taskId>
9
9
  * infer-project <repoRoot>
10
10
  * next-task-id <repoRoot> [--project=<p>]
11
- * advance-status <repoRoot> <taskId> <toStatus> <actor> [gate] [path]
11
+ * advance-status <repoRoot> <taskId> <toStatus> <actor> [gate]
12
12
  * write-feedback <repoRoot> <taskId> <envelopeJsonPath>
13
13
  * latest-feedback <repoRoot> <taskId>
14
14
  * emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]
@@ -47,6 +47,7 @@
47
47
  * apply-council-verdict <repoRoot> <taskId> <gate> <councilVerdictJson>
48
48
  * chair-context <chairMemberInputsJson>
49
49
  * chair-verdict <chairRawJson> <membersJson>
50
+ * validate-council <repoRoot>
50
51
  */
51
52
  import { readFileSync, mkdirSync, copyFileSync, appendFileSync } from 'node:fs';
52
53
  import { dirname, join } from 'node:path';
@@ -77,7 +78,9 @@ import { classifyFiles, normalizePath } from './scope-check.mjs';
77
78
  import { computeRfcTasksView } from './rfc-tasks.mjs';
78
79
  import { loadSecretPatternsConfig, scanSecrets } from './secret-scan.mjs';
79
80
  import { classifyTaskSecurity } from './security-classify.mjs';
80
- import { resolveCouncilPlan, applyCouncilVerdict } from './council.mjs';
81
+ import { resolveCouncilPlan, applyCouncilVerdict, GATE_DESCRIPTORS } from './council.mjs';
82
+ import { loadCouncilConfigWithSource } from './council-config.mjs';
83
+ import { validateCouncilConfig } from '@cloverleaf/standard/validators/index.js';
81
84
  import { aggregate } from './aggregation.mjs';
82
85
  import { buildChairContext, finalizeChairVerdict } from './chair.mjs';
83
86
  function die(msg, code = 1) {
@@ -92,7 +95,7 @@ function usage(msg) {
92
95
  ' load-task <repoRoot> <taskId>\n' +
93
96
  ' infer-project <repoRoot>\n' +
94
97
  ' next-task-id <repoRoot> [--project=<p>]\n' +
95
- ' advance-status <repoRoot> <taskId> <toStatus> <actor> [gate] [path]\n' +
98
+ ' advance-status <repoRoot> <taskId> <toStatus> <actor> [gate]\n' +
96
99
  ' write-feedback <repoRoot> <taskId> <envelopeJsonPath>\n' +
97
100
  ' latest-feedback <repoRoot> <taskId>\n' +
98
101
  ' emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]\n' +
@@ -130,7 +133,8 @@ function usage(msg) {
130
133
  ' apply-council-verdict <repoRoot> <taskId> <gate> <councilVerdictJson>\n' +
131
134
  ' chair-context <chairMemberInputsJson>\n' +
132
135
  ' chair-verdict <chairRawJson> <membersJson>\n' +
133
- ' set-task-field <repoRoot> <taskId> <field> <value>\n');
136
+ ' set-task-field <repoRoot> <taskId> <field> <value>\n' +
137
+ ' validate-council <repoRoot>\n');
134
138
  process.exit(2);
135
139
  }
136
140
  const [, , command, ...rest] = process.argv;
@@ -173,9 +177,9 @@ try {
173
177
  break;
174
178
  }
175
179
  case 'advance-status': {
176
- const [repoRoot, taskId, toStatus, actorArg, gate, path] = rest;
180
+ const [repoRoot, taskId, toStatus, actorArg, gate] = rest;
177
181
  if (!repoRoot || !taskId || !toStatus || !actorArg)
178
- usage('advance-status requires <repoRoot> <taskId> <toStatus> <actor> [gate] [path]');
182
+ usage('advance-status requires <repoRoot> <taskId> <toStatus> <actor> [gate]');
179
183
  if (actorArg !== 'agent' && actorArg !== 'human') {
180
184
  die(`actor must be 'agent' or 'human' (got '${actorArg}')`, 2);
181
185
  }
@@ -183,8 +187,6 @@ try {
183
187
  const opts = {};
184
188
  if (gate)
185
189
  opts.gate = gate;
186
- if (path === 'fast_lane' || path === 'full_pipeline')
187
- opts.path = path;
188
190
  const updated = advanceStatus(repoRoot, taskId, toStatus, actor, opts);
189
191
  process.stdout.write(updated.status + '\n');
190
192
  break;
@@ -896,6 +898,23 @@ try {
896
898
  saveTask(repoRoot, task);
897
899
  break;
898
900
  }
901
+ case 'validate-council': {
902
+ const [repoRoot] = rest;
903
+ if (!repoRoot)
904
+ usage('validate-council requires <repoRoot>');
905
+ const { config } = loadCouncilConfigWithSource(repoRoot);
906
+ const gd = Object.fromEntries(Object.entries(GATE_DESCRIPTORS).map(([k, d]) => [k, { kind: d.kind ?? 'code' }]));
907
+ const result = validateCouncilConfig(config, gd);
908
+ if (result.ok) {
909
+ process.stdout.write('council config OK\n');
910
+ }
911
+ else {
912
+ for (const v of result.violations)
913
+ process.stderr.write(`${v.rule}: ${v.message}\n`);
914
+ process.exit(1);
915
+ }
916
+ break;
917
+ }
899
918
  default:
900
919
  usage(`Unknown command: ${command}`);
901
920
  }