cclaw-cli 2.0.0 → 3.0.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/dist/config.js CHANGED
@@ -2,219 +2,67 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
4
  import { CCLAW_VERSION, DEFAULT_HARNESSES, FLOW_VERSION, RUNTIME_ROOT } from "./constants.js";
5
- import { isIronLawId, normalizeStrictLawIds } from "./content/iron-laws.js";
6
5
  import { exists, writeFileSafe } from "./fs-utils.js";
7
- import { FLOW_TRACKS, HARNESS_IDS, LANGUAGE_RULE_PACKS } from "./types.js";
6
+ import { HARNESS_IDS } from "./types.js";
8
7
  const CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
9
8
  const HARNESS_ID_SET = new Set(HARNESS_IDS);
10
- const FLOW_TRACK_SET = new Set(FLOW_TRACKS);
11
- const LANGUAGE_RULE_PACK_SET = new Set(LANGUAGE_RULE_PACKS);
9
+ const ALLOWED_CONFIG_KEYS = new Set(["version", "flowVersion", "harnesses"]);
12
10
  const SUPPORTED_HARNESSES_TEXT = HARNESS_IDS.join(", ");
13
- const SUPPORTED_TRACKS_TEXT = FLOW_TRACKS.join(", ");
14
- const SUPPORTED_LANGUAGE_RULE_PACKS_TEXT = LANGUAGE_RULE_PACKS.join(", ");
15
- const ALLOWED_CONFIG_KEYS = new Set([
16
- "version",
17
- "flowVersion",
18
- "harnesses",
19
- "vcs",
20
- "strictness",
21
- "tddTestGlobs",
22
- "tdd",
23
- "compound",
24
- "earlyLoop",
25
- "early_loop",
26
- "gitHookGuards",
27
- "defaultTrack",
28
- "languageRulePacks",
29
- "trackHeuristics",
30
- "sliceReview",
31
- "ironLaws",
32
- "optInAudits",
33
- "reviewLoop"
34
- ]);
35
- /**
36
- * Config keys removed in the advisory-by-default consolidation. Kept here so
37
- * the parser can emit a helpful migration error pointing users at the new
38
- * single `strictness` knob instead of a generic "unknown key" message.
39
- */
40
- const RETIRED_GUARD_CONFIG_KEYS = new Set([
41
- "promptGuardMode",
42
- "tddEnforcement",
43
- "workflowGuardMode"
44
- ]);
45
- /**
46
- * Config keys always present in the minimal init template. Everything else
47
- * is "advanced" — parsed when present, but not pre-populated by `cclaw init`.
48
- *
49
- * Deliberately small: a first-time user should only see knobs they might
50
- * actually flip. Power users override by adding more keys by hand; the
51
- * reference lives in `docs/config.md`.
52
- */
53
- const MINIMAL_CONFIG_KEYS = [
54
- "version",
55
- "flowVersion",
56
- "harnesses",
57
- "vcs",
58
- "strictness",
59
- "gitHookGuards"
11
+ // Kept for runtime modules that use these defaults directly.
12
+ export const DEFAULT_TDD_TEST_PATH_PATTERNS = [
13
+ "**/*.test.*",
14
+ "**/tests/**",
15
+ "**/__tests__/**"
60
16
  ];
61
- const DEFAULT_SLICE_REVIEW_THRESHOLD = 5;
62
- const DEFAULT_SLICE_REVIEW_TRACKS = ["standard"];
17
+ export const DEFAULT_TDD_TEST_GLOBS = [...DEFAULT_TDD_TEST_PATH_PATTERNS];
18
+ export const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = [];
19
+ export const DEFAULT_COMPOUND_RECURRENCE_THRESHOLD = 3;
20
+ export const DEFAULT_EARLY_LOOP_MAX_ITERATIONS = 3;
63
21
  export function createConfigWarningState() {
64
22
  return { emitted: new Set() };
65
23
  }
66
- function emitConfigWarningOnce(warningState, code, message) {
67
- const key = `${code}:${message}`;
68
- if (warningState.emitted.has(key)) {
69
- return;
24
+ export class InvalidConfigError extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "InvalidConfigError";
70
28
  }
71
- warningState.emitted.add(key);
72
- process.emitWarning(message, { code });
73
- }
74
- function sameStringArray(a, b) {
75
- if (!a || !b)
76
- return false;
77
- if (a.length !== b.length)
78
- return false;
79
- return a.every((value, index) => value === b[index]);
80
29
  }
81
30
  function configFixExample() {
82
31
  return `harnesses:
83
32
  - claude
84
33
  - cursor`;
85
34
  }
86
- export class InvalidConfigError extends Error {
87
- constructor(message) {
88
- super(message);
89
- this.name = "InvalidConfigError";
90
- }
91
- }
92
35
  function configValidationError(configFilePath, reason) {
93
36
  return new InvalidConfigError(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
94
37
  `Supported harnesses: ${SUPPORTED_HARNESSES_TEXT}\n` +
95
- `Supported tracks: ${SUPPORTED_TRACKS_TEXT}\n` +
96
- `Supported languageRulePacks: ${SUPPORTED_LANGUAGE_RULE_PACKS_TEXT}\n` +
97
38
  `Example config:\n${configFixExample()}\n` +
98
39
  `After fixing, run: cclaw sync`);
99
40
  }
100
41
  function isRecord(value) {
101
42
  return typeof value === "object" && value !== null && !Array.isArray(value);
102
43
  }
103
- function validateStringArray(value, fieldName, configFilePath) {
104
- if (value === undefined)
105
- return undefined;
106
- if (!Array.isArray(value)) {
107
- throw configValidationError(configFilePath, `"${fieldName}" must be an array of strings`);
108
- }
109
- const invalid = value.filter((item) => typeof item !== "string");
110
- if (invalid.length > 0) {
111
- throw configValidationError(configFilePath, `"${fieldName}" must contain only strings`);
112
- }
113
- return value;
114
- }
115
44
  export function configPath(projectRoot) {
116
45
  return path.join(projectRoot, CONFIG_PATH);
117
46
  }
118
- /**
119
- * Default test-path patterns used by the workflow-guard hook to classify TDD writes.
120
- *
121
- * Scope is intentionally narrow and language-agnostic; users can extend this
122
- * list in config when their repository uses different conventions.
123
- */
124
- export const DEFAULT_TDD_TEST_PATH_PATTERNS = [
125
- "**/*.test.*",
126
- "**/tests/**",
127
- "**/__tests__/**"
128
- ];
129
- /**
130
- * Legacy alias kept for backwards compatibility with `tddTestGlobs`.
131
- * Prefer `tdd.testPathPatterns` in new configurations.
132
- */
133
- export const DEFAULT_TDD_TEST_GLOBS = [...DEFAULT_TDD_TEST_PATH_PATTERNS];
134
- export const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = [];
135
- export const DEFAULT_COMPOUND_RECURRENCE_THRESHOLD = 3;
136
- export const DEFAULT_EARLY_LOOP_MAX_ITERATIONS = 3;
137
- /**
138
- * Populated runtime view of config values that downstream callers (install,
139
- * observe, sync/runtime checks) consume. Always has the derived guard modes populated,
140
- * regardless of whether the user wrote `strictness`, the legacy keys, both,
141
- * or neither.
142
- */
143
- export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack = "standard") {
144
- const tddTestPathPatterns = [...DEFAULT_TDD_TEST_PATH_PATTERNS];
145
- const tddProductionPathPatterns = [...DEFAULT_TDD_PRODUCTION_PATH_PATTERNS];
47
+ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, _defaultTrack = "standard") {
146
48
  return {
147
49
  version: CCLAW_VERSION,
148
50
  flowVersion: FLOW_VERSION,
149
- harnesses,
150
- vcs: "git-local-only",
151
- strictness: "advisory",
152
- tddTestGlobs: [...tddTestPathPatterns],
153
- tdd: {
154
- testPathPatterns: tddTestPathPatterns,
155
- productionPathPatterns: tddProductionPathPatterns,
156
- verificationRef: "auto"
157
- },
158
- compound: {
159
- recurrenceThreshold: DEFAULT_COMPOUND_RECURRENCE_THRESHOLD
160
- },
161
- earlyLoop: {
162
- enabled: true,
163
- maxIterations: DEFAULT_EARLY_LOOP_MAX_ITERATIONS
164
- },
165
- gitHookGuards: false,
166
- defaultTrack,
167
- languageRulePacks: [],
168
- ironLaws: {
169
- strictLaws: []
170
- },
171
- optInAudits: {
172
- scopePreAudit: false,
173
- staleDiagramAudit: true
174
- }
51
+ harnesses: [...new Set(harnesses)]
175
52
  };
176
53
  }
177
- /**
178
- * Probe common project-root manifests to infer which language rule packs the
179
- * user would reasonably want. Pure-functional best-effort: any filesystem
180
- * error is swallowed, producing an empty list — the user can always override
181
- * by hand.
182
- *
183
- * Called from `cclaw init` only (not `readConfig`), so subsequent upgrades
184
- * never surprise a user who intentionally cleared the list.
185
- */
186
- export async function detectLanguageRulePacks(projectRoot) {
187
- const detected = [];
188
- const pkgPath = path.join(projectRoot, "package.json");
189
- if (await exists(pkgPath)) {
190
- try {
191
- const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
192
- const deps = {
193
- ...pkg.dependencies,
194
- ...pkg.devDependencies
195
- };
196
- if ("typescript" in deps || typeof pkg.types === "string") {
197
- detected.push("typescript");
198
- }
199
- }
200
- catch {
201
- // Malformed package.json — skip; user can set the pack manually later.
202
- }
203
- }
204
- const pythonMarkers = ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"];
205
- for (const marker of pythonMarkers) {
206
- if (await exists(path.join(projectRoot, marker))) {
207
- detected.push("python");
208
- break;
209
- }
210
- }
211
- if (await exists(path.join(projectRoot, "go.mod"))) {
212
- detected.push("go");
213
- }
214
- return [...new Set(detected)];
54
+ function assertOnlySupportedKeys(parsed, fullPath) {
55
+ const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
56
+ if (unknownKeys.length === 0)
57
+ return;
58
+ const keyList = unknownKeys.join(", ");
59
+ throw configValidationError(fullPath, `key(s) ${keyList} are no longer supported in cclaw 3.0.0; see CHANGELOG.md`);
60
+ }
61
+ export async function detectLanguageRulePacks(_projectRoot) {
62
+ // Wave 21: harness-only config. Language packs are no longer configurable.
63
+ return [];
215
64
  }
216
- export async function readConfig(projectRoot, options = {}) {
217
- const warningState = options.warningState ?? createConfigWarningState();
65
+ export async function readConfig(projectRoot, _options = {}) {
218
66
  const fullPath = configPath(projectRoot);
219
67
  if (!(await exists(fullPath))) {
220
68
  return createDefaultConfig();
@@ -227,486 +75,48 @@ export async function readConfig(projectRoot, options = {}) {
227
75
  const reason = error instanceof Error ? error.message : "unknown parse error";
228
76
  throw configValidationError(fullPath, `failed to parse YAML (${reason})`);
229
77
  }
230
- if (parsedUnknown !== null && parsedUnknown !== undefined && typeof parsedUnknown !== "object") {
78
+ if (parsedUnknown !== null && parsedUnknown !== undefined && !isRecord(parsedUnknown)) {
231
79
  throw configValidationError(fullPath, "top-level config must be a YAML mapping/object");
232
80
  }
233
- const parsed = (parsedUnknown && typeof parsedUnknown === "object"
234
- ? parsedUnknown
235
- : {});
236
- const retiredGuardKeys = Object.keys(parsed).filter((key) => RETIRED_GUARD_CONFIG_KEYS.has(key));
237
- if (retiredGuardKeys.length > 0) {
238
- throw configValidationError(fullPath, `config key(s) ${retiredGuardKeys.join(", ")} were removed; ` +
239
- `use the single \`strictness: advisory|strict\` knob instead ` +
240
- `(advisory is the default). See docs/config.md#strictness for migration.`);
241
- }
242
- const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
243
- if (unknownKeys.length > 0) {
244
- throw configValidationError(fullPath, `unknown top-level key(s): ${unknownKeys.join(", ")}`);
245
- }
246
- const hasHarnessesField = Object.prototype.hasOwnProperty.call(parsed, "harnesses");
247
- if (hasHarnessesField && !Array.isArray(parsed.harnesses)) {
81
+ const parsed = (isRecord(parsedUnknown) ? parsedUnknown : {});
82
+ assertOnlySupportedKeys(parsed, fullPath);
83
+ if (Object.prototype.hasOwnProperty.call(parsed, "harnesses") &&
84
+ !Array.isArray(parsed.harnesses)) {
248
85
  throw configValidationError(fullPath, `"harnesses" must be an array`);
249
86
  }
250
- const configuredHarnesses = (parsed.harnesses ?? []);
251
- const invalidHarnesses = configuredHarnesses.filter((harness) => typeof harness !== "string" || !HARNESS_ID_SET.has(harness));
252
- if (invalidHarnesses.length > 0) {
253
- const formatted = invalidHarnesses
254
- .map((item) => (typeof item === "string" ? item : JSON.stringify(item)))
255
- .join(", ");
256
- throw configValidationError(fullPath, `unknown harness id(s): ${formatted}`);
257
- }
258
- const validatedHarnesses = configuredHarnesses;
259
- if (hasHarnessesField && validatedHarnesses.length === 0) {
260
- throw configValidationError(fullPath, `"harnesses" must include at least one harness`);
261
- }
262
- const harnesses = hasHarnessesField
263
- ? [...new Set(validatedHarnesses)]
264
- : DEFAULT_HARNESSES;
265
- const vcsRaw = parsed.vcs;
266
- if (Object.prototype.hasOwnProperty.call(parsed, "vcs") &&
267
- vcsRaw !== "git-with-remote" &&
268
- vcsRaw !== "git-local-only" &&
269
- vcsRaw !== "none") {
270
- throw configValidationError(fullPath, `"vcs" must be one of: git-with-remote, git-local-only, none`);
271
- }
272
- const vcs = vcsRaw === "git-with-remote" || vcsRaw === "git-local-only" || vcsRaw === "none"
273
- ? vcsRaw
274
- : "git-local-only";
275
- const strictnessRaw = parsed.strictness;
276
- if (Object.prototype.hasOwnProperty.call(parsed, "strictness") &&
277
- strictnessRaw !== "advisory" &&
278
- strictnessRaw !== "strict") {
279
- throw configValidationError(fullPath, `"strictness" must be "advisory" or "strict"`);
280
- }
281
- const strictness = strictnessRaw === "strict" ? "strict" : "advisory";
282
- const tddTestGlobsRaw = parsed.tddTestGlobs;
283
- const tddTestGlobs = validateStringArray(tddTestGlobsRaw, "tddTestGlobs", fullPath)
284
- ?? [...DEFAULT_TDD_TEST_GLOBS];
285
- const hasTddField = Object.prototype.hasOwnProperty.call(parsed, "tdd");
286
- const tddRaw = parsed.tdd;
287
- let explicitTddTestPathPatterns;
288
- let explicitTddProductionPathPatterns;
289
- let explicitTddVerificationRef;
290
- if (hasTddField) {
291
- if (!isRecord(tddRaw)) {
292
- throw configValidationError(fullPath, `"tdd" must be an object`);
293
- }
294
- const unknownTddKeys = Object.keys(tddRaw).filter((key) => key !== "testPathPatterns" && key !== "productionPathPatterns" && key !== "verificationRef");
295
- if (unknownTddKeys.length > 0) {
296
- throw configValidationError(fullPath, `"tdd" has unknown key(s): ${unknownTddKeys.join(", ")}`);
87
+ const rawHarnesses = Array.isArray(parsed.harnesses) ? parsed.harnesses : DEFAULT_HARNESSES;
88
+ const normalizedHarnesses = [];
89
+ for (const harness of rawHarnesses) {
90
+ if (typeof harness !== "string" || !HARNESS_ID_SET.has(harness)) {
91
+ throw configValidationError(fullPath, `unknown harness id "${String(harness)}"`);
297
92
  }
298
- explicitTddTestPathPatterns = validateStringArray(tddRaw.testPathPatterns, "tdd.testPathPatterns", fullPath);
299
- explicitTddProductionPathPatterns = validateStringArray(tddRaw.productionPathPatterns, "tdd.productionPathPatterns", fullPath);
300
- if (tddRaw.verificationRef !== undefined &&
301
- tddRaw.verificationRef !== "auto" &&
302
- tddRaw.verificationRef !== "required" &&
303
- tddRaw.verificationRef !== "disabled") {
304
- throw configValidationError(fullPath, '"tdd.verificationRef" must be one of: auto, required, disabled');
93
+ if (!normalizedHarnesses.includes(harness)) {
94
+ normalizedHarnesses.push(harness);
305
95
  }
306
- explicitTddVerificationRef = tddRaw.verificationRef;
307
- }
308
- if (tddTestGlobsRaw !== undefined &&
309
- explicitTddTestPathPatterns !== undefined &&
310
- !sameStringArray(tddTestGlobs, explicitTddTestPathPatterns)) {
311
- emitConfigWarningOnce(warningState, "CCLAW_CONFIG_DEPRECATED_TDD_TEST_GLOBS", `[cclaw] Both "tddTestGlobs" (deprecated) and "tdd.testPathPatterns" are set in ${fullPath}. ` +
312
- `Using "tdd.testPathPatterns".`);
313
96
  }
314
- const resolvedTddTestPathPatterns = [
315
- ...(explicitTddTestPathPatterns ?? tddTestGlobs ?? DEFAULT_TDD_TEST_PATH_PATTERNS)
316
- ];
317
- const resolvedTddProductionPathPatterns = [
318
- ...(explicitTddProductionPathPatterns ?? DEFAULT_TDD_PRODUCTION_PATH_PATTERNS)
319
- ];
320
- const hasCompoundField = Object.prototype.hasOwnProperty.call(parsed, "compound");
321
- const compoundRaw = parsed.compound;
322
- let compoundRecurrenceThreshold = DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
323
- if (hasCompoundField) {
324
- if (!isRecord(compoundRaw)) {
325
- throw configValidationError(fullPath, `"compound" must be an object`);
326
- }
327
- const unknownCompoundKeys = Object.keys(compoundRaw).filter((key) => key !== "recurrenceThreshold");
328
- if (unknownCompoundKeys.length > 0) {
329
- throw configValidationError(fullPath, `"compound" has unknown key(s): ${unknownCompoundKeys.join(", ")}`);
330
- }
331
- if (compoundRaw.recurrenceThreshold !== undefined &&
332
- (typeof compoundRaw.recurrenceThreshold !== "number" ||
333
- !Number.isInteger(compoundRaw.recurrenceThreshold) ||
334
- compoundRaw.recurrenceThreshold < 1)) {
335
- throw configValidationError(fullPath, `"compound.recurrenceThreshold" must be a positive integer`);
336
- }
337
- if (typeof compoundRaw.recurrenceThreshold === "number") {
338
- compoundRecurrenceThreshold = compoundRaw.recurrenceThreshold;
339
- }
340
- }
341
- const hasEarlyLoopField = Object.prototype.hasOwnProperty.call(parsed, "earlyLoop");
342
- const hasLegacyEarlyLoopField = Object.prototype.hasOwnProperty.call(parsed, "early_loop");
343
- if (hasEarlyLoopField && hasLegacyEarlyLoopField) {
344
- emitConfigWarningOnce(warningState, "CCLAW_CONFIG_EARLY_LOOP_ALIAS", `[cclaw] Both "earlyLoop" and legacy "early_loop" are set in ${fullPath}. Using "earlyLoop".`);
345
- }
346
- const earlyLoopRaw = hasEarlyLoopField
347
- ? parsed.earlyLoop
348
- : parsed.early_loop;
349
- let earlyLoopEnabled = true;
350
- let earlyLoopMaxIterations = DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
351
- if (hasEarlyLoopField || hasLegacyEarlyLoopField) {
352
- if (!isRecord(earlyLoopRaw)) {
353
- throw configValidationError(fullPath, `"${hasEarlyLoopField ? "earlyLoop" : "early_loop"}" must be an object`);
354
- }
355
- const unknownEarlyLoopKeys = Object.keys(earlyLoopRaw).filter((key) => key !== "enabled" && key !== "maxIterations" && key !== "max_iterations");
356
- if (unknownEarlyLoopKeys.length > 0) {
357
- throw configValidationError(fullPath, `"${hasEarlyLoopField ? "earlyLoop" : "early_loop"}" has unknown key(s): ${unknownEarlyLoopKeys.join(", ")}`);
358
- }
359
- if (earlyLoopRaw.enabled !== undefined && typeof earlyLoopRaw.enabled !== "boolean") {
360
- throw configValidationError(fullPath, `"${hasEarlyLoopField ? "earlyLoop" : "early_loop"}.enabled" must be a boolean`);
361
- }
362
- if (earlyLoopRaw.maxIterations !== undefined &&
363
- earlyLoopRaw.max_iterations !== undefined &&
364
- earlyLoopRaw.maxIterations !== earlyLoopRaw.max_iterations) {
365
- emitConfigWarningOnce(warningState, "CCLAW_CONFIG_EARLY_LOOP_MAX_ITERATIONS_ALIAS", `[cclaw] Both "${hasEarlyLoopField ? "earlyLoop.maxIterations" : "early_loop.maxIterations"}" and "${hasEarlyLoopField ? "earlyLoop.max_iterations" : "early_loop.max_iterations"}" are set in ${fullPath}. Using "maxIterations".`);
366
- }
367
- const rawMaxIterations = earlyLoopRaw.maxIterations ?? earlyLoopRaw.max_iterations;
368
- if (rawMaxIterations !== undefined &&
369
- (typeof rawMaxIterations !== "number" ||
370
- !Number.isInteger(rawMaxIterations) ||
371
- rawMaxIterations < 1)) {
372
- throw configValidationError(fullPath, `"${hasEarlyLoopField ? "earlyLoop" : "early_loop"}.maxIterations" must be a positive integer`);
373
- }
374
- if (typeof earlyLoopRaw.enabled === "boolean") {
375
- earlyLoopEnabled = earlyLoopRaw.enabled;
376
- }
377
- if (typeof rawMaxIterations === "number") {
378
- earlyLoopMaxIterations = rawMaxIterations;
379
- }
380
- }
381
- const gitHookGuardsRaw = parsed.gitHookGuards;
382
- if (Object.prototype.hasOwnProperty.call(parsed, "gitHookGuards") &&
383
- typeof gitHookGuardsRaw !== "boolean") {
384
- throw configValidationError(fullPath, `"gitHookGuards" must be a boolean`);
385
- }
386
- const gitHookGuards = typeof gitHookGuardsRaw === "boolean" ? gitHookGuardsRaw : false;
387
- const defaultTrackRaw = parsed.defaultTrack;
388
- if (Object.prototype.hasOwnProperty.call(parsed, "defaultTrack") &&
389
- (typeof defaultTrackRaw !== "string" || !FLOW_TRACK_SET.has(defaultTrackRaw))) {
390
- throw configValidationError(fullPath, `"defaultTrack" must be one of: ${SUPPORTED_TRACKS_TEXT}`);
391
- }
392
- const defaultTrack = typeof defaultTrackRaw === "string" && FLOW_TRACK_SET.has(defaultTrackRaw)
393
- ? defaultTrackRaw
394
- : "standard";
395
- const languageRulePacksRaw = parsed.languageRulePacks;
396
- const hasLanguageRulePacksField = Object.prototype.hasOwnProperty.call(parsed, "languageRulePacks");
397
- if (hasLanguageRulePacksField && !Array.isArray(languageRulePacksRaw)) {
398
- throw configValidationError(fullPath, `"languageRulePacks" must be an array`);
399
- }
400
- const rawPacks = (languageRulePacksRaw ?? []);
401
- const invalidPacks = rawPacks.filter((pack) => typeof pack !== "string" || !LANGUAGE_RULE_PACK_SET.has(pack));
402
- if (invalidPacks.length > 0) {
403
- const formatted = invalidPacks
404
- .map((item) => (typeof item === "string" ? item : JSON.stringify(item)))
405
- .join(", ");
406
- throw configValidationError(fullPath, `unknown languageRulePacks id(s): ${formatted}`);
407
- }
408
- const languageRulePacks = [...new Set(rawPacks)];
409
- const trackHeuristicsRaw = parsed.trackHeuristics;
410
- let trackHeuristics = undefined;
411
- if (Object.prototype.hasOwnProperty.call(parsed, "trackHeuristics")) {
412
- if (!isRecord(trackHeuristicsRaw)) {
413
- throw configValidationError(fullPath, `"trackHeuristics" must be an object`);
414
- }
415
- const fallbackRaw = trackHeuristicsRaw.fallback;
416
- if (fallbackRaw !== undefined && (typeof fallbackRaw !== "string" || !FLOW_TRACK_SET.has(fallbackRaw))) {
417
- throw configValidationError(fullPath, `"trackHeuristics.fallback" must be one of: ${SUPPORTED_TRACKS_TEXT}`);
418
- }
419
- if (Object.prototype.hasOwnProperty.call(trackHeuristicsRaw, "priority")) {
420
- throw configValidationError(fullPath, `"trackHeuristics.priority" is no longer supported (removed in v0.38.0). Track evaluation order is always standard -> medium -> quick. Remove the field to upgrade.`);
421
- }
422
- const tracksRaw = trackHeuristicsRaw.tracks;
423
- let tracks = undefined;
424
- if (tracksRaw !== undefined) {
425
- if (!isRecord(tracksRaw)) {
426
- throw configValidationError(fullPath, `"trackHeuristics.tracks" must be an object`);
427
- }
428
- tracks = {};
429
- for (const [trackName, ruleRaw] of Object.entries(tracksRaw)) {
430
- if (!FLOW_TRACK_SET.has(trackName)) {
431
- throw configValidationError(fullPath, `"trackHeuristics.tracks" contains unknown track "${trackName}". Supported: ${SUPPORTED_TRACKS_TEXT}`);
432
- }
433
- if (!isRecord(ruleRaw)) {
434
- throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}" must be an object`);
435
- }
436
- if (Object.prototype.hasOwnProperty.call(ruleRaw, "patterns")) {
437
- throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}.patterns" is no longer supported (removed in v0.38.0). Regex patterns were never wired into runtime routing. Move the intent into "triggers" (substrings) or "veto".`);
438
- }
439
- const triggers = validateStringArray(ruleRaw.triggers, `trackHeuristics.tracks.${trackName}.triggers`, fullPath);
440
- const veto = validateStringArray(ruleRaw.veto, `trackHeuristics.tracks.${trackName}.veto`, fullPath);
441
- tracks[trackName] = {
442
- triggers,
443
- veto
444
- };
445
- }
446
- }
447
- trackHeuristics = {
448
- fallback: fallbackRaw,
449
- tracks
450
- };
451
- }
452
- const sliceReviewRaw = parsed.sliceReview;
453
- let sliceReview = undefined;
454
- if (Object.prototype.hasOwnProperty.call(parsed, "sliceReview")) {
455
- if (!isRecord(sliceReviewRaw)) {
456
- throw configValidationError(fullPath, `"sliceReview" must be an object`);
457
- }
458
- const enabledRaw = sliceReviewRaw.enabled;
459
- if (enabledRaw !== undefined && typeof enabledRaw !== "boolean") {
460
- throw configValidationError(fullPath, `"sliceReview.enabled" must be a boolean`);
461
- }
462
- const thresholdRaw = sliceReviewRaw.filesChangedThreshold;
463
- if (thresholdRaw !== undefined &&
464
- (typeof thresholdRaw !== "number" || !Number.isInteger(thresholdRaw) || thresholdRaw < 1)) {
465
- throw configValidationError(fullPath, `"sliceReview.filesChangedThreshold" must be a positive integer`);
466
- }
467
- const touchTriggers = validateStringArray(sliceReviewRaw.touchTriggers, "sliceReview.touchTriggers", fullPath);
468
- const enforceRaw = sliceReviewRaw.enforceOnTracks;
469
- let enforceOnTracks;
470
- if (enforceRaw !== undefined) {
471
- if (!Array.isArray(enforceRaw)) {
472
- throw configValidationError(fullPath, `"sliceReview.enforceOnTracks" must be an array`);
473
- }
474
- const invalidTracks = enforceRaw.filter((value) => typeof value !== "string" || !FLOW_TRACK_SET.has(value));
475
- if (invalidTracks.length > 0) {
476
- throw configValidationError(fullPath, `"sliceReview.enforceOnTracks" must contain only: ${SUPPORTED_TRACKS_TEXT}`);
477
- }
478
- enforceOnTracks = [...new Set(enforceRaw)];
479
- }
480
- sliceReview = {
481
- enabled: typeof enabledRaw === "boolean" ? enabledRaw : false,
482
- filesChangedThreshold: typeof thresholdRaw === "number" ? thresholdRaw : DEFAULT_SLICE_REVIEW_THRESHOLD,
483
- touchTriggers: touchTriggers ?? [],
484
- enforceOnTracks: enforceOnTracks ?? DEFAULT_SLICE_REVIEW_TRACKS
485
- };
486
- }
487
- const ironLawsRaw = parsed.ironLaws;
488
- let ironLaws = undefined;
489
- if (Object.prototype.hasOwnProperty.call(parsed, "ironLaws")) {
490
- if (!isRecord(ironLawsRaw)) {
491
- throw configValidationError(fullPath, `"ironLaws" must be an object`);
492
- }
493
- if (Object.prototype.hasOwnProperty.call(ironLawsRaw, "mode")) {
494
- throw configValidationError(fullPath, `"ironLaws.mode" was removed; the project-wide \`strictness\` knob now ` +
495
- `controls iron-law enforcement. Use \`ironLaws.strictLaws\` for per-law overrides.`);
496
- }
497
- const unknownIronLawKeys = Object.keys(ironLawsRaw).filter((key) => key !== "strictLaws");
498
- if (unknownIronLawKeys.length > 0) {
499
- throw configValidationError(fullPath, `"ironLaws" has unknown key(s): ${unknownIronLawKeys.join(", ")}`);
500
- }
501
- const strictLawIdsRaw = validateStringArray(ironLawsRaw.strictLaws, "ironLaws.strictLaws", fullPath) ?? [];
502
- const unknownStrictLawIds = strictLawIdsRaw.filter((id) => !isIronLawId(id));
503
- if (unknownStrictLawIds.length > 0) {
504
- throw configValidationError(fullPath, `"ironLaws.strictLaws" contains unknown law id(s): ${unknownStrictLawIds.join(", ")}`);
505
- }
506
- ironLaws = {
507
- strictLaws: normalizeStrictLawIds(strictLawIdsRaw)
508
- };
509
- }
510
- else {
511
- ironLaws = { strictLaws: [] };
512
- }
513
- const optInAuditsRaw = parsed.optInAudits;
514
- let optInAudits = undefined;
515
- if (Object.prototype.hasOwnProperty.call(parsed, "optInAudits")) {
516
- if (!isRecord(optInAuditsRaw)) {
517
- throw configValidationError(fullPath, `"optInAudits" must be an object`);
518
- }
519
- const unknownOptInAuditKeys = Object.keys(optInAuditsRaw).filter((key) => key !== "scopePreAudit" && key !== "staleDiagramAudit");
520
- if (unknownOptInAuditKeys.length > 0) {
521
- throw configValidationError(fullPath, `"optInAudits" has unknown key(s): ${unknownOptInAuditKeys.join(", ")}`);
522
- }
523
- if (optInAuditsRaw.scopePreAudit !== undefined &&
524
- typeof optInAuditsRaw.scopePreAudit !== "boolean") {
525
- throw configValidationError(fullPath, `"optInAudits.scopePreAudit" must be a boolean`);
526
- }
527
- if (optInAuditsRaw.staleDiagramAudit !== undefined &&
528
- typeof optInAuditsRaw.staleDiagramAudit !== "boolean") {
529
- throw configValidationError(fullPath, `"optInAudits.staleDiagramAudit" must be a boolean`);
530
- }
531
- optInAudits = {
532
- scopePreAudit: typeof optInAuditsRaw.scopePreAudit === "boolean"
533
- ? optInAuditsRaw.scopePreAudit
534
- : false,
535
- staleDiagramAudit: typeof optInAuditsRaw.staleDiagramAudit === "boolean"
536
- ? optInAuditsRaw.staleDiagramAudit
537
- : true
538
- };
539
- }
540
- if (!optInAudits) {
541
- optInAudits = {
542
- scopePreAudit: false,
543
- staleDiagramAudit: true
544
- };
545
- }
546
- const reviewLoopRaw = parsed.reviewLoop;
547
- let reviewLoop = undefined;
548
- if (Object.prototype.hasOwnProperty.call(parsed, "reviewLoop")) {
549
- if (!isRecord(reviewLoopRaw)) {
550
- throw configValidationError(fullPath, `"reviewLoop" must be an object`);
551
- }
552
- const unknownReviewLoopKeys = Object.keys(reviewLoopRaw).filter((key) => key !== "externalSecondOpinion");
553
- if (unknownReviewLoopKeys.length > 0) {
554
- throw configValidationError(fullPath, `"reviewLoop" has unknown key(s): ${unknownReviewLoopKeys.join(", ")}`);
555
- }
556
- const externalRaw = reviewLoopRaw.externalSecondOpinion;
557
- let externalSecondOpinion = undefined;
558
- if (externalRaw !== undefined) {
559
- if (!isRecord(externalRaw)) {
560
- throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion" must be an object`);
561
- }
562
- const unknownExternalKeys = Object.keys(externalRaw).filter((key) => key !== "enabled" && key !== "model" && key !== "scoreDeltaThreshold");
563
- if (unknownExternalKeys.length > 0) {
564
- throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion" has unknown key(s): ${unknownExternalKeys.join(", ")}`);
565
- }
566
- if (externalRaw.enabled !== undefined && typeof externalRaw.enabled !== "boolean") {
567
- throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.enabled" must be a boolean`);
568
- }
569
- if (externalRaw.model !== undefined && typeof externalRaw.model !== "string") {
570
- throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.model" must be a string`);
571
- }
572
- if (externalRaw.scoreDeltaThreshold !== undefined &&
573
- (typeof externalRaw.scoreDeltaThreshold !== "number" ||
574
- Number.isNaN(externalRaw.scoreDeltaThreshold) ||
575
- externalRaw.scoreDeltaThreshold < 0 ||
576
- externalRaw.scoreDeltaThreshold > 1)) {
577
- throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.scoreDeltaThreshold" must be a number between 0 and 1`);
578
- }
579
- externalSecondOpinion = {
580
- enabled: externalRaw.enabled === true,
581
- model: typeof externalRaw.model === "string" ? externalRaw.model : undefined,
582
- scoreDeltaThreshold: typeof externalRaw.scoreDeltaThreshold === "number"
583
- ? externalRaw.scoreDeltaThreshold
584
- : 0.2
585
- };
586
- }
587
- reviewLoop = { externalSecondOpinion };
97
+ if (normalizedHarnesses.length === 0) {
98
+ throw configValidationError(fullPath, `"harnesses" must include at least one harness`);
588
99
  }
100
+ const version = typeof parsed.version === "string" && parsed.version.trim().length > 0
101
+ ? parsed.version
102
+ : CCLAW_VERSION;
103
+ const flowVersion = typeof parsed.flowVersion === "string" && parsed.flowVersion.trim().length > 0
104
+ ? parsed.flowVersion
105
+ : FLOW_VERSION;
589
106
  return {
590
- version: parsed.version ?? CCLAW_VERSION,
591
- flowVersion: parsed.flowVersion ?? FLOW_VERSION,
592
- harnesses,
593
- vcs,
594
- strictness,
595
- tddTestGlobs,
596
- tdd: {
597
- testPathPatterns: resolvedTddTestPathPatterns,
598
- productionPathPatterns: resolvedTddProductionPathPatterns,
599
- verificationRef: explicitTddVerificationRef ?? "auto"
600
- },
601
- compound: {
602
- recurrenceThreshold: compoundRecurrenceThreshold
603
- },
604
- earlyLoop: {
605
- enabled: earlyLoopEnabled,
606
- maxIterations: earlyLoopMaxIterations
607
- },
608
- gitHookGuards,
609
- defaultTrack,
610
- languageRulePacks,
611
- trackHeuristics,
612
- sliceReview,
613
- ironLaws,
614
- optInAudits,
615
- reviewLoop
107
+ version,
108
+ flowVersion,
109
+ harnesses: normalizedHarnesses
616
110
  };
617
111
  }
618
- function isMinimalKey(key) {
619
- return MINIMAL_CONFIG_KEYS.includes(key);
620
- }
621
- function buildSerializableConfig(config, options = {}) {
622
- const mode = options.mode ?? "full";
623
- const advanced = options.advancedKeysPresent;
624
- const output = {};
625
- const ordered = [
626
- "version",
627
- "flowVersion",
628
- "harnesses",
629
- "vcs",
630
- "strictness",
631
- "tddTestGlobs",
632
- "tdd",
633
- "compound",
634
- "earlyLoop",
635
- "gitHookGuards",
636
- "defaultTrack",
637
- "languageRulePacks",
638
- "trackHeuristics",
639
- "sliceReview",
640
- "ironLaws",
641
- "optInAudits",
642
- "reviewLoop"
643
- ];
644
- for (const key of ordered) {
645
- const value = config[key];
646
- if (value === undefined)
647
- continue;
648
- if (mode === "full") {
649
- output[key] = value;
650
- continue;
651
- }
652
- // Minimal mode: always include the short list; advanced keys only when
653
- // the caller explicitly opted in, or for auto-detected non-empty
654
- // `languageRulePacks`.
655
- if (isMinimalKey(key)) {
656
- output[key] = value;
657
- continue;
658
- }
659
- if (advanced?.has(key)) {
660
- output[key] = value;
661
- continue;
662
- }
663
- if (key === "languageRulePacks" && Array.isArray(value) && value.length > 0) {
664
- output[key] = value;
665
- }
666
- }
667
- return output;
668
- }
669
- export async function writeConfig(projectRoot, config, options = {}) {
670
- const serialisable = buildSerializableConfig(config, options);
112
+ export async function writeConfig(projectRoot, config, _options = {}) {
113
+ const serialisable = {
114
+ version: config.version,
115
+ flowVersion: config.flowVersion,
116
+ harnesses: config.harnesses
117
+ };
671
118
  await writeFileSafe(configPath(projectRoot), stringify(serialisable));
672
119
  }
673
- /**
674
- * Enumerate which advanced keys are currently set in the on-disk config.
675
- * Used by `cclaw upgrade` to preserve the user's existing shape — if they
676
- * wrote `tddTestGlobs` by hand, the upgrade keeps it; if they didn't, the
677
- * upgrade stays minimal.
678
- */
679
- export async function detectAdvancedKeys(projectRoot) {
680
- const fullPath = configPath(projectRoot);
681
- if (!(await exists(fullPath)))
682
- return new Set();
683
- try {
684
- const parsedUnknown = parse(await fs.readFile(fullPath, "utf8"));
685
- if (!isRecord(parsedUnknown))
686
- return new Set();
687
- const advancedCandidates = [
688
- "tddTestGlobs",
689
- "tdd",
690
- "compound",
691
- "earlyLoop",
692
- "defaultTrack",
693
- "languageRulePacks",
694
- "trackHeuristics",
695
- "sliceReview",
696
- "ironLaws",
697
- "optInAudits",
698
- "reviewLoop"
699
- ];
700
- const present = new Set();
701
- for (const key of advancedCandidates) {
702
- if (Object.prototype.hasOwnProperty.call(parsedUnknown, key) ||
703
- (key === "earlyLoop" && Object.prototype.hasOwnProperty.call(parsedUnknown, "early_loop"))) {
704
- present.add(key);
705
- }
706
- }
707
- return present;
708
- }
709
- catch {
710
- return new Set();
711
- }
120
+ export async function detectAdvancedKeys(_projectRoot) {
121
+ return new Set();
712
122
  }