@cloverleaf/reference-impl 0.6.3 → 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
  }
@@ -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
  }
@@ -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.3",
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",
@@ -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