@cloverleaf/reference-impl 0.12.0 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +101 -37
- package/VERSION +1 -1
- package/config/council.json +10 -2
- package/dist/cli.mjs +27 -8
- package/dist/council.mjs +285 -64
- package/dist/events.mjs +3 -8
- package/dist/ids.mjs +7 -0
- package/dist/qa-rules.mjs +19 -3
- package/dist/task.mjs +6 -25
- package/dist/work-item.mjs +2 -3
- package/lib/cli.ts +28 -8
- package/lib/council-config.ts +1 -0
- package/lib/council.ts +343 -67
- package/lib/events.ts +4 -8
- package/lib/ids.ts +7 -0
- package/lib/qa-rules.ts +24 -3
- package/lib/task.ts +7 -33
- package/lib/work-item.ts +2 -4
- package/package.json +5 -3
- package/prompts/documenter.md +1 -1
- package/prompts/implementer.md +8 -2
- package/prompts/qa.md +6 -9
- package/prompts/reviewer.md +4 -2
- package/prompts/ui-reviewer.md +83 -11
- package/skills/cloverleaf-approve-baselines/SKILL.md +18 -26
- package/skills/cloverleaf-discover/SKILL.md +7 -1
- package/skills/cloverleaf-document/SKILL.md +5 -6
- package/skills/cloverleaf-implement/SKILL.md +24 -30
- package/skills/cloverleaf-merge/SKILL.md +14 -23
- package/skills/cloverleaf-new-task/SKILL.md +23 -4
- package/skills/cloverleaf-qa/SKILL.md +25 -24
- package/skills/cloverleaf-review/SKILL.md +24 -18
- package/skills/cloverleaf-run/SKILL.md +45 -106
- package/skills/cloverleaf-run-plan/SKILL.md +19 -26
- package/skills/cloverleaf-security-review/SKILL.md +22 -17
- package/skills/cloverleaf-ui-review/SKILL.md +59 -35
|
@@ -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.
|
|
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.
|
|
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
|
-
##
|
|
18
|
+
## How delivery works
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Every Task walks one path, whatever its risk:
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
```
|
|
23
|
+
pending → tactical-plan → implementing → documenting → council → final-gate → merged
|
|
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
|
|
48
|
+
| Documenter | Real | Subagent, doc-only commits per file-path rules |
|
|
31
49
|
| Reviewer | Real | Subagent, read-only review of diff |
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
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` —
|
|
41
|
-
- `/cloverleaf-document` —
|
|
42
|
-
- `/cloverleaf-
|
|
43
|
-
- `/cloverleaf-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
- `/cloverleaf-review` — Reviewer
|
|
77
|
+
- `/cloverleaf-security-review` — Security Reviewer
|
|
78
|
+
- `/cloverleaf-ui-review` — UI Reviewer
|
|
79
|
+
- `/cloverleaf-qa` — QA
|
|
54
80
|
|
|
55
|
-
|
|
56
|
-
- `config/qa-rules.json` — per-package test commands
|
|
81
|
+
**Human gates:**
|
|
57
82
|
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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**
|
|
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
|
|
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
|
|
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+).**
|
|
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 both — including 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.
|
|
1
|
+
0.13.0
|
package/config/council.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"profiles": {
|
|
3
|
-
"
|
|
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": "
|
|
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]
|
|
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]
|
|
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
|
|
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]
|
|
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
|
}
|