@curdx/flow 2.3.2 → 2.3.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.
@@ -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.2"
9
+ "version": "2.3.4"
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.2",
19
+ "version": "2.3.4",
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.2",
3
+ "version": "2.3.4",
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 = [
@@ -120,6 +121,23 @@ const CURDX_FLOW_REQUIRED_PLUGIN_IDS = ["context7-plugin@context7-marketplace"];
120
121
  const HTTP_HOOK_SETTINGS = ["allowedHttpHookUrls", "httpHookAllowedEnvVars"];
121
122
  const PERSISTED_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
122
123
  const ENV_EFFORT_LEVELS = [...PERSISTED_EFFORT_LEVELS, "max", "auto"];
124
+ const CURDX_FLOW_RUNTIME_CONSUMERS = {
125
+ autonomous_blocking: {
126
+ envVar: "CLAUDE_PLUGIN_OPTION_AUTONOMOUS_BLOCKING",
127
+ consumer: "hooks/scripts/stop-watcher.sh",
128
+ summary: "controls whether the Stop hook blocks Claude from stopping while execute work remains",
129
+ },
130
+ daily_dependency_check: {
131
+ envVar: "CLAUDE_PLUGIN_OPTION_DAILY_DEPENDENCY_CHECK",
132
+ consumer: "hooks/scripts/session-start.sh",
133
+ summary: "controls the once-per-day recommended companion plugin reminder",
134
+ },
135
+ monitor_interval_seconds: {
136
+ envVar: "CLAUDE_PLUGIN_OPTION_MONITOR_INTERVAL_SECONDS",
137
+ consumer: "monitors/scripts/flow-state-monitor.sh",
138
+ summary: "controls the polling interval for the interactive flow-state monitor",
139
+ },
140
+ };
123
141
 
124
142
  function envFlagEnabled(value) {
125
143
  if (value === true || value === 1) return true;
@@ -208,6 +226,12 @@ function createCurdxFlowPluginOptionsState() {
208
226
  return {
209
227
  pluginId: CURDX_FLOW_PLUGIN_ID,
210
228
  definitions,
229
+ user: {
230
+ pluginConfigsPresent: false,
231
+ pluginConfigPresent: false,
232
+ optionsPresent: false,
233
+ overrides: {},
234
+ },
211
235
  project: {
212
236
  pluginConfigsPresent: false,
213
237
  pluginConfigPresent: false,
@@ -221,11 +245,71 @@ function createCurdxFlowPluginOptionsState() {
221
245
  overrides: {},
222
246
  },
223
247
  repoEffective,
248
+ machineEffective: structuredClone(repoEffective),
224
249
  };
225
250
  }
226
251
 
227
252
  function pluginScopeLabel(scope) {
228
- return scope === "local" ? "settings.local.json" : "settings.json";
253
+ if (scope === "local") return "settings.local.json";
254
+ if (scope === "user") return "~/.claude/settings.json";
255
+ return "settings.json";
256
+ }
257
+
258
+ function invalidSettingKindForScope(scope) {
259
+ if (scope === "local") return "invalid-local-setting";
260
+ if (scope === "user") return "invalid-user-setting";
261
+ return "invalid-project-setting";
262
+ }
263
+
264
+ function applyCurdxFlowPluginOptionOverride(pluginOptionsState, key, value, scope) {
265
+ pluginOptionsState.machineEffective[key] = {
266
+ value,
267
+ source: scope,
268
+ };
269
+
270
+ if (scope === "project" || scope === "local") {
271
+ pluginOptionsState.repoEffective[key] = {
272
+ value,
273
+ source: scope,
274
+ };
275
+ }
276
+ }
277
+
278
+ function buildCurdxFlowRuntimeProjection(pluginOptionsState) {
279
+ return pluginOptionsState.definitions.map((definition) => {
280
+ const effective = pluginOptionsState.machineEffective[definition.key] || {
281
+ value: definition.default,
282
+ source: "default",
283
+ };
284
+ const runtime = CURDX_FLOW_RUNTIME_CONSUMERS[definition.key] || {};
285
+ const details = [];
286
+
287
+ if (definition.key === "autonomous_blocking") {
288
+ details.push(
289
+ effective.value === false
290
+ ? "stop-hook continuation is disabled; Claude may stop at turn end even when execute tasks remain"
291
+ : "stop-hook continuation is enabled; CurDX-Flow can block turn end while execute tasks remain"
292
+ );
293
+ } else if (definition.key === "daily_dependency_check") {
294
+ details.push(
295
+ effective.value === false
296
+ ? "SessionStart plugin reminder is disabled on this machine"
297
+ : "SessionStart plugin reminder runs at most once per day on this machine"
298
+ );
299
+ } else if (definition.key === "monitor_interval_seconds") {
300
+ details.push(`flow-state monitor polls every ${effective.value} second(s) when Monitor is available`);
301
+ }
302
+
303
+ return {
304
+ key: definition.key,
305
+ envVar: runtime.envVar || `CLAUDE_PLUGIN_OPTION_${definition.key.toUpperCase()}`,
306
+ consumer: runtime.consumer || "plugin subprocess",
307
+ summary: runtime.summary || "runtime consumer",
308
+ value: effective.value,
309
+ source: effective.source,
310
+ details,
311
+ };
312
+ });
229
313
  }
230
314
 
231
315
  function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope = "project") {
@@ -233,14 +317,18 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
233
317
  return;
234
318
  }
235
319
 
236
- const target = scope === "local" ? pluginOptionsState.local : pluginOptionsState.project;
320
+ const target = scope === "local"
321
+ ? pluginOptionsState.local
322
+ : scope === "user"
323
+ ? pluginOptionsState.user
324
+ : pluginOptionsState.project;
237
325
  const settingsLabel = pluginScopeLabel(scope);
238
326
  target.pluginConfigsPresent = true;
239
327
 
240
328
  if (!isNonArrayObject(parsed.pluginConfigs)) {
241
329
  pushScopedWarning(
242
330
  warnings,
243
- scope === "local" ? "invalid-local-setting" : "invalid-project-setting",
331
+ invalidSettingKindForScope(scope),
244
332
  `pluginConfigs in ${settingsLabel} must be an object keyed by plugin@marketplace id`,
245
333
  scope
246
334
  );
@@ -257,7 +345,7 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
257
345
  if (!isNonArrayObject(pluginConfig)) {
258
346
  pushScopedWarning(
259
347
  warnings,
260
- scope === "local" ? "invalid-local-setting" : "invalid-project-setting",
348
+ invalidSettingKindForScope(scope),
261
349
  `pluginConfigs[${CURDX_FLOW_PLUGIN_ID}] in ${settingsLabel} must be an object when set`,
262
350
  scope
263
351
  );
@@ -273,7 +361,7 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
273
361
  if (!isNonArrayObject(pluginConfig.options)) {
274
362
  pushScopedWarning(
275
363
  warnings,
276
- scope === "local" ? "invalid-local-setting" : "invalid-project-setting",
364
+ invalidSettingKindForScope(scope),
277
365
  `pluginConfigs[${CURDX_FLOW_PLUGIN_ID}].options in ${settingsLabel} must be an object when set`,
278
366
  scope
279
367
  );
@@ -296,7 +384,7 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
296
384
  if (definition.type === "boolean" && typeof value !== "boolean") {
297
385
  pushScopedWarning(
298
386
  warnings,
299
- scope === "local" ? "invalid-local-setting" : "invalid-project-setting",
387
+ invalidSettingKindForScope(scope),
300
388
  `CurDX-Flow plugin option ${key} in ${settingsLabel} must be boolean`,
301
389
  scope
302
390
  );
@@ -307,7 +395,7 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
307
395
  if (typeof value !== "number" || Number.isNaN(value)) {
308
396
  pushScopedWarning(
309
397
  warnings,
310
- scope === "local" ? "invalid-local-setting" : "invalid-project-setting",
398
+ invalidSettingKindForScope(scope),
311
399
  `CurDX-Flow plugin option ${key} in ${settingsLabel} must be a number`,
312
400
  scope
313
401
  );
@@ -317,7 +405,7 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
317
405
  if (definition.integer && !Number.isInteger(value)) {
318
406
  pushScopedWarning(
319
407
  warnings,
320
- scope === "local" ? "invalid-local-setting" : "invalid-project-setting",
408
+ invalidSettingKindForScope(scope),
321
409
  `CurDX-Flow plugin option ${key} in ${settingsLabel} must be an integer`,
322
410
  scope
323
411
  );
@@ -327,7 +415,7 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
327
415
  if (typeof definition.min === "number" && value < definition.min) {
328
416
  pushScopedWarning(
329
417
  warnings,
330
- scope === "local" ? "invalid-local-setting" : "invalid-project-setting",
418
+ invalidSettingKindForScope(scope),
331
419
  `CurDX-Flow plugin option ${key} in ${settingsLabel} must be between ${definition.min} and ${definition.max}`,
332
420
  scope
333
421
  );
@@ -337,7 +425,7 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
337
425
  if (typeof definition.max === "number" && value > definition.max) {
338
426
  pushScopedWarning(
339
427
  warnings,
340
- scope === "local" ? "invalid-local-setting" : "invalid-project-setting",
428
+ invalidSettingKindForScope(scope),
341
429
  `CurDX-Flow plugin option ${key} in ${settingsLabel} must be between ${definition.min} and ${definition.max}`,
342
430
  scope
343
431
  );
@@ -346,10 +434,7 @@ function auditCurdxFlowPluginOptions(parsed, warnings, pluginOptionsState, scope
346
434
  }
347
435
 
348
436
  target.overrides[key] = value;
349
- pluginOptionsState.repoEffective[key] = {
350
- value,
351
- source: scope,
352
- };
437
+ applyCurdxFlowPluginOptionOverride(pluginOptionsState, key, value, scope);
353
438
  }
354
439
  }
355
440
 
@@ -544,10 +629,15 @@ function auditLocalClaudeSettings(parsed, warnings) {
544
629
  }
545
630
  }
546
631
 
547
- export async function readProjectClaudeSettings(cwd = process.cwd()) {
632
+ export async function readProjectClaudeSettings(cwd = process.cwd(), { homeDir = os.homedir() } = {}) {
548
633
  const settingsPath = path.join(cwd, ".claude", "settings.json");
549
634
  const localSettingsPath = path.join(cwd, ".claude", "settings.local.json");
635
+ const userSettingsPath = path.join(homeDir, ".claude", "settings.json");
550
636
  const state = {
637
+ userExists: false,
638
+ userInvalid: false,
639
+ userParseError: null,
640
+ userWarnings: [],
551
641
  exists: false,
552
642
  localExists: false,
553
643
  invalid: false,
@@ -557,8 +647,16 @@ export async function readProjectClaudeSettings(cwd = process.cwd()) {
557
647
  localParseError: null,
558
648
  localWarnings: [],
559
649
  pluginOptions: createCurdxFlowPluginOptionsState(),
650
+ pluginRuntimeProjection: [],
560
651
  };
561
652
 
653
+ try {
654
+ const userStat = await fs.stat(userSettingsPath);
655
+ state.userExists = userStat.isFile();
656
+ } catch {
657
+ state.userExists = false;
658
+ }
659
+
562
660
  try {
563
661
  const localStat = await fs.stat(localSettingsPath);
564
662
  state.localExists = localStat.isFile();
@@ -566,6 +664,16 @@ export async function readProjectClaudeSettings(cwd = process.cwd()) {
566
664
  state.localExists = false;
567
665
  }
568
666
 
667
+ if (state.userExists) {
668
+ try {
669
+ const userParsed = JSON.parse(await fs.readFile(userSettingsPath, "utf-8"));
670
+ auditCurdxFlowPluginOptions(userParsed, state.userWarnings, state.pluginOptions, "user");
671
+ } catch (error) {
672
+ state.userInvalid = true;
673
+ state.userParseError = error?.message || String(error);
674
+ }
675
+ }
676
+
569
677
  let parsed;
570
678
  try {
571
679
  const stat = await fs.stat(settingsPath);
@@ -919,5 +1027,7 @@ export async function readProjectClaudeSettings(cwd = process.cwd()) {
919
1027
  }
920
1028
  }
921
1029
 
1030
+ state.pluginRuntimeProjection = buildCurdxFlowRuntimeProjection(state.pluginOptions);
1031
+
922
1032
  return state;
923
1033
  }
@@ -13,6 +13,33 @@ 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") {
17
44
  if (warning.kind === "unknown-plugin-option") {
18
45
  return [
@@ -454,9 +481,10 @@ export function buildDoctorReport({
454
481
  ) {
455
482
  const configuredOptionsSection = createSection("CurDX-Flow configured options:");
456
483
  const optionState = projectClaudeSettings?.pluginOptions;
484
+ const userOverrides = optionState?.user?.overrides || {};
457
485
  const projectOverrides = optionState?.project?.overrides || {};
458
486
  const localOverrides = optionState?.local?.overrides || {};
459
- const repoEffective = optionState?.repoEffective || {};
487
+ const machineEffective = optionState?.machineEffective || optionState?.repoEffective || {};
460
488
  const definitions = bundledPluginRuntimeDefaults?.userConfig?.length
461
489
  ? bundledPluginRuntimeDefaults.userConfig
462
490
  : (optionState?.definitions || []);
@@ -464,13 +492,42 @@ export function buildDoctorReport({
464
492
  pushSectionLine(
465
493
  configuredOptionsSection,
466
494
  "info",
467
- "Scope precedence settings.local.json > settings.json > bundled default",
495
+ "Scope precedence settings.local.json > settings.json > ~/.claude/settings.json > bundled default",
468
496
  [
469
- "this section audits repo-scoped CurDX-Flow plugin options only",
470
- "user-level ~/.claude/settings.json is not inspected here",
497
+ "managed settings and command-line overrides are not inspected here",
498
+ "effective values below reflect this machine only",
471
499
  ]
472
500
  );
473
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
+
474
531
  if (Object.keys(projectOverrides).length > 0) {
475
532
  const summary = Object.entries(projectOverrides)
476
533
  .map(([key, value]) => `${key}=${formatInlineValue(value)}`)
@@ -522,7 +579,7 @@ export function buildDoctorReport({
522
579
  }
523
580
 
524
581
  for (const definition of definitions) {
525
- const effective = repoEffective[definition.key] || {
582
+ const effective = machineEffective[definition.key] || {
526
583
  value: definition.default,
527
584
  source: "default",
528
585
  };
@@ -530,6 +587,8 @@ export function buildDoctorReport({
530
587
  ? "local"
531
588
  : effective.source === "project"
532
589
  ? "project"
590
+ : effective.source === "user"
591
+ ? "user"
533
592
  : "bundled default";
534
593
  pushSectionLine(
535
594
  configuredOptionsSection,
@@ -539,6 +598,30 @@ export function buildDoctorReport({
539
598
  }
540
599
  }
541
600
 
601
+ if ((projectClaudeSettings?.pluginRuntimeProjection || []).length > 0) {
602
+ const runtimeProjectionSection = createSection("CurDX-Flow runtime projection:");
603
+
604
+ for (const entry of projectClaudeSettings.pluginRuntimeProjection) {
605
+ const sourceLabel = entry.source === "local"
606
+ ? "local"
607
+ : entry.source === "project"
608
+ ? "project"
609
+ : entry.source === "user"
610
+ ? "user"
611
+ : "bundled default";
612
+ pushSectionLine(
613
+ runtimeProjectionSection,
614
+ "info",
615
+ `${entry.envVar.padEnd(36)} ${formatInlineValue(entry.value)} (${sourceLabel})`,
616
+ [
617
+ `consumer: ${entry.consumer}`,
618
+ entry.summary,
619
+ ...(entry.details || []),
620
+ ]
621
+ );
622
+ }
623
+ }
624
+
542
625
  const localProjectSection = createSection("Local project:");
543
626
  if (projectState?.exists) {
544
627
  pushSectionLine(localProjectSection, "ok", `.flow/ ${cwd}`);
@@ -626,6 +709,33 @@ export function buildDoctorReport({
626
709
  }
627
710
 
628
711
  const projectSettingsSection = createSection("Project Claude settings:");
712
+ const userSettingsSection = createSection("User Claude settings:");
713
+
714
+ if (projectClaudeSettings?.userExists) {
715
+ if (projectClaudeSettings.userInvalid) {
716
+ pushSectionLine(
717
+ userSettingsSection,
718
+ "err",
719
+ "~/.claude/settings.json invalid JSON",
720
+ [projectClaudeSettings.userParseError]
721
+ );
722
+ } else if ((projectClaudeSettings.userWarnings || []).length > 0) {
723
+ pushSectionLine(userSettingsSection, "warn", "~/.claude/settings.json affects CurDX-Flow globally");
724
+ for (const warning of projectClaudeSettings.userWarnings) {
725
+ pushSectionLine(
726
+ userSettingsSection,
727
+ "warn",
728
+ warning.message,
729
+ projectSettingsWarningDetails(warning)
730
+ );
731
+ }
732
+ } else {
733
+ pushSectionLine(userSettingsSection, "ok", "~/.claude/settings.json present");
734
+ }
735
+ } else {
736
+ pushSectionLine(userSettingsSection, "info", "~/.claude/settings.json not present");
737
+ }
738
+
629
739
  if (projectClaudeSettings?.exists) {
630
740
  if (projectClaudeSettings.invalid) {
631
741
  pushSectionLine(
@@ -88,6 +88,7 @@ Guarded artifact targets:
88
88
  - `autonomous_blocking`: lets users disable stop-hook continuation without editing plugin files.
89
89
  - `daily_dependency_check`: silences or enables the once-per-day recommended-plugin reminder.
90
90
  - `monitor_interval_seconds`: controls plugin monitor polling cadence.
91
+ - `doctor` should explain both the machine-effective config value and the projected plugin subprocess env var for these knobs, since hook/monitor behavior depends on the env projection rather than direct JSON parsing.
91
92
 
92
93
  ## Plugin Dependency Constraints
93
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "Skill-first discipline layer and CLI installer for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {