@curdx/flow 2.2.3 → 2.2.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.
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { readProjectClaudeSettings } from "./lib/doctor-claude-settings.js";
5
+ import { inspectRuntimeEnvironment } from "./lib/doctor-runtime-environment.js";
4
6
  import {
5
7
  claudeVersion,
6
8
  color,
@@ -14,6 +16,9 @@ import {
14
16
  runSync,
15
17
  } from "./utils.js";
16
18
 
19
+ export { readProjectClaudeSettings };
20
+ export { inspectRuntimeEnvironment };
21
+
17
22
  export function createDoctorContext(args = []) {
18
23
  return {
19
24
  fix: args.includes("--fix"),
@@ -54,405 +59,6 @@ function looksLikeRelativePathToken(value) {
54
59
  /(^|[\\/])(?:scripts?|bin|dist|src|tools|vendor|node_modules|venv|\.venv)([\\/]|$)/i.test(token)
55
60
  );
56
61
  }
57
-
58
- const PROJECT_ONLY_IGNORED_SETTINGS = [
59
- {
60
- key: "autoMemoryDirectory",
61
- kind: "ignored-project-setting",
62
- message: "autoMemoryDirectory is not accepted in project settings; move it to user or local settings",
63
- },
64
- {
65
- key: "autoMode",
66
- kind: "ignored-project-setting",
67
- message: "autoMode is not read from shared project settings; move it to user or local settings",
68
- },
69
- {
70
- key: "useAutoModeDuringPlan",
71
- kind: "ignored-project-setting",
72
- message: "useAutoModeDuringPlan is not read from shared project settings; move it to user or local settings",
73
- },
74
- {
75
- key: "sshConfigs",
76
- kind: "ignored-project-setting",
77
- message: "sshConfigs is only read from user or managed settings; remove it from shared project settings",
78
- },
79
- {
80
- key: "teammateMode",
81
- kind: "invalid-project-setting",
82
- message: "teammateMode belongs in the global ~/.claude.json config, not project settings.json",
83
- },
84
- ];
85
-
86
- const MANAGED_ONLY_SETTINGS = [
87
- "allowedChannelPlugins",
88
- "allowedMcpServers",
89
- "allowManagedHooksOnly",
90
- "allowManagedMcpServersOnly",
91
- "allowManagedPermissionRulesOnly",
92
- "blockedMarketplaces",
93
- "channelsEnabled",
94
- "deniedMcpServers",
95
- "forceRemoteSettingsRefresh",
96
- "pluginTrustMessage",
97
- "strictKnownMarketplaces",
98
- ];
99
-
100
- const SHARED_SCRIPT_SETTINGS = [
101
- {
102
- key: "apiKeyHelper",
103
- kind: "shared-script-setting",
104
- message: "apiKeyHelper runs a local script from shared project settings",
105
- },
106
- {
107
- key: "awsAuthRefresh",
108
- kind: "shared-script-setting",
109
- message: "awsAuthRefresh runs a local credential-refresh script from shared project settings",
110
- },
111
- {
112
- key: "awsCredentialExport",
113
- kind: "shared-script-setting",
114
- message: "awsCredentialExport runs a local credential-export script from shared project settings",
115
- },
116
- {
117
- key: "otelHeadersHelper",
118
- kind: "shared-script-setting",
119
- message: "otelHeadersHelper runs a local telemetry script from shared project settings",
120
- },
121
- ];
122
-
123
- const CURDX_FLOW_AGENT_TYPES = [
124
- "flow-adversary",
125
- "flow-architect",
126
- "flow-brownfield-analyst",
127
- "flow-debugger",
128
- "flow-edge-hunter",
129
- "flow-executor",
130
- "flow-planner",
131
- "flow-product-designer",
132
- "flow-qa-engineer",
133
- "flow-researcher",
134
- "flow-reviewer",
135
- "flow-security-auditor",
136
- "flow-triage-analyst",
137
- "flow-ui-researcher",
138
- "flow-ux-designer",
139
- "flow-verifier",
140
- ];
141
-
142
- const CURDX_FLOW_REQUIRED_MODEL_ALIASES = ["sonnet", "opus"];
143
- const CURDX_FLOW_REQUIRED_TOOLS = ["Agent", "AskUserQuestion", "Bash", "Monitor", "Read", "Write", "Edit", "Grep", "Glob"];
144
- const CURDX_FLOW_PLUGIN_ID = "curdx-flow@curdx-flow-marketplace";
145
- const CURDX_FLOW_REQUIRED_PLUGIN_IDS = ["context7-plugin@context7-marketplace"];
146
- const HTTP_HOOK_SETTINGS = ["allowedHttpHookUrls", "httpHookAllowedEnvVars"];
147
- const PERSISTED_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
148
- const ENV_EFFORT_LEVELS = [...PERSISTED_EFFORT_LEVELS, "max", "auto"];
149
- const PINNED_MODEL_ENV_FAMILIES = [
150
- {
151
- modelVar: "ANTHROPIC_DEFAULT_OPUS_MODEL",
152
- capsVar: "ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES",
153
- label: "Opus",
154
- },
155
- {
156
- modelVar: "ANTHROPIC_DEFAULT_SONNET_MODEL",
157
- capsVar: "ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES",
158
- label: "Sonnet",
159
- },
160
- {
161
- modelVar: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
162
- capsVar: "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES",
163
- label: "Haiku",
164
- },
165
- {
166
- modelVar: "ANTHROPIC_CUSTOM_MODEL_OPTION",
167
- capsVar: "ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES",
168
- label: "Custom model option",
169
- },
170
- ];
171
-
172
- function envFlagEnabled(value) {
173
- if (value === true || value === 1) return true;
174
- if (typeof value !== "string") return false;
175
- return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
176
- }
177
-
178
- function normalizedEnvValue(value) {
179
- if (typeof value !== "string") return null;
180
- const normalized = value.trim();
181
- return normalized.length > 0 ? normalized : null;
182
- }
183
-
184
- function stripExtendedContextSuffix(modelId) {
185
- return modelId.replace(/\[1m\]$/i, "");
186
- }
187
-
188
- function looksProviderSpecificModelId(modelId) {
189
- const normalized = stripExtendedContextSuffix(modelId);
190
- if (normalized.includes(":") || normalized.includes("/")) return true;
191
- if (/^(?:us\.)?anthropic\./i.test(normalized)) return true;
192
- if (!/^claude-(?:opus|sonnet|haiku)-/i.test(normalized)) return true;
193
- return false;
194
- }
195
-
196
- function positiveIntegerFromEnv(value) {
197
- const normalized = normalizedEnvValue(value);
198
- if (!normalized) return null;
199
- if (!/^[0-9]+$/.test(normalized)) return Number.NaN;
200
- const parsed = Number(normalized);
201
- return parsed > 0 ? parsed : Number.NaN;
202
- }
203
-
204
- function parsePermissionRule(rule) {
205
- if (typeof rule !== "string") return null;
206
- const match = rule.match(/^([A-Za-z][A-Za-z0-9]*)(?:\((.*)\))?$/);
207
- if (!match) return null;
208
- return {
209
- tool: match[1],
210
- matcher: match[2] ?? null,
211
- };
212
- }
213
-
214
- function isBroadToolDeny(rule) {
215
- const parsed = parsePermissionRule(rule);
216
- if (!parsed || !CURDX_FLOW_REQUIRED_TOOLS.includes(parsed.tool)) return false;
217
- return parsed.matcher === null || parsed.matcher === "*" || parsed.matcher === "**";
218
- }
219
-
220
- function isCurdxFlowAgentDeny(rule) {
221
- const parsed = parsePermissionRule(rule);
222
- if (!parsed || parsed.tool !== "Agent") return false;
223
- if (parsed.matcher === null || parsed.matcher === "*" || parsed.matcher === "**") return true;
224
- return CURDX_FLOW_AGENT_TYPES.some(
225
- (agentType) => parsed.matcher === agentType || parsed.matcher === `curdx-flow:${agentType}`
226
- );
227
- }
228
-
229
- function disabledPluginIdsFromSettings(enabledPlugins) {
230
- if (!enabledPlugins || typeof enabledPlugins !== "object" || Array.isArray(enabledPlugins)) {
231
- return [];
232
- }
233
-
234
- return Object.entries(enabledPlugins)
235
- .filter(([, enabled]) => enabled === false)
236
- .map(([pluginId]) => pluginId);
237
- }
238
-
239
- function isNonArrayObject(value) {
240
- return value && typeof value === "object" && !Array.isArray(value);
241
- }
242
-
243
- function hasProjectBlockingSandboxPath(values) {
244
- if (!Array.isArray(values)) return false;
245
-
246
- return values.some((value) => {
247
- if (typeof value !== "string") return false;
248
- const token = value.trim().replace(/\/\*{1,2}$/, "").replace(/\/$/, "");
249
- return [
250
- ".",
251
- ".flow",
252
- "./.flow",
253
- ".git",
254
- "./.git",
255
- ].includes(token);
256
- });
257
- }
258
-
259
- function pushScopedWarning(target, kind, message, scope = "project") {
260
- const warning = { kind, message };
261
- if (scope !== "project") warning.scope = scope;
262
- target.push(warning);
263
- }
264
-
265
- function auditLocalClaudeSettings(parsed, warnings) {
266
- const scope = "local";
267
- const permissions = parsed?.permissions && typeof parsed.permissions === "object"
268
- ? parsed.permissions
269
- : {};
270
- const deny = Array.isArray(permissions.deny) ? permissions.deny : [];
271
-
272
- if ("enabledPlugins" in parsed && (
273
- !parsed.enabledPlugins ||
274
- typeof parsed.enabledPlugins !== "object" ||
275
- Array.isArray(parsed.enabledPlugins)
276
- )) {
277
- pushScopedWarning(
278
- warnings,
279
- "invalid-local-setting",
280
- "enabledPlugins in settings.local.json must be an object keyed by plugin@marketplace id",
281
- scope
282
- );
283
- }
284
-
285
- const disabledPluginIds = disabledPluginIdsFromSettings(parsed.enabledPlugins);
286
- if (disabledPluginIds.includes(CURDX_FLOW_PLUGIN_ID)) {
287
- pushScopedWarning(
288
- warnings,
289
- "flow-runtime-blocker",
290
- `settings.local.json disables CurDX-Flow for this machine: ${CURDX_FLOW_PLUGIN_ID}`,
291
- scope
292
- );
293
- }
294
-
295
- for (const pluginId of CURDX_FLOW_REQUIRED_PLUGIN_IDS) {
296
- if (disabledPluginIds.includes(pluginId)) {
297
- pushScopedWarning(
298
- warnings,
299
- "required-plugin-disabled",
300
- `settings.local.json disables required CurDX-Flow companion plugin: ${pluginId}`,
301
- scope
302
- );
303
- }
304
- }
305
-
306
- if (permissions.defaultMode === "bypassPermissions") {
307
- pushScopedWarning(
308
- warnings,
309
- "bypass-default-mode",
310
- 'permissions.defaultMode in settings.local.json is "bypassPermissions"',
311
- scope
312
- );
313
- }
314
-
315
- if (permissions.defaultMode === "dontAsk") {
316
- pushScopedWarning(
317
- warnings,
318
- "flow-runtime-blocker",
319
- 'permissions.defaultMode in settings.local.json is "dontAsk"; CurDX-Flow clarification and Agent dispatch prompts may be auto-denied',
320
- scope
321
- );
322
- }
323
-
324
- for (const rule of deny) {
325
- if (isCurdxFlowAgentDeny(rule)) {
326
- pushScopedWarning(
327
- warnings,
328
- "flow-runtime-blocker",
329
- `settings.local.json deny rule blocks CurDX-Flow subagent dispatch: ${rule}`,
330
- scope
331
- );
332
- continue;
333
- }
334
-
335
- if (isBroadToolDeny(rule)) {
336
- pushScopedWarning(
337
- warnings,
338
- "flow-runtime-blocker",
339
- `settings.local.json deny rule blocks a tool CurDX-Flow workflows require: ${rule}`,
340
- scope
341
- );
342
- }
343
- }
344
-
345
- if (parsed.env && typeof parsed.env === "object" && !Array.isArray(parsed.env)) {
346
- if (envFlagEnabled(parsed.env.CLAUDE_CODE_SIMPLE)) {
347
- pushScopedWarning(
348
- warnings,
349
- "flow-runtime-blocker",
350
- "settings.local.json env.CLAUDE_CODE_SIMPLE=1 enables bare/simple mode, disabling CurDX-Flow plugin, hook, skill, MCP, and CLAUDE.md discovery",
351
- scope
352
- );
353
- }
354
-
355
- if (envFlagEnabled(parsed.env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT)) {
356
- pushScopedWarning(
357
- warnings,
358
- "local-runtime-setting",
359
- "settings.local.json env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=1 forces the minimal Claude system prompt for local sessions",
360
- scope
361
- );
362
- }
363
- }
364
-
365
- if (parsed.disableAllHooks === true) {
366
- pushScopedWarning(
367
- warnings,
368
- "flow-runtime-blocker",
369
- "settings.local.json disableAllHooks disables CurDX-Flow stop/recovery hooks and plugin subagent status lines",
370
- scope
371
- );
372
- }
373
-
374
- if (parsed.disableSkillShellExecution === true) {
375
- pushScopedWarning(
376
- warnings,
377
- "skill-shell-disabled",
378
- "settings.local.json disableSkillShellExecution disables inline shell expansion in project/plugin skills and commands",
379
- scope
380
- );
381
- }
382
-
383
- if (Array.isArray(parsed.availableModels)) {
384
- const missing = CURDX_FLOW_REQUIRED_MODEL_ALIASES.filter(
385
- (modelAlias) => !parsed.availableModels.includes(modelAlias)
386
- );
387
- if (missing.length > 0) {
388
- pushScopedWarning(
389
- warnings,
390
- "flow-runtime-blocker",
391
- `settings.local.json availableModels excludes CurDX-Flow agent model aliases: ${missing.join(", ")}`,
392
- scope
393
- );
394
- }
395
- }
396
-
397
- if ("agent" in parsed) {
398
- if (typeof parsed.agent !== "string" || parsed.agent.trim().length === 0) {
399
- pushScopedWarning(
400
- warnings,
401
- "invalid-local-setting",
402
- "agent in settings.local.json must be a non-empty subagent name when set",
403
- scope
404
- );
405
- } else {
406
- pushScopedWarning(
407
- warnings,
408
- "flow-runtime-blocker",
409
- `settings.local.json routes the main thread through subagent "${parsed.agent}", overriding CurDX-Flow prompt, tools, and model`,
410
- scope
411
- );
412
- }
413
- }
414
-
415
- if ("sandbox" in parsed) {
416
- if (!isNonArrayObject(parsed.sandbox)) {
417
- pushScopedWarning(
418
- warnings,
419
- "invalid-local-setting",
420
- "sandbox in settings.local.json must be an object when set",
421
- scope
422
- );
423
- } else {
424
- if (parsed.sandbox.failIfUnavailable === true) {
425
- pushScopedWarning(
426
- warnings,
427
- "flow-runtime-blocker",
428
- "settings.local.json sandbox.failIfUnavailable can prevent Claude Code startup on hosts where sandboxing is unavailable",
429
- scope
430
- );
431
- }
432
-
433
- const filesystem = isNonArrayObject(parsed.sandbox.filesystem)
434
- ? parsed.sandbox.filesystem
435
- : {};
436
- if (hasProjectBlockingSandboxPath(filesystem.denyRead)) {
437
- pushScopedWarning(
438
- warnings,
439
- "flow-runtime-blocker",
440
- "settings.local.json sandbox.filesystem.denyRead blocks .flow/.git or the project root, which CurDX-Flow must inspect",
441
- scope
442
- );
443
- }
444
- if (hasProjectBlockingSandboxPath(filesystem.denyWrite)) {
445
- pushScopedWarning(
446
- warnings,
447
- "flow-runtime-blocker",
448
- "settings.local.json sandbox.filesystem.denyWrite blocks .flow/.git or the project root, which CurDX-Flow must update",
449
- scope
450
- );
451
- }
452
- }
453
- }
454
- }
455
-
456
62
  export async function readProjectMcpConfig(cwd = process.cwd()) {
457
63
  const rootPath = path.join(cwd, ".mcp.json");
458
64
  const misplacedPath = path.join(cwd, ".claude", ".mcp.json");
@@ -544,380 +150,6 @@ export async function readProjectTeamConfig(cwd = process.cwd()) {
544
150
  return state;
545
151
  }
546
152
 
547
- export async function readProjectClaudeSettings(cwd = process.cwd()) {
548
- const settingsPath = path.join(cwd, ".claude", "settings.json");
549
- const localSettingsPath = path.join(cwd, ".claude", "settings.local.json");
550
- const state = {
551
- exists: false,
552
- localExists: false,
553
- invalid: false,
554
- parseError: null,
555
- warnings: [],
556
- localInvalid: false,
557
- localParseError: null,
558
- localWarnings: [],
559
- };
560
-
561
- try {
562
- const localStat = await fs.stat(localSettingsPath);
563
- state.localExists = localStat.isFile();
564
- } catch {
565
- state.localExists = false;
566
- }
567
-
568
- let parsed;
569
- try {
570
- const stat = await fs.stat(settingsPath);
571
- if (!stat.isFile()) {
572
- parsed = null;
573
- } else {
574
- state.exists = true;
575
- parsed = JSON.parse(await fs.readFile(settingsPath, "utf-8"));
576
- }
577
- } catch (error) {
578
- if (error?.code !== "ENOENT") {
579
- state.invalid = true;
580
- state.parseError = error.message;
581
- return state;
582
- }
583
- }
584
-
585
- if (state.exists) {
586
- const permissions = parsed?.permissions && typeof parsed.permissions === "object"
587
- ? parsed.permissions
588
- : {};
589
- const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
590
- const deny = Array.isArray(permissions.deny) ? permissions.deny : [];
591
-
592
- if ("enabledPlugins" in parsed && (
593
- !parsed.enabledPlugins ||
594
- typeof parsed.enabledPlugins !== "object" ||
595
- Array.isArray(parsed.enabledPlugins)
596
- )) {
597
- pushScopedWarning(
598
- state.warnings,
599
- "invalid-project-setting",
600
- "enabledPlugins must be an object keyed by plugin@marketplace id"
601
- );
602
- }
603
-
604
- const disabledPluginIds = disabledPluginIdsFromSettings(parsed.enabledPlugins);
605
- if (disabledPluginIds.includes(CURDX_FLOW_PLUGIN_ID)) {
606
- state.warnings.push({
607
- kind: "flow-runtime-blocker",
608
- message: `enabledPlugins disables CurDX-Flow in this project: ${CURDX_FLOW_PLUGIN_ID}`,
609
- });
610
- }
611
-
612
- for (const pluginId of CURDX_FLOW_REQUIRED_PLUGIN_IDS) {
613
- if (disabledPluginIds.includes(pluginId)) {
614
- state.warnings.push({
615
- kind: "required-plugin-disabled",
616
- message: `enabledPlugins disables required CurDX-Flow companion plugin: ${pluginId}`,
617
- });
618
- }
619
- }
620
-
621
- if (permissions.defaultMode === "bypassPermissions") {
622
- state.warnings.push({
623
- kind: "bypass-default-mode",
624
- message: 'permissions.defaultMode is "bypassPermissions"',
625
- });
626
- }
627
-
628
- if (permissions.defaultMode === "dontAsk") {
629
- state.warnings.push({
630
- kind: "flow-runtime-blocker",
631
- message: 'permissions.defaultMode is "dontAsk"; CurDX-Flow clarification and Agent dispatch prompts may be auto-denied',
632
- });
633
- }
634
-
635
- if (permissions.skipDangerousModePermissionPrompt === true) {
636
- state.warnings.push({
637
- kind: "ignored-project-setting",
638
- message: "permissions.skipDangerousModePermissionPrompt is ignored in project settings; move it to user or local settings",
639
- });
640
- }
641
-
642
- for (const rule of allow) {
643
- if (rule === "Bash" || rule === "Bash(*)" || rule === "*" || rule === "Read(**)") {
644
- state.warnings.push({
645
- kind: "broad-allow-rule",
646
- message: `overbroad allow rule: ${rule}`,
647
- });
648
- }
649
- }
650
-
651
- for (const rule of deny) {
652
- if (isCurdxFlowAgentDeny(rule)) {
653
- state.warnings.push({
654
- kind: "flow-runtime-blocker",
655
- message: `deny rule blocks CurDX-Flow subagent dispatch: ${rule}`,
656
- });
657
- continue;
658
- }
659
-
660
- if (isBroadToolDeny(rule)) {
661
- state.warnings.push({
662
- kind: "flow-runtime-blocker",
663
- message: `deny rule blocks a tool CurDX-Flow workflows require: ${rule}`,
664
- });
665
- }
666
- }
667
-
668
- const sensitiveDenyPatterns = ["Read(./.env)", "Read(./.env.*)"];
669
- for (const pattern of sensitiveDenyPatterns) {
670
- if (!deny.includes(pattern)) {
671
- state.warnings.push({
672
- kind: "missing-sensitive-deny",
673
- message: `missing recommended deny rule: ${pattern}`,
674
- });
675
- }
676
- }
677
-
678
- for (const setting of PROJECT_ONLY_IGNORED_SETTINGS) {
679
- if (setting.key in parsed) {
680
- state.warnings.push({
681
- kind: setting.kind,
682
- message: setting.message,
683
- });
684
- }
685
- }
686
-
687
- for (const key of MANAGED_ONLY_SETTINGS) {
688
- if (key in parsed) {
689
- state.warnings.push({
690
- kind: "managed-only-setting",
691
- message: `${key} only applies from managed settings; remove it from shared project settings`,
692
- });
693
- }
694
- }
695
-
696
- for (const setting of SHARED_SCRIPT_SETTINGS) {
697
- if (setting.key in parsed) {
698
- state.warnings.push({
699
- kind: setting.kind,
700
- message: setting.message,
701
- });
702
- }
703
- }
704
-
705
- if (parsed.fileSuggestion?.type === "command" && parsed.fileSuggestion.command) {
706
- state.warnings.push({
707
- kind: "shared-script-setting",
708
- message: "fileSuggestion.command runs a local script from shared project settings",
709
- });
710
- }
711
-
712
- if (parsed.statusLine?.type === "command" && parsed.statusLine.command) {
713
- state.warnings.push({
714
- kind: "shared-script-setting",
715
- message: "statusLine.command runs a local script from shared project settings",
716
- });
717
- }
718
-
719
- if (parsed.env && typeof parsed.env === "object" && !Array.isArray(parsed.env)) {
720
- state.warnings.push({
721
- kind: "shared-env-setting",
722
- message: "env injects shared environment variables into every session; avoid machine-specific values or secrets",
723
- });
724
-
725
- if (envFlagEnabled(parsed.env.CLAUDE_CODE_SIMPLE)) {
726
- state.warnings.push({
727
- kind: "flow-runtime-blocker",
728
- message: "env.CLAUDE_CODE_SIMPLE=1 enables bare/simple mode, disabling CurDX-Flow plugin, hook, skill, MCP, and CLAUDE.md discovery",
729
- });
730
- }
731
-
732
- if (envFlagEnabled(parsed.env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT)) {
733
- state.warnings.push({
734
- kind: "shared-env-setting",
735
- message: "env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=1 forces the minimal Claude system prompt for every collaborator session",
736
- });
737
- }
738
-
739
- const effortEnv = normalizedEnvValue(parsed.env.CLAUDE_CODE_EFFORT_LEVEL);
740
- if (effortEnv && !ENV_EFFORT_LEVELS.includes(effortEnv)) {
741
- state.warnings.push({
742
- kind: "invalid-project-setting",
743
- message: `env.CLAUDE_CODE_EFFORT_LEVEL must be one of ${ENV_EFFORT_LEVELS.join(", ")}`,
744
- });
745
- } else if (effortEnv === "low" || effortEnv === "medium") {
746
- state.warnings.push({
747
- kind: "low-effort-project-setting",
748
- message: `env.CLAUDE_CODE_EFFORT_LEVEL=${effortEnv} lowers reasoning for every collaborator session`,
749
- });
750
- }
751
-
752
- if ("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" in parsed.env) {
753
- state.warnings.push({
754
- kind: "shared-env-setting",
755
- message: "env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS shares an experimental Claude runtime flag across this project",
756
- });
757
- }
758
- }
759
-
760
- if (parsed.disableAllHooks === true) {
761
- state.warnings.push({
762
- kind: "flow-runtime-blocker",
763
- message: "disableAllHooks disables CurDX-Flow stop/recovery hooks and plugin subagent status lines",
764
- });
765
- }
766
-
767
- if (parsed.disableSkillShellExecution === true) {
768
- state.warnings.push({
769
- kind: "skill-shell-disabled",
770
- message: "disableSkillShellExecution disables inline shell expansion in project/plugin skills and commands",
771
- });
772
- }
773
-
774
- for (const key of HTTP_HOOK_SETTINGS) {
775
- if (!(key in parsed)) continue;
776
- if (!Array.isArray(parsed[key])) {
777
- state.warnings.push({
778
- kind: "invalid-project-setting",
779
- message: `${key} must be an array when set in settings.json`,
780
- });
781
- continue;
782
- }
783
- if (parsed[key].length === 0) {
784
- state.warnings.push({
785
- kind: "shared-hook-policy",
786
- message: `${key} is an empty allowlist; matching HTTP hook behavior is blocked`,
787
- });
788
- }
789
- }
790
-
791
- if (Array.isArray(parsed.availableModels)) {
792
- const missing = CURDX_FLOW_REQUIRED_MODEL_ALIASES.filter(
793
- (modelAlias) => !parsed.availableModels.includes(modelAlias)
794
- );
795
- if (missing.length > 0) {
796
- state.warnings.push({
797
- kind: "flow-runtime-blocker",
798
- message: `availableModels excludes CurDX-Flow agent model aliases: ${missing.join(", ")}`,
799
- });
800
- }
801
- }
802
-
803
- if ("agent" in parsed) {
804
- if (typeof parsed.agent !== "string" || parsed.agent.trim().length === 0) {
805
- state.warnings.push({
806
- kind: "invalid-project-setting",
807
- message: "agent must be a non-empty subagent name when set in settings.json",
808
- });
809
- } else {
810
- state.warnings.push({
811
- kind: "flow-runtime-blocker",
812
- message: `agent routes the main thread through subagent "${parsed.agent}", overriding CurDX-Flow prompt, tools, and model`,
813
- });
814
- }
815
- }
816
-
817
- if ("effortLevel" in parsed) {
818
- if (parsed.effortLevel === "max") {
819
- state.warnings.push({
820
- kind: "invalid-project-setting",
821
- message:
822
- "effortLevel in settings.json only supports low, medium, high, or xhigh; use /effort max or CLAUDE_CODE_EFFORT_LEVEL for session-only max",
823
- });
824
- } else if (!PERSISTED_EFFORT_LEVELS.includes(parsed.effortLevel)) {
825
- state.warnings.push({
826
- kind: "invalid-project-setting",
827
- message:
828
- "effortLevel in settings.json must be one of low, medium, high, or xhigh",
829
- });
830
- } else if (parsed.effortLevel === "low" || parsed.effortLevel === "medium") {
831
- state.warnings.push({
832
- kind: "low-effort-project-setting",
833
- message: `effortLevel ${parsed.effortLevel} is shared at project scope; CurDX-Flow planning/review turns may need higher reasoning`,
834
- });
835
- }
836
- }
837
-
838
- if ("sandbox" in parsed) {
839
- if (!isNonArrayObject(parsed.sandbox)) {
840
- state.warnings.push({
841
- kind: "invalid-project-setting",
842
- message: "sandbox must be an object when set in settings.json",
843
- });
844
- } else {
845
- if (parsed.sandbox.failIfUnavailable === true) {
846
- state.warnings.push({
847
- kind: "flow-runtime-blocker",
848
- message: "sandbox.failIfUnavailable can prevent Claude Code startup on hosts where sandboxing is unavailable",
849
- });
850
- }
851
- if (parsed.sandbox.allowUnsandboxedCommands === false) {
852
- state.warnings.push({
853
- kind: "shared-sandbox-policy",
854
- message: "sandbox.allowUnsandboxedCommands=false disables the unsandboxed Bash escape hatch for this project",
855
- });
856
- }
857
-
858
- const filesystem = isNonArrayObject(parsed.sandbox.filesystem)
859
- ? parsed.sandbox.filesystem
860
- : {};
861
- if (hasProjectBlockingSandboxPath(filesystem.denyRead)) {
862
- state.warnings.push({
863
- kind: "flow-runtime-blocker",
864
- message: "sandbox.filesystem.denyRead blocks .flow/.git or the project root, which CurDX-Flow must inspect",
865
- });
866
- }
867
- if (hasProjectBlockingSandboxPath(filesystem.denyWrite)) {
868
- state.warnings.push({
869
- kind: "flow-runtime-blocker",
870
- message: "sandbox.filesystem.denyWrite blocks .flow/.git or the project root, which CurDX-Flow must update",
871
- });
872
- }
873
-
874
- const network = isNonArrayObject(parsed.sandbox.network)
875
- ? parsed.sandbox.network
876
- : {};
877
- if (Array.isArray(network.allowedDomains) && network.allowedDomains.length === 0) {
878
- state.warnings.push({
879
- kind: "shared-sandbox-policy",
880
- message: "sandbox.network.allowedDomains is empty; sandboxed commands have no outbound network access",
881
- });
882
- }
883
- }
884
- }
885
-
886
- if (parsed.enableAllProjectMcpServers === true) {
887
- state.warnings.push({
888
- kind: "shared-mcp-auto-approve",
889
- message: "enableAllProjectMcpServers auto-approves every project MCP server",
890
- });
891
- }
892
-
893
- if (Array.isArray(parsed.enabledMcpjsonServers) && parsed.enabledMcpjsonServers.length > 0) {
894
- state.warnings.push({
895
- kind: "shared-mcp-auto-approve",
896
- message: `enabledMcpjsonServers auto-approves project MCP servers: ${parsed.enabledMcpjsonServers.join(", ")}`,
897
- });
898
- }
899
-
900
- if ("includeCoAuthoredBy" in parsed) {
901
- state.warnings.push({
902
- kind: "deprecated-setting",
903
- message: "includeCoAuthoredBy is deprecated; migrate to attribution",
904
- });
905
- }
906
- }
907
-
908
- if (state.localExists) {
909
- try {
910
- const localParsed = JSON.parse(await fs.readFile(localSettingsPath, "utf-8"));
911
- auditLocalClaudeSettings(localParsed, state.localWarnings);
912
- } catch (error) {
913
- state.localInvalid = true;
914
- state.localParseError = error?.message || String(error);
915
- }
916
- }
917
-
918
- return state;
919
- }
920
-
921
153
  export async function readProjectState(cwd = process.cwd()) {
922
154
  const flowDir = path.join(cwd, ".flow");
923
155
  try {
@@ -937,147 +169,6 @@ export async function readProjectState(cwd = process.cwd()) {
937
169
  }
938
170
  }
939
171
 
940
- export function inspectRuntimeEnvironment(env = process.env) {
941
- const entries = [];
942
- const inCi = envFlagEnabled(env.CI);
943
-
944
- if (envFlagEnabled(env.CLAUDE_CODE_SIMPLE)) {
945
- entries.push({
946
- level: "err",
947
- text: "CLAUDE_CODE_SIMPLE enabled (bare/simple mode)",
948
- details: [
949
- "official docs: this disables auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md",
950
- "CurDX-Flow cannot load correctly in this mode; unset it before launching Claude Code",
951
- ],
952
- });
953
- }
954
-
955
- if (envFlagEnabled(env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT)) {
956
- entries.push({
957
- level: "warn",
958
- text: "CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT enabled",
959
- details: [
960
- "official docs: discovery still works, but Claude runs with the minimal system prompt and collapsed tool descriptions",
961
- "CurDX-Flow may still load, but planning/review behavior can degrade versus the normal Claude Code prompt",
962
- ],
963
- });
964
- }
965
-
966
- const effortLevel = normalizedEnvValue(env.CLAUDE_CODE_EFFORT_LEVEL);
967
- if (effortLevel && !ENV_EFFORT_LEVELS.includes(effortLevel)) {
968
- entries.push({
969
- level: "warn",
970
- text: `CLAUDE_CODE_EFFORT_LEVEL invalid (${effortLevel})`,
971
- details: [
972
- `expected one of: ${ENV_EFFORT_LEVELS.join(", ")}`,
973
- "invalid effort env values can make sessions harder to reason about; remove or correct it",
974
- ],
975
- });
976
- } else if (effortLevel === "low" || effortLevel === "medium") {
977
- entries.push({
978
- level: "warn",
979
- text: `CLAUDE_CODE_EFFORT_LEVEL ${effortLevel}`,
980
- details: [
981
- "this takes precedence over /effort and settings effortLevel",
982
- "CurDX-Flow planning, verification, and review-heavy turns usually work better at high or xhigh",
983
- ],
984
- });
985
- } else if (effortLevel) {
986
- entries.push({
987
- level: "info",
988
- text: `CLAUDE_CODE_EFFORT_LEVEL ${effortLevel}`,
989
- details: [
990
- "session effort is pinned through the environment for this process",
991
- ],
992
- });
993
- }
994
-
995
- if (envFlagEnabled(env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)) {
996
- entries.push({
997
- level: "info",
998
- text: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS enabled",
999
- details: [
1000
- "official docs: this enables experimental team surfaces such as SendMessage / TeamCreate / TeamDelete",
1001
- "CurDX-Flow does not depend on these runtime-gated tools, but this explains why teammate features may appear in this session",
1002
- ],
1003
- });
1004
- }
1005
-
1006
- const syncPluginInstall = envFlagEnabled(env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL);
1007
- const syncPluginInstallTimeout = positiveIntegerFromEnv(
1008
- env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS
1009
- );
1010
- const pluginSeedDir = normalizedEnvValue(env.CLAUDE_CODE_PLUGIN_SEED_DIR);
1011
-
1012
- if (normalizedEnvValue(env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS)) {
1013
- if (Number.isNaN(syncPluginInstallTimeout)) {
1014
- entries.push({
1015
- level: "warn",
1016
- text: "CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS invalid",
1017
- details: [
1018
- "expected a positive integer timeout in milliseconds",
1019
- "invalid timeout values can make headless plugin-install behavior harder to reason about",
1020
- ],
1021
- });
1022
- } else if (!syncPluginInstall) {
1023
- entries.push({
1024
- level: "warn",
1025
- text: "CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS set without CLAUDE_CODE_SYNC_PLUGIN_INSTALL",
1026
- details: [
1027
- "official docs: the timeout only applies when synchronous plugin installation is enabled",
1028
- "set CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 or remove the timeout override",
1029
- ],
1030
- });
1031
- }
1032
- }
1033
-
1034
- if (pluginSeedDir) {
1035
- entries.push({
1036
- level: "info",
1037
- text: "CLAUDE_CODE_PLUGIN_SEED_DIR configured",
1038
- details: [
1039
- `seed dir: ${pluginSeedDir}`,
1040
- "official docs: pre-populated plugin seeds let containers and CI start with marketplaces/plugins already available",
1041
- ],
1042
- });
1043
- }
1044
-
1045
- if (inCi && !syncPluginInstall && !pluginSeedDir) {
1046
- entries.push({
1047
- level: "warn",
1048
- text: "CI environment without synchronous or seeded plugin availability",
1049
- details: [
1050
- "prefer claude --bare -p for CI so runs do not inherit local hooks, skills, plugins, MCP discovery, or CLAUDE.md unexpectedly",
1051
- "official docs: in non-interactive/headless mode, marketplace plugins may install in the background and miss the first turn",
1052
- "set CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 for headless runs that depend on plugin availability on turn one",
1053
- "or pre-populate plugins with CLAUDE_CODE_PLUGIN_SEED_DIR in containers/CI images",
1054
- "if the run needs project assets in bare mode, pass them explicitly with --plugin-dir, --settings, or --mcp-config",
1055
- ],
1056
- });
1057
- }
1058
-
1059
- for (const family of PINNED_MODEL_ENV_FAMILIES) {
1060
- const modelId = normalizedEnvValue(env[family.modelVar]);
1061
- if (!modelId) continue;
1062
-
1063
- const caps = normalizedEnvValue(env[family.capsVar]);
1064
- if (!looksProviderSpecificModelId(modelId) || caps) continue;
1065
-
1066
- entries.push({
1067
- level: "warn",
1068
- text: `${family.modelVar} uses a provider-specific/custom model id`,
1069
- details: [
1070
- `${family.label} pinned to: ${modelId}`,
1071
- `${family.capsVar} is unset`,
1072
- "official docs: custom/provider model IDs can disable Claude Code feature detection for effort and thinking",
1073
- `declare ${family.capsVar} when pinning custom Bedrock / Vertex / Foundry / gateway model IDs`,
1074
- ],
1075
- });
1076
- }
1077
-
1078
- return { entries };
1079
- }
1080
-
1081
172
  export async function collectDoctorData(
1082
173
  {
1083
174
  cwd = process.cwd(),