@cloverleaf/reference-impl 0.13.1 → 0.13.3

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.13.1",
4
+ "version": "0.13.2",
5
5
  "author": {
6
6
  "name": "Renato D'Arrigo",
7
7
  "email": "renato.darrigo@gmail.com"
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.13.1
1
+ 0.13.3
package/dist/cli.mjs CHANGED
@@ -87,57 +87,70 @@ function die(msg, code = 1) {
87
87
  process.stderr.write(msg + '\n');
88
88
  process.exit(code);
89
89
  }
90
+ /**
91
+ * The command list, shared by both exits below. `--help` is a request and its
92
+ * answer is this text, so it goes to stdout and exits 0; every error path keeps
93
+ * stderr and exit 2, so a caller piping stdout gets the list or nothing.
94
+ */
95
+ const USAGE_TEXT = 'Usage: cloverleaf-cli <command> [args...]\n' +
96
+ 'Commands:\n' +
97
+ ' load-task <repoRoot> <taskId>\n' +
98
+ ' infer-project <repoRoot>\n' +
99
+ ' next-task-id <repoRoot> [--project=<p>]\n' +
100
+ ' advance-status <repoRoot> <taskId> <toStatus> <actor> [gate]\n' +
101
+ ' write-feedback <repoRoot> <taskId> <envelopeJsonPath>\n' +
102
+ ' latest-feedback <repoRoot> <taskId>\n' +
103
+ ' emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]\n' +
104
+ ' ui-review-config --repo-root <repoRoot>\n' +
105
+ ' read-ui-review-state <repoRoot> <taskId>\n' +
106
+ ' write-ui-review-state <repoRoot> <taskId> <baselines_pending>\n' +
107
+ ' write-baseline <repoRoot> <taskId> <browser> <slug> <viewport> <sourceFile>\n' +
108
+ ' plugin-root\n' +
109
+ ' load-rfc <repoRoot> <id>\n' +
110
+ ' save-rfc <repoRoot> <filePath>\n' +
111
+ ' advance-rfc <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
112
+ ' rfc-tasks <repoRoot> <rfcId> [--pretty]\n' +
113
+ ' load-spike <repoRoot> <id>\n' +
114
+ ' save-spike <repoRoot> <filePath>\n' +
115
+ ' advance-spike <repoRoot> <id> <toStatus> <agent|human>\n' +
116
+ ' load-plan <repoRoot> <id>\n' +
117
+ ' save-plan <repoRoot> <filePath>\n' +
118
+ ' advance-plan <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
119
+ ' materialise-tasks <repoRoot> <planId>\n' +
120
+ ' next-work-item-id <repoRoot> <project>\n' +
121
+ ' discovery-config --repo-root <repoRoot>\n' +
122
+ ' prep-worktree <mainRoot> <worktreePath>\n' +
123
+ ' qa-report <runs.json> <out.html>\n' +
124
+ ' dag-ready-tasks <repoRoot> <planId> <maxConcurrent>\n' +
125
+ ' dag-detect-cycle <repoRoot> <planId>\n' +
126
+ ' walk-state-read <repoRoot> <planId>\n' +
127
+ ' walk-state-write <repoRoot> <walkStateJsonPath>\n' +
128
+ ' walker-default-concurrency [--explain]\n' +
129
+ ' check-scope <repoRoot> <taskId> --branch <branchName>\n' +
130
+ ' extend-scope <repoRoot> <taskId> --add <file>... --reason <text>\n' +
131
+ ' secret-scan <repoRoot> --branch <branch>\n' +
132
+ ' classify-security <repoRoot> <taskId> [--branch <branch>]\n' +
133
+ ' council-plan <repoRoot> <taskId> [gateKey] [--changed-files=a,b,c]\n' +
134
+ ' aggregate-verdicts <membersJson> <rule> [--weighted-threshold=N]\n' +
135
+ ' apply-council-verdict <repoRoot> <taskId> <gate> <councilVerdictJson>\n' +
136
+ ' chair-context <chairMemberInputsJson>\n' +
137
+ ' chair-verdict <chairRawJson> <membersJson>\n' +
138
+ ' set-task-field <repoRoot> <taskId> <field> <value>\n' +
139
+ ' validate-council <repoRoot>\n';
90
140
  function usage(msg) {
91
141
  if (msg)
92
142
  process.stderr.write(msg + '\n');
93
- process.stderr.write('Usage: cloverleaf-cli <command> [args...]\n' +
94
- 'Commands:\n' +
95
- ' load-task <repoRoot> <taskId>\n' +
96
- ' infer-project <repoRoot>\n' +
97
- ' next-task-id <repoRoot> [--project=<p>]\n' +
98
- ' advance-status <repoRoot> <taskId> <toStatus> <actor> [gate]\n' +
99
- ' write-feedback <repoRoot> <taskId> <envelopeJsonPath>\n' +
100
- ' latest-feedback <repoRoot> <taskId>\n' +
101
- ' emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]\n' +
102
- ' ui-review-config --repo-root <repoRoot>\n' +
103
- ' read-ui-review-state <repoRoot> <taskId>\n' +
104
- ' write-ui-review-state <repoRoot> <taskId> <baselines_pending>\n' +
105
- ' write-baseline <repoRoot> <taskId> <browser> <slug> <viewport> <sourceFile>\n' +
106
- ' plugin-root\n' +
107
- ' load-rfc <repoRoot> <id>\n' +
108
- ' save-rfc <repoRoot> <filePath>\n' +
109
- ' advance-rfc <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
110
- ' rfc-tasks <repoRoot> <rfcId> [--pretty]\n' +
111
- ' load-spike <repoRoot> <id>\n' +
112
- ' save-spike <repoRoot> <filePath>\n' +
113
- ' advance-spike <repoRoot> <id> <toStatus> <agent|human>\n' +
114
- ' load-plan <repoRoot> <id>\n' +
115
- ' save-plan <repoRoot> <filePath>\n' +
116
- ' advance-plan <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
117
- ' materialise-tasks <repoRoot> <planId>\n' +
118
- ' next-work-item-id <repoRoot> <project>\n' +
119
- ' discovery-config --repo-root <repoRoot>\n' +
120
- ' prep-worktree <mainRoot> <worktreePath>\n' +
121
- ' qa-report <runs.json> <out.html>\n' +
122
- ' dag-ready-tasks <repoRoot> <planId> <maxConcurrent>\n' +
123
- ' dag-detect-cycle <repoRoot> <planId>\n' +
124
- ' walk-state-read <repoRoot> <planId>\n' +
125
- ' walk-state-write <repoRoot> <walkStateJsonPath>\n' +
126
- ' walker-default-concurrency [--explain]\n' +
127
- ' check-scope <repoRoot> <taskId> --branch <branchName>\n' +
128
- ' extend-scope <repoRoot> <taskId> --add <file>... --reason <text>\n' +
129
- ' secret-scan <repoRoot> --branch <branch>\n' +
130
- ' classify-security <repoRoot> <taskId> [--branch <branch>]\n' +
131
- ' council-plan <repoRoot> <taskId> [gateKey] [--changed-files=a,b,c]\n' +
132
- ' aggregate-verdicts <membersJson> <rule> [--weighted-threshold=N]\n' +
133
- ' apply-council-verdict <repoRoot> <taskId> <gate> <councilVerdictJson>\n' +
134
- ' chair-context <chairMemberInputsJson>\n' +
135
- ' chair-verdict <chairRawJson> <membersJson>\n' +
136
- ' set-task-field <repoRoot> <taskId> <field> <value>\n' +
137
- ' validate-council <repoRoot>\n');
143
+ process.stderr.write(USAGE_TEXT);
138
144
  process.exit(2);
139
145
  }
146
+ function help() {
147
+ process.stdout.write(USAGE_TEXT);
148
+ process.exit(0);
149
+ }
140
150
  const [, , command, ...rest] = process.argv;
151
+ if (command === '--help' || command === '-h') {
152
+ help();
153
+ }
141
154
  if (!command) {
142
155
  usage('Error: no command given');
143
156
  }
package/lib/cli.ts CHANGED
@@ -92,60 +92,75 @@ function die(msg: string, code = 1): never {
92
92
  process.exit(code);
93
93
  }
94
94
 
95
+ /**
96
+ * The command list, shared by both exits below. `--help` is a request and its
97
+ * answer is this text, so it goes to stdout and exits 0; every error path keeps
98
+ * stderr and exit 2, so a caller piping stdout gets the list or nothing.
99
+ */
100
+ const USAGE_TEXT =
101
+ 'Usage: cloverleaf-cli <command> [args...]\n' +
102
+ 'Commands:\n' +
103
+ ' load-task <repoRoot> <taskId>\n' +
104
+ ' infer-project <repoRoot>\n' +
105
+ ' next-task-id <repoRoot> [--project=<p>]\n' +
106
+ ' advance-status <repoRoot> <taskId> <toStatus> <actor> [gate]\n' +
107
+ ' write-feedback <repoRoot> <taskId> <envelopeJsonPath>\n' +
108
+ ' latest-feedback <repoRoot> <taskId>\n' +
109
+ ' emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]\n' +
110
+ ' ui-review-config --repo-root <repoRoot>\n' +
111
+ ' read-ui-review-state <repoRoot> <taskId>\n' +
112
+ ' write-ui-review-state <repoRoot> <taskId> <baselines_pending>\n' +
113
+ ' write-baseline <repoRoot> <taskId> <browser> <slug> <viewport> <sourceFile>\n' +
114
+ ' plugin-root\n' +
115
+ ' load-rfc <repoRoot> <id>\n' +
116
+ ' save-rfc <repoRoot> <filePath>\n' +
117
+ ' advance-rfc <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
118
+ ' rfc-tasks <repoRoot> <rfcId> [--pretty]\n' +
119
+ ' load-spike <repoRoot> <id>\n' +
120
+ ' save-spike <repoRoot> <filePath>\n' +
121
+ ' advance-spike <repoRoot> <id> <toStatus> <agent|human>\n' +
122
+ ' load-plan <repoRoot> <id>\n' +
123
+ ' save-plan <repoRoot> <filePath>\n' +
124
+ ' advance-plan <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
125
+ ' materialise-tasks <repoRoot> <planId>\n' +
126
+ ' next-work-item-id <repoRoot> <project>\n' +
127
+ ' discovery-config --repo-root <repoRoot>\n' +
128
+ ' prep-worktree <mainRoot> <worktreePath>\n' +
129
+ ' qa-report <runs.json> <out.html>\n' +
130
+ ' dag-ready-tasks <repoRoot> <planId> <maxConcurrent>\n' +
131
+ ' dag-detect-cycle <repoRoot> <planId>\n' +
132
+ ' walk-state-read <repoRoot> <planId>\n' +
133
+ ' walk-state-write <repoRoot> <walkStateJsonPath>\n' +
134
+ ' walker-default-concurrency [--explain]\n' +
135
+ ' check-scope <repoRoot> <taskId> --branch <branchName>\n' +
136
+ ' extend-scope <repoRoot> <taskId> --add <file>... --reason <text>\n' +
137
+ ' secret-scan <repoRoot> --branch <branch>\n' +
138
+ ' classify-security <repoRoot> <taskId> [--branch <branch>]\n' +
139
+ ' council-plan <repoRoot> <taskId> [gateKey] [--changed-files=a,b,c]\n' +
140
+ ' aggregate-verdicts <membersJson> <rule> [--weighted-threshold=N]\n' +
141
+ ' apply-council-verdict <repoRoot> <taskId> <gate> <councilVerdictJson>\n' +
142
+ ' chair-context <chairMemberInputsJson>\n' +
143
+ ' chair-verdict <chairRawJson> <membersJson>\n' +
144
+ ' set-task-field <repoRoot> <taskId> <field> <value>\n' +
145
+ ' validate-council <repoRoot>\n';
146
+
95
147
  function usage(msg?: string): never {
96
148
  if (msg) process.stderr.write(msg + '\n');
97
- process.stderr.write(
98
- 'Usage: cloverleaf-cli <command> [args...]\n' +
99
- 'Commands:\n' +
100
- ' load-task <repoRoot> <taskId>\n' +
101
- ' infer-project <repoRoot>\n' +
102
- ' next-task-id <repoRoot> [--project=<p>]\n' +
103
- ' advance-status <repoRoot> <taskId> <toStatus> <actor> [gate]\n' +
104
- ' write-feedback <repoRoot> <taskId> <envelopeJsonPath>\n' +
105
- ' latest-feedback <repoRoot> <taskId>\n' +
106
- ' emit-gate-decision <repoRoot> <workItemId> <gate> <decision> <actor> [--comment=<str>]\n' +
107
- ' ui-review-config --repo-root <repoRoot>\n' +
108
- ' read-ui-review-state <repoRoot> <taskId>\n' +
109
- ' write-ui-review-state <repoRoot> <taskId> <baselines_pending>\n' +
110
- ' write-baseline <repoRoot> <taskId> <browser> <slug> <viewport> <sourceFile>\n' +
111
- ' plugin-root\n' +
112
- ' load-rfc <repoRoot> <id>\n' +
113
- ' save-rfc <repoRoot> <filePath>\n' +
114
- ' advance-rfc <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
115
- ' rfc-tasks <repoRoot> <rfcId> [--pretty]\n' +
116
- ' load-spike <repoRoot> <id>\n' +
117
- ' save-spike <repoRoot> <filePath>\n' +
118
- ' advance-spike <repoRoot> <id> <toStatus> <agent|human>\n' +
119
- ' load-plan <repoRoot> <id>\n' +
120
- ' save-plan <repoRoot> <filePath>\n' +
121
- ' advance-plan <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
122
- ' materialise-tasks <repoRoot> <planId>\n' +
123
- ' next-work-item-id <repoRoot> <project>\n' +
124
- ' discovery-config --repo-root <repoRoot>\n' +
125
- ' prep-worktree <mainRoot> <worktreePath>\n' +
126
- ' qa-report <runs.json> <out.html>\n' +
127
- ' dag-ready-tasks <repoRoot> <planId> <maxConcurrent>\n' +
128
- ' dag-detect-cycle <repoRoot> <planId>\n' +
129
- ' walk-state-read <repoRoot> <planId>\n' +
130
- ' walk-state-write <repoRoot> <walkStateJsonPath>\n' +
131
- ' walker-default-concurrency [--explain]\n' +
132
- ' check-scope <repoRoot> <taskId> --branch <branchName>\n' +
133
- ' extend-scope <repoRoot> <taskId> --add <file>... --reason <text>\n' +
134
- ' secret-scan <repoRoot> --branch <branch>\n' +
135
- ' classify-security <repoRoot> <taskId> [--branch <branch>]\n' +
136
- ' council-plan <repoRoot> <taskId> [gateKey] [--changed-files=a,b,c]\n' +
137
- ' aggregate-verdicts <membersJson> <rule> [--weighted-threshold=N]\n' +
138
- ' apply-council-verdict <repoRoot> <taskId> <gate> <councilVerdictJson>\n' +
139
- ' chair-context <chairMemberInputsJson>\n' +
140
- ' chair-verdict <chairRawJson> <membersJson>\n' +
141
- ' set-task-field <repoRoot> <taskId> <field> <value>\n' +
142
- ' validate-council <repoRoot>\n'
143
- );
149
+ process.stderr.write(USAGE_TEXT);
144
150
  process.exit(2);
145
151
  }
146
152
 
153
+ function help(): never {
154
+ process.stdout.write(USAGE_TEXT);
155
+ process.exit(0);
156
+ }
157
+
147
158
  const [, , command, ...rest] = process.argv;
148
159
 
160
+ if (command === '--help' || command === '-h') {
161
+ help();
162
+ }
163
+
149
164
  if (!command) {
150
165
  usage('Error: no command given');
151
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloverleaf/reference-impl",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "description": "Reference implementation of the Cloverleaf methodology as Claude Code skills. Implements the Tight Loop (Implementer + Reviewer).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -145,10 +145,18 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
145
145
  # to see why and return verdict `escalate` — do NOT continue with a broken install.
146
146
  npx astro preferences disable devToolbar > /tmp/ui-devtoolbar.log 2>&1; echo "EXIT=$?"
147
147
  # Non-zero EXIT means this UI directory is not an Astro project — see below.
148
- npm run dev -- --port={{preview_port}} &
148
+ setsid npm run dev -- --port={{preview_port}} < /dev/null > /tmp/ui-dev-server.log 2>&1 &
149
149
  SERVER_PID=$!
150
150
  ```
151
151
 
152
+ `setsid` is load-bearing, not decoration. `$SERVER_PID` is **npm's** PID and
153
+ npm runs the dev server as a child, so `kill $SERVER_PID` reaps npm and leaves
154
+ the server holding the port, reparented to init. `setsid` puts the whole tree
155
+ in its own process group whose id equals `$SERVER_PID`, which is what lets
156
+ step 13 kill the group. Keep the two together: without `setsid` the group kill
157
+ silently matches nothing, because the job would otherwise sit in your shell's
158
+ group and `$SERVER_PID` would not be a group id at all.
159
+
152
160
  `astro preferences disable devToolbar` is **project-scoped** by default: it writes into `$WT/site/.astro/`, which step 13 deletes along with the worktree, so it turns the toolbar off for this capture alone and nothing outside this run changes. Never pass `--global` — that writes to the operator's home directory and silently changes every other Astro project on the machine. Disable it before backgrounding the server, not after: Astro decides whether to inject the toolbar when the dev server boots.
153
161
 
154
162
  If that command exits non-zero, this UI directory is **not an Astro project**. Disable that toolchain's own dev overlay instead. If it has none you can turn off, emit an `info` finding recording that the baseline may contain dev-only UI rather than capturing a contaminated one silently.
@@ -159,13 +167,27 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
159
167
  node "$DRIVER" > /tmp/ui-driver.log 2>&1; echo "EXIT=$?"
160
168
  ```
161
169
 
162
- 4. Wait up to 30s for `http://localhost:{{preview_port}}/` to respond 200. If the server fails to start in 30s, run teardown (step 13) `kill $SERVER_PID`, never a command-line pattern and return verdict `escalate`.
163
-
164
- 5. Determine the site base path:
165
- 1. Check `{{repo_root}}/.cloverleaf/config/astro-base.json`. Expected shape: `{ "base": "<path>" }`. If present, use the `base` field verbatim and skip to step 6. (Consumer override — checked before parsing astro config.)
170
+ 4. Determine the site base path. This step runs **before** the readiness gate because the gate probes the base, and it needs no server at all every branch below is pure filesystem.
171
+ 1. Check `{{repo_root}}/.cloverleaf/config/astro-base.json`. Expected shape: `{ "base": "<path>" }`. If present, use the `base` field verbatim and skip to step 5. (Consumer override — checked before parsing astro config.)
166
172
  2. Otherwise, attempt to locate and parse an astro config file (common locations: `site/astro.config.mjs`, `astro.config.mjs` at repo root, `apps/web/astro.config.mjs`). Best-effort fallback.
167
173
  3. If both fail, treat base as empty string.
168
174
 
175
+ 5. Wait up to 30s for the dev server to answer on `http://localhost:{{preview_port}}<base>/`. Keep the trailing slash: a site configured `trailingSlash: 'always'` serves `<base>/` and returns 404 for `<base>`.
176
+
177
+ **Readiness means the server ANSWERS — not that it answers 200.** A dev server for a based site returns 404 on every path outside its base, so a probe that insists on 200 can burn the entire 30s budget against a server that is up and serving. Judge the connection, not the status:
178
+
179
+ - **Any HTTP status — 404 included — means the server is up.** Stop waiting and go on.
180
+ - **A refused connection is the only "not up yet."** Keep waiting until the budget is spent.
181
+ - **An answer that is not 200 means the server is up but the base is probably wrong.** Go on — step 8's navigation already retries without the base — and emit one `severity: "warning"` finding with `rule: "ui-review-base"` naming the probed URL and the status. A wrong base then shows up as a finding instead of only as surprising pixels. Keep it at `warning`: step 11 gates on `blocker`/`error` only, so this reports without bouncing the task.
182
+
183
+ Probe with the status as the payload — a refused connection prints `000`, which is what distinguishes it from a real 404:
184
+
185
+ ```bash
186
+ curl -s -o /dev/null -w '%{http_code}' "http://localhost:{{preview_port}}<base>/"
187
+ ```
188
+
189
+ If the server fails to start in 30s, read `/tmp/ui-dev-server.log` to see why, run teardown (step 13) — `kill -- -$SERVER_PID`, never a command-line pattern — and return verdict `escalate`.
190
+
169
191
  6. **Apply maxCombinations cap** (when `affected_routes` is a list, not `"all"`):
170
192
  - Compute `routes × viewports × browsers`. Use diff line counts as proxy for route diff size.
171
193
  - Call `applyMaxCombinationsCap` from `lib/ui-browser.ts`.
@@ -252,13 +274,15 @@ Do not attempt to launch a missing engine — fail fast with `verdict: "escalate
252
274
 
253
275
  13. Teardown:
254
276
  ```bash
255
- kill $SERVER_PID 2>/dev/null || true
277
+ kill -- -$SERVER_PID 2>/dev/null || true
256
278
  rm -f "$DRIVER"
257
279
  cd {{repo_root}}
258
280
  git worktree remove --force "$WT"
259
281
  ```
260
282
 
261
- Kill the server by the PID you captured in step 3, never by command-line pattern. `pkill -f "astro dev"` also matches the command line of the shell running it, so the shell kills itself: exit 144, and every command after it in the same compound statement the `rm -f` and `git worktree remove` above silently never runs.
283
+ Kill the process **group** you created in step 3, never by command-line pattern. The leading `-` in `-$SERVER_PID` is what makes this a group kill; `kill $SERVER_PID` without it reaps only npm and leaves the dev server orphaned on the port, which the next run then fails to bind.
284
+
285
+ `pkill -f "astro dev"` also matches the command line of the shell running it, so the shell kills itself: exit 144, and every command after it in the same compound statement — the `rm -f` and `git worktree remove` above — silently never runs.
262
286
 
263
287
  Delete every driver script you wrote, wherever you put it. `git worktree remove --force` only clears what lives inside `$WT`; a driver written anywhere else survives the run and leaks into the next one.
264
288