@cloverleaf/reference-impl 0.6.2 → 0.6.4

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/README.md CHANGED
@@ -152,7 +152,7 @@ The Reviewer never switches branches. It reads files via `git show` and runs tes
152
152
 
153
153
  ## Package layout
154
154
 
155
- - `lib/` — TypeScript library used by the CLI. State, events, feedback, IDs, paths. Includes `buildBaselinePath(repoRoot, browser, slug, viewport)` (`lib/visual-diff.ts`) for constructing canonical baseline paths under `.cloverleaf/baselines/{browser}/`. `lib/ui-browser.ts` exports `buildBrowserEscalationFinding` and `applyMaxCombinationsCap` (used by the UI Reviewer prompt for per-engine escalation and combination-count capping). `lib/ui-review-state.ts` exports `readUiReviewState`, `writeUiReviewState`, and `uiReviewStatePath` — the baseline-approval sidecar API for `.cloverleaf/runs/{taskId}/ui-review/state.json`. The CLI exposes `write-baseline <repoRoot> <taskId> <browser> <slug> <viewport> <sourceFile>` as the safe write path for baselines; it enforces the `baselines_pending` guard and uses `buildBaselinePath` internally.
155
+ - `lib/` — TypeScript library used by the CLI. State, events, feedback, IDs, paths. Includes `buildBaselinePath(repoRoot, browser, slug, viewport)` (`lib/visual-diff.ts`) for constructing canonical baseline paths under `.cloverleaf/baselines/{browser}/`. `lib/ui-browser.ts` exports `buildBrowserEscalationFinding` and `applyMaxCombinationsCap` (used by the UI Reviewer prompt for per-engine escalation and combination-count capping). `lib/ui-review-state.ts` exports `readUiReviewState`, `writeUiReviewState`, and `uiReviewStatePath` — the baseline-approval sidecar API for `.cloverleaf/runs/{taskId}/ui-review/state.json`. The CLI exposes `write-baseline <repoRoot> <taskId> <browser> <slug> <viewport> <sourceFile>` as the safe write path for baselines; it enforces the `baselines_pending` guard and uses `buildBaselinePath` internally. `lib/walker-config.ts` exports `loadWalkerConfig()` — reads `~/.config/cloverleaf/walker.json` (XDG-aware) for the user-level `max_concurrent` override; exposed via `cloverleaf-cli walker-default-concurrency [--explain]`.
156
156
  - `skills/` — Claude Code skill markdown files.
157
157
  - `prompts/` — Implementer/Reviewer subagent system prompts.
158
158
  - `examples/toy-repo/` — standalone demo repo.
package/dist/cli.mjs CHANGED
@@ -34,6 +34,7 @@
34
34
  * dag-detect-cycle <repoRoot> <planId>
35
35
  * walk-state-read <repoRoot> <planId>
36
36
  * walk-state-write <repoRoot> <walkStateJsonPath>
37
+ * walker-default-concurrency [--explain]
37
38
  */
38
39
  import { readFileSync, mkdirSync, copyFileSync } from 'node:fs';
39
40
  import { dirname } from 'node:path';
@@ -58,6 +59,7 @@ import { readUiReviewState, writeUiReviewState } from './ui-review-state.mjs';
58
59
  import { buildBaselinePath } from './visual-diff.mjs';
59
60
  import { computeReadyTasks, detectCycle } from './dag-walker.mjs';
60
61
  import { readWalkState, writeWalkState, walkStatePath } from './walk-state.mjs';
62
+ import { loadWalkerConfig } from './walker-config.mjs';
61
63
  function die(msg, code = 1) {
62
64
  process.stderr.write(msg + '\n');
63
65
  process.exit(code);
@@ -95,7 +97,8 @@ function usage(msg) {
95
97
  ' dag-ready-tasks <repoRoot> <planId> <maxConcurrent>\n' +
96
98
  ' dag-detect-cycle <repoRoot> <planId>\n' +
97
99
  ' walk-state-read <repoRoot> <planId>\n' +
98
- ' walk-state-write <repoRoot> <walkStateJsonPath>\n');
100
+ ' walk-state-write <repoRoot> <walkStateJsonPath>\n' +
101
+ ' walker-default-concurrency [--explain]\n');
99
102
  process.exit(2);
100
103
  }
101
104
  const [, , command, ...rest] = process.argv;
@@ -485,6 +488,26 @@ try {
485
488
  writeWalkState(repoRoot, state);
486
489
  break;
487
490
  }
491
+ case 'walker-default-concurrency': {
492
+ const explain = rest.includes('--explain');
493
+ let cfg;
494
+ try {
495
+ cfg = loadWalkerConfig();
496
+ }
497
+ catch (err) {
498
+ const msg = err instanceof Error ? err.message : String(err);
499
+ process.stderr.write(msg + '\n');
500
+ process.exit(1);
501
+ }
502
+ if (explain) {
503
+ const location = cfg.source === 'user' ? `from ${cfg.path}` : 'default';
504
+ process.stdout.write(`max_concurrent=${cfg.maxConcurrent} (${location})\n`);
505
+ }
506
+ else {
507
+ process.stdout.write(`${cfg.maxConcurrent}\n`);
508
+ }
509
+ process.exit(0);
510
+ }
488
511
  default:
489
512
  usage(`Unknown command: ${command}`);
490
513
  }
@@ -115,6 +115,7 @@ export function prepWorktree(mainRoot, worktreePath) {
115
115
  // EEXIST on vite/node_modules/.bin on second invocation).
116
116
  primeCopy(mainStandardNm, wtStandardNm);
117
117
  primeCopy(mainRefImplNm, wtRefImplNm);
118
+ primeCopy(join(resolvedMain, 'reference-impl', 'dist'), join(worktreePath, 'reference-impl', 'dist'));
118
119
  execSync('npm run build', {
119
120
  cwd: join(worktreePath, 'standard'),
120
121
  stdio: 'pipe',
@@ -0,0 +1,63 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Resolve the absolute path to the walker config file.
6
+ * Respects XDG_CONFIG_HOME if set; otherwise falls back to $HOME/.config.
7
+ */
8
+ export function walkerConfigPath() {
9
+ const xdgConfigHome = process.env['XDG_CONFIG_HOME'] ?? join(homedir(), '.config');
10
+ return join(xdgConfigHome, 'cloverleaf', 'walker.json');
11
+ }
12
+ /**
13
+ * Load and validate the walker configuration.
14
+ *
15
+ * Resolution rules:
16
+ * - File absent (ENOENT) → { maxConcurrent: 3, source: 'default' }
17
+ * - File present, no max_concurrent field → { maxConcurrent: 3, source: 'default' }
18
+ * - File present, valid positive integer → { maxConcurrent: N, source: 'user' }
19
+ * - Malformed JSON → throws with file path and parser detail
20
+ * - Invalid max_concurrent (0, negative, float, string, null) → throws with field path and actual value
21
+ *
22
+ * No AJV dependency — hand-rolled validation.
23
+ */
24
+ export function loadWalkerConfig() {
25
+ const configPath = walkerConfigPath();
26
+ let raw;
27
+ try {
28
+ raw = readFileSync(configPath, 'utf-8');
29
+ }
30
+ catch (err) {
31
+ const e = err;
32
+ if (e.code === 'ENOENT') {
33
+ return { maxConcurrent: 3, source: 'default', path: configPath };
34
+ }
35
+ throw err;
36
+ }
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(raw);
40
+ }
41
+ catch (err) {
42
+ const detail = err instanceof Error ? err.message : String(err);
43
+ throw new Error(`walker config at ${configPath} contains malformed JSON: ${detail}`);
44
+ }
45
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
46
+ throw new Error(`walker config at ${configPath}: expected a JSON object, got ${JSON.stringify(parsed)}`);
47
+ }
48
+ const doc = parsed;
49
+ if (!('max_concurrent' in doc)) {
50
+ return { maxConcurrent: 3, source: 'default', path: configPath };
51
+ }
52
+ const value = doc['max_concurrent'];
53
+ if (typeof value !== 'number') {
54
+ throw new Error(`walker config at ${configPath}: max_concurrent must be a positive integer, got ${JSON.stringify(value)}`);
55
+ }
56
+ if (!Number.isInteger(value)) {
57
+ throw new Error(`walker config at ${configPath}: max_concurrent must be a positive integer (no floats), got ${value}`);
58
+ }
59
+ if (value <= 0) {
60
+ throw new Error(`walker config at ${configPath}: max_concurrent must be a positive integer (> 0), got ${value}`);
61
+ }
62
+ return { maxConcurrent: value, source: 'user', path: configPath };
63
+ }
package/lib/cli.ts CHANGED
@@ -34,6 +34,7 @@
34
34
  * dag-detect-cycle <repoRoot> <planId>
35
35
  * walk-state-read <repoRoot> <planId>
36
36
  * walk-state-write <repoRoot> <walkStateJsonPath>
37
+ * walker-default-concurrency [--explain]
37
38
  */
38
39
 
39
40
  import { readFileSync, mkdirSync, copyFileSync } from 'node:fs';
@@ -60,6 +61,7 @@ import { readUiReviewState, writeUiReviewState } from './ui-review-state.js';
60
61
  import { buildBaselinePath } from './visual-diff.js';
61
62
  import { computeReadyTasks, detectCycle } from './dag-walker.js';
62
63
  import { readWalkState, writeWalkState, walkStatePath } from './walk-state.js';
64
+ import { loadWalkerConfig } from './walker-config.js';
63
65
 
64
66
  function die(msg: string, code = 1): never {
65
67
  process.stderr.write(msg + '\n');
@@ -99,7 +101,8 @@ function usage(msg?: string): never {
99
101
  ' dag-ready-tasks <repoRoot> <planId> <maxConcurrent>\n' +
100
102
  ' dag-detect-cycle <repoRoot> <planId>\n' +
101
103
  ' walk-state-read <repoRoot> <planId>\n' +
102
- ' walk-state-write <repoRoot> <walkStateJsonPath>\n'
104
+ ' walk-state-write <repoRoot> <walkStateJsonPath>\n' +
105
+ ' walker-default-concurrency [--explain]\n'
103
106
  );
104
107
  process.exit(2);
105
108
  }
@@ -499,6 +502,26 @@ try {
499
502
  break;
500
503
  }
501
504
 
505
+ case 'walker-default-concurrency': {
506
+ const explain = rest.includes('--explain');
507
+ let cfg: { maxConcurrent: number; source: 'user' | 'default'; path: string };
508
+ try {
509
+ cfg = loadWalkerConfig();
510
+ } catch (err: unknown) {
511
+ const msg = err instanceof Error ? err.message : String(err);
512
+ process.stderr.write(msg + '\n');
513
+ process.exit(1);
514
+ }
515
+ if (explain) {
516
+ const location =
517
+ cfg.source === 'user' ? `from ${cfg.path}` : 'default';
518
+ process.stdout.write(`max_concurrent=${cfg.maxConcurrent} (${location})\n`);
519
+ } else {
520
+ process.stdout.write(`${cfg.maxConcurrent}\n`);
521
+ }
522
+ process.exit(0);
523
+ }
524
+
502
525
  default:
503
526
  usage(`Unknown command: ${command}`);
504
527
  }
@@ -128,6 +128,7 @@ export function prepWorktree(mainRoot: string, worktreePath: string): void {
128
128
  // EEXIST on vite/node_modules/.bin on second invocation).
129
129
  primeCopy(mainStandardNm, wtStandardNm);
130
130
  primeCopy(mainRefImplNm, wtRefImplNm);
131
+ primeCopy(join(resolvedMain, 'reference-impl', 'dist'), join(worktreePath, 'reference-impl', 'dist'));
131
132
 
132
133
  execSync('npm run build', {
133
134
  cwd: join(worktreePath, 'standard'),
@@ -0,0 +1,90 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ export interface WalkerConfig {
6
+ maxConcurrent: number;
7
+ source: 'user' | 'default';
8
+ /** Absolute resolved path to the walker.json file (whether it exists or not). */
9
+ path: string;
10
+ }
11
+
12
+ /**
13
+ * Resolve the absolute path to the walker config file.
14
+ * Respects XDG_CONFIG_HOME if set; otherwise falls back to $HOME/.config.
15
+ */
16
+ export function walkerConfigPath(): string {
17
+ const xdgConfigHome = process.env['XDG_CONFIG_HOME'] ?? join(homedir(), '.config');
18
+ return join(xdgConfigHome, 'cloverleaf', 'walker.json');
19
+ }
20
+
21
+ /**
22
+ * Load and validate the walker configuration.
23
+ *
24
+ * Resolution rules:
25
+ * - File absent (ENOENT) → { maxConcurrent: 3, source: 'default' }
26
+ * - File present, no max_concurrent field → { maxConcurrent: 3, source: 'default' }
27
+ * - File present, valid positive integer → { maxConcurrent: N, source: 'user' }
28
+ * - Malformed JSON → throws with file path and parser detail
29
+ * - Invalid max_concurrent (0, negative, float, string, null) → throws with field path and actual value
30
+ *
31
+ * No AJV dependency — hand-rolled validation.
32
+ */
33
+ export function loadWalkerConfig(): WalkerConfig {
34
+ const configPath = walkerConfigPath();
35
+
36
+ let raw: string;
37
+ try {
38
+ raw = readFileSync(configPath, 'utf-8');
39
+ } catch (err: unknown) {
40
+ const e = err as NodeJS.ErrnoException;
41
+ if (e.code === 'ENOENT') {
42
+ return { maxConcurrent: 3, source: 'default', path: configPath };
43
+ }
44
+ throw err;
45
+ }
46
+
47
+ let parsed: unknown;
48
+ try {
49
+ parsed = JSON.parse(raw);
50
+ } catch (err: unknown) {
51
+ const detail = err instanceof Error ? err.message : String(err);
52
+ throw new Error(
53
+ `walker config at ${configPath} contains malformed JSON: ${detail}`
54
+ );
55
+ }
56
+
57
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
58
+ throw new Error(
59
+ `walker config at ${configPath}: expected a JSON object, got ${JSON.stringify(parsed)}`
60
+ );
61
+ }
62
+
63
+ const doc = parsed as Record<string, unknown>;
64
+
65
+ if (!('max_concurrent' in doc)) {
66
+ return { maxConcurrent: 3, source: 'default', path: configPath };
67
+ }
68
+
69
+ const value = doc['max_concurrent'];
70
+
71
+ if (typeof value !== 'number') {
72
+ throw new Error(
73
+ `walker config at ${configPath}: max_concurrent must be a positive integer, got ${JSON.stringify(value)}`
74
+ );
75
+ }
76
+
77
+ if (!Number.isInteger(value)) {
78
+ throw new Error(
79
+ `walker config at ${configPath}: max_concurrent must be a positive integer (no floats), got ${value}`
80
+ );
81
+ }
82
+
83
+ if (value <= 0) {
84
+ throw new Error(
85
+ `walker config at ${configPath}: max_concurrent must be a positive integer (> 0), got ${value}`
86
+ );
87
+ }
88
+
89
+ return { maxConcurrent: value, source: 'user', path: configPath };
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloverleaf/reference-impl",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit",
46
46
  "build": "tsc -p tsconfig.build.json && node scripts/rename-to-mjs.mjs",
47
47
  "acceptance:walker": "bash scripts/acceptance-walker.sh",
48
- "prepublishOnly": "npm test && npm run build"
48
+ "prepublishOnly": "node scripts/check-standard-prepped.mjs && npm test && npm run build"
49
49
  },
50
50
  "dependencies": {
51
51
  "@cloverleaf/standard": "^0.4.0",
@@ -7,7 +7,9 @@ description: End-to-end orchestrator. Reads task.risk_class to dispatch fast lan
7
7
 
8
8
  ## Branch discipline
9
9
 
10
- Each sub-skill runs from `main`. Between steps, confirm branch is `main` before proceeding. All sub-skills return the user to `main`.
10
+ `<repo_root>` is `$(git rev-parse --show-toplevel)` of your current working directory. In walker context (Session B inside `$WORKTREE_ROOT`), this is the worktree path, NOT the primary repo. Pass `<repo_root>` explicitly to every `cloverleaf-cli` invocation, and run `git add .cloverleaf/ && git commit` from `<repo_root>` so state-advance commits land on the worktree's current branch (`cloverleaf/<TASK-ID>` in walker mode).
11
+
12
+ Do NOT `git checkout main` from a walker worktree — main is held by the primary repo. To compare against main, use `git diff main..HEAD` or `git show main:<path>`. Sub-skills run from the worktree's current branch and stay on it; the walker (in the primary repo) does the final merge to main itself after all tasks reach final-gate.
11
13
 
12
14
  ## Per-agent bounce budget
13
15
 
@@ -20,9 +20,27 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
20
20
 
21
21
  1. Capture the `<PLAN-ID>` argument and optional flags:
22
22
 
23
- - `--max-concurrent=N` — cap simultaneous sessions. Default `3`. Setting `--max-concurrent=1` yields serial behaviour.
23
+ - `--max-concurrent=N` — cap simultaneous sessions. Default `3` (resolved via `cloverleaf-cli walker-default-concurrency`). Setting `--max-concurrent=1` yields serial behaviour.
24
24
  - `--reset` — wipe `.cloverleaf/runs/plan/<PLAN-ID>/walk-state.json` and start fresh.
25
25
 
26
+ Resolve `MAX` and print exactly one startup info line:
27
+
28
+ ```bash
29
+ if [ -n "$MAX_FLAG" ]; then
30
+ MAX="$MAX_FLAG"
31
+ echo "max_concurrent=$MAX (from --max-concurrent flag)"
32
+ else
33
+ if ! MAX=$(cloverleaf-cli walker-default-concurrency); then
34
+ echo "ERROR: cloverleaf-cli walker-default-concurrency failed."
35
+ echo "Fix or remove \`~/.config/cloverleaf/walker.json\` and retry."
36
+ exit 1
37
+ fi
38
+ cloverleaf-cli walker-default-concurrency --explain
39
+ fi
40
+ ```
41
+
42
+ If `cloverleaf-cli walker-default-concurrency` exits non-zero (e.g. malformed `${XDG_CONFIG_HOME:-$HOME/.config}/cloverleaf/walker.json`), the walker stops and reports the error: **Fix or remove `~/.config/cloverleaf/walker.json` and retry.** This mirrors the step-0 uncommitted-changes stop-and-report pattern.
43
+
26
44
  2. **Guard against cycles.**
27
45
 
28
46
  ```bash
@@ -178,6 +196,18 @@ description: Autonomous DAG walker for Cloverleaf Plans. Given a PLAN-ID in stat
178
196
 
179
197
  If every task in the plan's `task_dag.nodes` has `state: "merged"`, print: "✓ Plan `<PLAN-ID>` complete."
180
198
 
199
+ ## Next steps (release publishing)
200
+
201
+ Once all tasks are merged, run the following commands in order to tag and publish the release:
202
+
203
+ ```bash
204
+ git tag -a reference-impl-v<VERSION> -m "reference-impl v<VERSION>"
205
+ git push origin main
206
+ git push origin reference-impl-v<VERSION>
207
+ (cd reference-impl && npm publish --access public)
208
+ gh release create reference-impl-v<VERSION> --title "reference-impl v<VERSION>" --notes-from-tag
209
+ ```
210
+
181
211
  ## Session brief template
182
212
 
183
213
  The walker constructs a per-task `scenario_brief` roughly like:
@@ -191,6 +221,13 @@ cloverleaf/<TASK-ID> (already created from main). Task risk_class: <class>.
191
221
  working directory is the worktree root, not whatever directory the session
192
222
  inherited.**
193
223
 
224
+ **DO NOT run `git checkout main` from this worktree.** The `main` branch is
225
+ held by the primary repo — attempting to check it out here will fail because
226
+ the same branch cannot be checked out in two worktrees simultaneously. To
227
+ compare your work against main, use `git diff main..HEAD` (safe diff) or
228
+ `git show main:<path>` (safe file inspection). All state-advance commits must
229
+ stay on the worktree's current branch (`cloverleaf/<TASK-ID>`).
230
+
194
231
  Plan: invoke `/cloverleaf-run <TASK-ID>`.
195
232
 
196
233
  **DO NOT invoke `/cloverleaf-merge`**. Fast lane stops after `/cloverleaf-review`