@curdx/flow 2.3.1 → 2.3.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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code Discipline Layer — spec-driven workflow + goal-backward verification + Karpathy 4 principles enforced via gates. Stops Claude from faking \"done\" on non-trivial features.",
9
- "version": "2.3.1"
9
+ "version": "2.3.3"
10
10
  },
11
11
  "allowCrossMarketplaceDependenciesOn": [
12
12
  "context7-marketplace"
@@ -16,7 +16,7 @@
16
16
  "name": "curdx-flow",
17
17
  "source": "./",
18
18
  "description": "Claude Code Discipline Layer — spec-driven workflow + goal-backward verification + Karpathy 4 principles enforced via gates. Stops Claude from faking \"done\" on non-trivial features.",
19
- "version": "2.3.1",
19
+ "version": "2.3.3",
20
20
  "author": {
21
21
  "name": "wdx",
22
22
  "email": "bydongxin@gmail.com"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "curdx-flow",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "Claude Code Discipline Layer — spec-driven workflow + goal-backward verification + Karpathy 4 principles enforced via gates. Stops Claude from faking \"done\" on non-trivial features.",
5
5
  "author": {
6
6
  "name": "wdx",
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  const PROJECT_ONLY_IGNORED_SETTINGS = [
@@ -99,6 +100,23 @@ const CURDX_FLOW_REQUIRED_TOOLS = [
99
100
  "Glob",
100
101
  ];
101
102
  const CURDX_FLOW_PLUGIN_ID = "curdx-flow@curdx-flow-marketplace";
103
+ const CURDX_FLOW_PLUGIN_OPTIONS = {
104
+ autonomous_blocking: {
105
+ type: "boolean",
106
+ default: true,
107
+ },
108
+ daily_dependency_check: {
109
+ type: "boolean",
110
+ default: true,
111
+ },
112
+ monitor_interval_seconds: {
113
+ type: "number",
114
+ default: 8,
115
+ integer: true,
116
+ min: 3,
117
+ max: 60,
118
+ },
119
+ };
102
120
  const CURDX_FLOW_REQUIRED_PLUGIN_IDS = ["context7-plugin@context7-marketplace"];
103
121
  const HTTP_HOOK_SETTINGS = ["allowedHttpHookUrls", "httpHookAllowedEnvVars"];
104
122
  const PERSISTED_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
@@ -171,6 +189,201 @@ function pushScopedWarning(target, kind, message, scope = "project") {
171
189
  target.push(warning);
172
190
  }
173
191
 
192
+ function createCurdxFlowPluginOptionsState() {
193
+ const definitions = Object.entries(CURDX_FLOW_PLUGIN_OPTIONS).map(([key, value]) => ({
194
+ key,
195
+ type: value.type,
196
+ default: value.default,
197
+ min: value.min,
198
+ max: value.max,
199
+ integer: value.integer === true,
200
+ }));
201
+
202
+ const repoEffective = Object.fromEntries(
203
+ definitions.map((definition) => [
204
+ definition.key,
205
+ { value: definition.default, source: "default" },
206
+ ])
207
+ );
208
+
209
+ return {
210
+ pluginId: CURDX_FLOW_PLUGIN_ID,
211
+ definitions,
212
+ user: {
213
+ pluginConfigsPresent: false,
214
+ pluginConfigPresent: false,
215
+ optionsPresent: false,
216
+ overrides: {},
217
+ },
218
+ project: {
219
+ pluginConfigsPresent: false,
220
+ pluginConfigPresent: false,
221
+ optionsPresent: false,
222
+ overrides: {},
223
+ },
224
+ local: {
225
+ pluginConfigsPresent: false,
226
+ pluginConfigPresent: false,
227
+ optionsPresent: false,
228
+ overrides: {},
229
+ },
230
+ repoEffective,
231
+ machineEffective: structuredClone(repoEffective),
232
+ };
233
+ }
234
+
235
+ function pluginScopeLabel(scope) {
236
+ if (scope === "local") return "settings.local.json";
237
+ if (scope === "user") return "~/.claude/settings.json";
238
+ return "settings.json";
239
+ }
240
+
241
+ function invalidSettingKindForScope(scope) {
242
+ if (scope === "local") return "invalid-local-setting";
243
+ if (scope === "user") return "invalid-user-setting";
244
+ return "invalid-project-setting";
245
+ }
246
+
247
+ function applyCurdxFlowPluginOptionOverride(pluginOptionsState, key, value, scope) {
248
+ pluginOptionsState.machineEffective[key] = {
249
+ value,
250
+ source: scope,
251
+ };
252
+
253
+ if (scope === "project" || scope === "local") {
254
+ pluginOptionsState.repoEffective[key] = {
255
+ value,
256
+ source: scope,
257
+ };
258
+ }
259
+ }
260
+
261
+ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope = "project") {
262
+ if (!isNonArrayObject(parsed) || !("pluginConfigs" in parsed)) {
263
+ return;
264
+ }
265
+
266
+ const target = scope === "local"
267
+ ? pluginOptionsState.local
268
+ : scope === "user"
269
+ ? pluginOptionsState.user
270
+ : pluginOptionsState.project;
271
+ const settingsLabel = pluginScopeLabel(scope);
272
+ target.pluginConfigsPresent = true;
273
+
274
+ if (!isNonArrayObject(parsed.pluginConfigs)) {
275
+ pushScopedWarning(
276
+ warnings,
277
+ invalidSettingKindForScope(scope),
278
+ `pluginConfigs in ${settingsLabel} must be an object keyed by plugin@marketplace id`,
279
+ scope
280
+ );
281
+ return;
282
+ }
283
+
284
+ const pluginConfig = parsed.pluginConfigs[CURDX_FLOW_PLUGIN_ID];
285
+ if (pluginConfig === undefined) {
286
+ return;
287
+ }
288
+
289
+ target.pluginConfigPresent = true;
290
+
291
+ if (!isNonArrayObject(pluginConfig)) {
292
+ pushScopedWarning(
293
+ warnings,
294
+ invalidSettingKindForScope(scope),
295
+ `pluginConfigs[${CURDX_FLOW_PLUGIN_ID}] in ${settingsLabel} must be an object when set`,
296
+ scope
297
+ );
298
+ return;
299
+ }
300
+
301
+ if (!("options" in pluginConfig)) {
302
+ return;
303
+ }
304
+
305
+ target.optionsPresent = true;
306
+
307
+ if (!isNonArrayObject(pluginConfig.options)) {
308
+ pushScopedWarning(
309
+ warnings,
310
+ invalidSettingKindForScope(scope),
311
+ `pluginConfigs[${CURDX_FLOW_PLUGIN_ID}].options in ${settingsLabel} must be an object when set`,
312
+ scope
313
+ );
314
+ return;
315
+ }
316
+
317
+ for (const [key, value] of Object.entries(pluginConfig.options)) {
318
+ const definition = CURDX_FLOW_PLUGIN_OPTIONS[key];
319
+
320
+ if (!definition) {
321
+ pushScopedWarning(
322
+ warnings,
323
+ "unknown-plugin-option",
324
+ `unknown CurDX-Flow plugin option in ${settingsLabel}: ${key}`,
325
+ scope
326
+ );
327
+ continue;
328
+ }
329
+
330
+ if (definition.type === "boolean" && typeof value !== "boolean") {
331
+ pushScopedWarning(
332
+ warnings,
333
+ invalidSettingKindForScope(scope),
334
+ `CurDX-Flow plugin option ${key} in ${settingsLabel} must be boolean`,
335
+ scope
336
+ );
337
+ continue;
338
+ }
339
+
340
+ if (definition.type === "number") {
341
+ if (typeof value !== "number" || Number.isNaN(value)) {
342
+ pushScopedWarning(
343
+ warnings,
344
+ invalidSettingKindForScope(scope),
345
+ `CurDX-Flow plugin option ${key} in ${settingsLabel} must be a number`,
346
+ scope
347
+ );
348
+ continue;
349
+ }
350
+
351
+ if (definition.integer && !Number.isInteger(value)) {
352
+ pushScopedWarning(
353
+ warnings,
354
+ invalidSettingKindForScope(scope),
355
+ `CurDX-Flow plugin option ${key} in ${settingsLabel} must be an integer`,
356
+ scope
357
+ );
358
+ continue;
359
+ }
360
+
361
+ if (typeof definition.min === "number" && value < definition.min) {
362
+ pushScopedWarning(
363
+ warnings,
364
+ invalidSettingKindForScope(scope),
365
+ `CurDX-Flow plugin option ${key} in ${settingsLabel} must be between ${definition.min} and ${definition.max}`,
366
+ scope
367
+ );
368
+ continue;
369
+ }
370
+
371
+ if (typeof definition.max === "number" && value > definition.max) {
372
+ pushScopedWarning(
373
+ warnings,
374
+ invalidSettingKindForScope(scope),
375
+ `CurDX-Flow plugin option ${key} in ${settingsLabel} must be between ${definition.min} and ${definition.max}`,
376
+ scope
377
+ );
378
+ continue;
379
+ }
380
+ }
381
+
382
+ target.overrides[key] = value;
383
+ applyCurdxFlowPluginOptionOverride(pluginOptionsState, key, value, scope);
384
+ }
385
+ }
386
+
174
387
  function auditLocalClaudeSettings(parsed, warnings) {
175
388
  const scope = "local";
176
389
  const permissions = parsed?.permissions && typeof parsed.permissions === "object"
@@ -362,10 +575,15 @@ function auditLocalClaudeSettings(parsed, warnings) {
362
575
  }
363
576
  }
364
577
 
365
- export async function readProjectClaudeSettings(cwd = process.cwd()) {
578
+ export async function readProjectClaudeSettings(cwd = process.cwd(), { homeDir = os.homedir() } = {}) {
366
579
  const settingsPath = path.join(cwd, ".claude", "settings.json");
367
580
  const localSettingsPath = path.join(cwd, ".claude", "settings.local.json");
581
+ const userSettingsPath = path.join(homeDir, ".claude", "settings.json");
368
582
  const state = {
583
+ userExists: false,
584
+ userInvalid: false,
585
+ userParseError: null,
586
+ userWarnings: [],
369
587
  exists: false,
370
588
  localExists: false,
371
589
  invalid: false,
@@ -374,8 +592,16 @@ export async function readProjectClaudeSettings(cwd = process.cwd()) {
374
592
  localInvalid: false,
375
593
  localParseError: null,
376
594
  localWarnings: [],
595
+ pluginOptions: createCurdxFlowPluginOptionsState(),
377
596
  };
378
597
 
598
+ try {
599
+ const userStat = await fs.stat(userSettingsPath);
600
+ state.userExists = userStat.isFile();
601
+ } catch {
602
+ state.userExists = false;
603
+ }
604
+
379
605
  try {
380
606
  const localStat = await fs.stat(localSettingsPath);
381
607
  state.localExists = localStat.isFile();
@@ -383,6 +609,16 @@ export async function readProjectClaudeSettings(cwd = process.cwd()) {
383
609
  state.localExists = false;
384
610
  }
385
611
 
612
+ if (state.userExists) {
613
+ try {
614
+ const userParsed = JSON.parse(await fs.readFile(userSettingsPath, "utf-8"));
615
+ auditCurdxFlowPluginOptions(userParsed, state.userWarnings, state.pluginOptions, "user");
616
+ } catch (error) {
617
+ state.userInvalid = true;
618
+ state.userParseError = error?.message || String(error);
619
+ }
620
+ }
621
+
386
622
  let parsed;
387
623
  try {
388
624
  const stat = await fs.stat(settingsPath);
@@ -407,6 +643,8 @@ export async function readProjectClaudeSettings(cwd = process.cwd()) {
407
643
  const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
408
644
  const deny = Array.isArray(permissions.deny) ? permissions.deny : [];
409
645
 
646
+ auditCurdxFlowPluginOptions(parsed, state.warnings, state.pluginOptions, "project");
647
+
410
648
  if ("enabledPlugins" in parsed && (
411
649
  !parsed.enabledPlugins ||
412
650
  typeof parsed.enabledPlugins !== "object" ||
@@ -726,6 +964,7 @@ export async function readProjectClaudeSettings(cwd = process.cwd()) {
726
964
  if (state.localExists) {
727
965
  try {
728
966
  const localParsed = JSON.parse(await fs.readFile(localSettingsPath, "utf-8"));
967
+ auditCurdxFlowPluginOptions(localParsed, state.localWarnings, state.pluginOptions, "local");
729
968
  auditLocalClaudeSettings(localParsed, state.localWarnings);
730
969
  } catch (error) {
731
970
  state.localInvalid = true;
@@ -13,7 +13,54 @@ function pluginErrorDetails(plugin) {
13
13
  }
14
14
 
15
15
  function projectSettingsWarningDetails(warning) {
16
+ if (warning?.scope === "user") {
17
+ if (warning.kind === "unknown-plugin-option") {
18
+ return [
19
+ "~/.claude/settings.json applies across all your repositories on this machine",
20
+ "remove the unknown key or rename it to a supported CurDX-Flow plugin option under pluginConfigs[curdx-flow@curdx-flow-marketplace].options",
21
+ ];
22
+ }
23
+
24
+ if (
25
+ warning.kind === "invalid-user-setting" &&
26
+ (
27
+ warning.message?.includes("pluginConfigs[curdx-flow@curdx-flow-marketplace]") ||
28
+ warning.message?.includes("CurDX-Flow plugin option")
29
+ )
30
+ ) {
31
+ return [
32
+ "CurDX-Flow plugin options in ~/.claude/settings.json apply across all local repositories unless a project or local override wins",
33
+ "use boolean values for autonomous_blocking and daily_dependency_check, and keep monitor_interval_seconds as an integer between 3 and 60",
34
+ ];
35
+ }
36
+
37
+ return [
38
+ "~/.claude/settings.json applies across all your repositories on this machine",
39
+ "fix or remove the user-scoped override if CurDX-Flow behaves unexpectedly everywhere",
40
+ ];
41
+ }
42
+
16
43
  if (warning?.scope === "local") {
44
+ if (warning.kind === "unknown-plugin-option") {
45
+ return [
46
+ "settings.local.json overrides shared project settings on this machine",
47
+ "remove the unknown key or rename it to a supported CurDX-Flow plugin option under pluginConfigs[curdx-flow@curdx-flow-marketplace].options",
48
+ ];
49
+ }
50
+
51
+ if (
52
+ warning.kind === "invalid-local-setting" &&
53
+ (
54
+ warning.message?.includes("pluginConfigs[curdx-flow@curdx-flow-marketplace]") ||
55
+ warning.message?.includes("CurDX-Flow plugin option")
56
+ )
57
+ ) {
58
+ return [
59
+ "CurDX-Flow plugin options in settings.local.json override project and user settings on this machine",
60
+ "use boolean values for autonomous_blocking and daily_dependency_check, and keep monitor_interval_seconds as an integer between 3 and 60",
61
+ ];
62
+ }
63
+
17
64
  if (warning.kind === "invalid-local-setting") {
18
65
  return [
19
66
  "settings.local.json is the highest-precedence repo-scoped settings surface on this machine",
@@ -41,6 +88,26 @@ function projectSettingsWarningDetails(warning) {
41
88
  ];
42
89
  }
43
90
 
91
+ if (warning.kind === "unknown-plugin-option") {
92
+ return [
93
+ "CurDX-Flow only reads keys declared in the plugin manifest",
94
+ "remove the unknown key or rename it under pluginConfigs[curdx-flow@curdx-flow-marketplace].options",
95
+ ];
96
+ }
97
+
98
+ if (
99
+ warning.kind === "invalid-project-setting" &&
100
+ (
101
+ warning.message?.includes("pluginConfigs[curdx-flow@curdx-flow-marketplace]") ||
102
+ warning.message?.includes("CurDX-Flow plugin option")
103
+ )
104
+ ) {
105
+ return [
106
+ "CurDX-Flow non-sensitive plugin options belong under pluginConfigs[curdx-flow@curdx-flow-marketplace].options in .claude/settings.json",
107
+ "use boolean values for autonomous_blocking and daily_dependency_check, and keep monitor_interval_seconds as an integer between 3 and 60",
108
+ ];
109
+ }
110
+
44
111
  if (!warning?.kind) {
45
112
  return [
46
113
  "project settings are shared with collaborators",
@@ -116,6 +183,11 @@ function projectSettingsWarningDetails(warning) {
116
183
  ];
117
184
  }
118
185
 
186
+ function formatInlineValue(value) {
187
+ if (typeof value === "string") return `"${value}"`;
188
+ return String(value);
189
+ }
190
+
119
191
  export function buildDoctorReport({
120
192
  claudeVersionValue,
121
193
  nodeVersion,
@@ -403,6 +475,129 @@ export function buildDoctorReport({
403
475
  }
404
476
  }
405
477
 
478
+ if (
479
+ bundledPluginRuntimeDefaults?.exists ||
480
+ projectClaudeSettings?.pluginOptions
481
+ ) {
482
+ const configuredOptionsSection = createSection("CurDX-Flow configured options:");
483
+ const optionState = projectClaudeSettings?.pluginOptions;
484
+ const userOverrides = optionState?.user?.overrides || {};
485
+ const projectOverrides = optionState?.project?.overrides || {};
486
+ const localOverrides = optionState?.local?.overrides || {};
487
+ const machineEffective = optionState?.machineEffective || optionState?.repoEffective || {};
488
+ const definitions = bundledPluginRuntimeDefaults?.userConfig?.length
489
+ ? bundledPluginRuntimeDefaults.userConfig
490
+ : (optionState?.definitions || []);
491
+
492
+ pushSectionLine(
493
+ configuredOptionsSection,
494
+ "info",
495
+ "Scope precedence settings.local.json > settings.json > ~/.claude/settings.json > bundled default",
496
+ [
497
+ "managed settings and command-line overrides are not inspected here",
498
+ "effective values below reflect this machine only",
499
+ ]
500
+ );
501
+
502
+ if (projectClaudeSettings?.userExists) {
503
+ if (Object.keys(userOverrides).length > 0) {
504
+ const summary = Object.entries(userOverrides)
505
+ .map(([key, value]) => `${key}=${formatInlineValue(value)}`)
506
+ .join(", ");
507
+ pushSectionLine(
508
+ configuredOptionsSection,
509
+ "info",
510
+ `~/.claude/settings.json ${Object.keys(userOverrides).length} valid override(s)`,
511
+ [
512
+ summary,
513
+ "user-scoped overrides apply across repositories unless project/local settings win",
514
+ ]
515
+ );
516
+ } else {
517
+ pushSectionLine(
518
+ configuredOptionsSection,
519
+ "info",
520
+ "~/.claude/settings.json present without valid CurDX-Flow overrides"
521
+ );
522
+ }
523
+ } else {
524
+ pushSectionLine(
525
+ configuredOptionsSection,
526
+ "info",
527
+ "~/.claude/settings.json not present"
528
+ );
529
+ }
530
+
531
+ if (Object.keys(projectOverrides).length > 0) {
532
+ const summary = Object.entries(projectOverrides)
533
+ .map(([key, value]) => `${key}=${formatInlineValue(value)}`)
534
+ .join(", ");
535
+ pushSectionLine(
536
+ configuredOptionsSection,
537
+ "info",
538
+ `.claude/settings.json ${Object.keys(projectOverrides).length} valid override(s)`,
539
+ [
540
+ summary,
541
+ "path: pluginConfigs[curdx-flow@curdx-flow-marketplace].options",
542
+ ]
543
+ );
544
+ } else {
545
+ pushSectionLine(
546
+ configuredOptionsSection,
547
+ "info",
548
+ ".claude/settings.json no CurDX-Flow repo override"
549
+ );
550
+ }
551
+
552
+ if (projectClaudeSettings?.localExists) {
553
+ if (Object.keys(localOverrides).length > 0) {
554
+ const summary = Object.entries(localOverrides)
555
+ .map(([key, value]) => `${key}=${formatInlineValue(value)}`)
556
+ .join(", ");
557
+ pushSectionLine(
558
+ configuredOptionsSection,
559
+ "info",
560
+ `.claude/settings.local.json ${Object.keys(localOverrides).length} valid override(s)`,
561
+ [
562
+ summary,
563
+ "local overrides have higher precedence than .claude/settings.json on this machine",
564
+ ]
565
+ );
566
+ } else {
567
+ pushSectionLine(
568
+ configuredOptionsSection,
569
+ "info",
570
+ ".claude/settings.local.json present without valid CurDX-Flow overrides"
571
+ );
572
+ }
573
+ } else {
574
+ pushSectionLine(
575
+ configuredOptionsSection,
576
+ "info",
577
+ ".claude/settings.local.json not present"
578
+ );
579
+ }
580
+
581
+ for (const definition of definitions) {
582
+ const effective = machineEffective[definition.key] || {
583
+ value: definition.default,
584
+ source: "default",
585
+ };
586
+ const sourceLabel = effective.source === "local"
587
+ ? "local"
588
+ : effective.source === "project"
589
+ ? "project"
590
+ : effective.source === "user"
591
+ ? "user"
592
+ : "bundled default";
593
+ pushSectionLine(
594
+ configuredOptionsSection,
595
+ "ok",
596
+ `${definition.key.padEnd(22)} ${formatInlineValue(effective.value)} (${sourceLabel})`
597
+ );
598
+ }
599
+ }
600
+
406
601
  const localProjectSection = createSection("Local project:");
407
602
  if (projectState?.exists) {
408
603
  pushSectionLine(localProjectSection, "ok", `.flow/ ${cwd}`);
@@ -490,6 +685,33 @@ export function buildDoctorReport({
490
685
  }
491
686
 
492
687
  const projectSettingsSection = createSection("Project Claude settings:");
688
+ const userSettingsSection = createSection("User Claude settings:");
689
+
690
+ if (projectClaudeSettings?.userExists) {
691
+ if (projectClaudeSettings.userInvalid) {
692
+ pushSectionLine(
693
+ userSettingsSection,
694
+ "err",
695
+ "~/.claude/settings.json invalid JSON",
696
+ [projectClaudeSettings.userParseError]
697
+ );
698
+ } else if ((projectClaudeSettings.userWarnings || []).length > 0) {
699
+ pushSectionLine(userSettingsSection, "warn", "~/.claude/settings.json affects CurDX-Flow globally");
700
+ for (const warning of projectClaudeSettings.userWarnings) {
701
+ pushSectionLine(
702
+ userSettingsSection,
703
+ "warn",
704
+ warning.message,
705
+ projectSettingsWarningDetails(warning)
706
+ );
707
+ }
708
+ } else {
709
+ pushSectionLine(userSettingsSection, "ok", "~/.claude/settings.json present");
710
+ }
711
+ } else {
712
+ pushSectionLine(userSettingsSection, "info", "~/.claude/settings.json not present");
713
+ }
714
+
493
715
  if (projectClaudeSettings?.exists) {
494
716
  if (projectClaudeSettings.invalid) {
495
717
  pushSectionLine(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "Skill-first discipline layer and CLI installer for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {