@curdx/flow 2.2.0 → 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.
Files changed (83) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +19 -2
  3. package/README.md +15 -8
  4. package/README.zh.md +5 -3
  5. package/agent-preamble/preamble.md +33 -0
  6. package/agents/flow-adversary.md +1 -1
  7. package/agents/flow-architect.md +2 -1
  8. package/agents/flow-brownfield-analyst.md +153 -0
  9. package/agents/flow-debugger.md +6 -11
  10. package/agents/flow-edge-hunter.md +1 -1
  11. package/agents/flow-executor.md +30 -8
  12. package/agents/flow-planner.md +38 -5
  13. package/agents/flow-product-designer.md +2 -1
  14. package/agents/flow-qa-engineer.md +9 -5
  15. package/agents/flow-researcher.md +2 -1
  16. package/agents/flow-reviewer.md +23 -5
  17. package/agents/flow-security-auditor.md +5 -3
  18. package/agents/flow-triage-analyst.md +5 -24
  19. package/agents/flow-ui-researcher.md +4 -3
  20. package/agents/flow-ux-designer.md +12 -39
  21. package/agents/flow-verifier.md +35 -3
  22. package/cli/README.md +3 -1
  23. package/cli/doctor-workflow.js +165 -2
  24. package/cli/doctor.js +8 -0
  25. package/cli/help.js +2 -0
  26. package/cli/lib/doctor-claude-settings.js +736 -0
  27. package/cli/lib/doctor-report.js +256 -1
  28. package/cli/lib/doctor-runtime-environment.js +196 -0
  29. package/cli/lib/frontmatter.js +44 -0
  30. package/cli/lib/json-schema.js +57 -0
  31. package/cli/lib/runtime.js +20 -2
  32. package/cli/lib/semver.js +14 -0
  33. package/cli/uninstall-actions.js +323 -0
  34. package/cli/uninstall.js +9 -253
  35. package/cli/utils.js +6 -1
  36. package/gates/adversarial-review-gate.md +1 -1
  37. package/gates/security-gate.md +2 -2
  38. package/gates/test-quality-gate.md +59 -0
  39. package/hooks/hooks.json +16 -2
  40. package/hooks/scripts/common.sh +4 -0
  41. package/hooks/scripts/session-start.sh +17 -2
  42. package/hooks/scripts/stop-watcher.sh +69 -18
  43. package/hooks/scripts/subagent-artifact-guard.sh +159 -0
  44. package/hooks/scripts/subagent-statusline.sh +105 -0
  45. package/knowledge/atomic-commits.md +1 -1
  46. package/knowledge/claude-code-runtime-contracts.md +203 -0
  47. package/knowledge/epic-decomposition.md +1 -1
  48. package/knowledge/execution-strategies.md +23 -1
  49. package/knowledge/planning-reviews.md +2 -2
  50. package/knowledge/poc-first-workflow.md +8 -8
  51. package/knowledge/review-feedback-intake.md +57 -0
  52. package/knowledge/two-stage-review.md +19 -6
  53. package/knowledge/wave-execution.md +16 -1
  54. package/output-styles/curdx-evidence-first.md +34 -0
  55. package/package.json +7 -1
  56. package/schemas/agent-frontmatter.schema.json +0 -7
  57. package/schemas/config.schema.json +14 -0
  58. package/schemas/hooks.schema.json +34 -2
  59. package/schemas/output-style-frontmatter.schema.json +22 -0
  60. package/schemas/plugin-manifest.schema.json +387 -17
  61. package/schemas/plugin-settings.schema.json +29 -0
  62. package/schemas/skill-frontmatter.schema.json +109 -4
  63. package/schemas/spec-state.schema.json +29 -4
  64. package/settings.json +6 -0
  65. package/skills/brownfield-index/SKILL.md +31 -35
  66. package/skills/browser-qa/SKILL.md +11 -3
  67. package/skills/cancel/SKILL.md +82 -0
  68. package/skills/debug/SKILL.md +6 -2
  69. package/skills/epic/SKILL.md +5 -3
  70. package/skills/fast/SKILL.md +1 -0
  71. package/skills/help/SKILL.md +17 -7
  72. package/skills/implement/SKILL.md +38 -7
  73. package/skills/init/SKILL.md +2 -1
  74. package/skills/review/SKILL.md +4 -1
  75. package/skills/security-audit/SKILL.md +17 -3
  76. package/skills/spec/SKILL.md +2 -1
  77. package/skills/start/SKILL.md +18 -18
  78. package/skills/status/SKILL.md +85 -0
  79. package/skills/ui-sketch/SKILL.md +11 -3
  80. package/skills/verify/SKILL.md +13 -1
  81. package/templates/config.json.tmpl +4 -1
  82. package/templates/progress.md.tmpl +19 -0
  83. package/templates/tasks.md.tmpl +26 -3
@@ -0,0 +1,736 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const PROJECT_ONLY_IGNORED_SETTINGS = [
5
+ {
6
+ key: "autoMemoryDirectory",
7
+ kind: "ignored-project-setting",
8
+ message: "autoMemoryDirectory is not accepted in project settings; move it to user or local settings",
9
+ },
10
+ {
11
+ key: "autoMode",
12
+ kind: "ignored-project-setting",
13
+ message: "autoMode is not read from shared project settings; move it to user or local settings",
14
+ },
15
+ {
16
+ key: "useAutoModeDuringPlan",
17
+ kind: "ignored-project-setting",
18
+ message: "useAutoModeDuringPlan is not read from shared project settings; move it to user or local settings",
19
+ },
20
+ {
21
+ key: "sshConfigs",
22
+ kind: "ignored-project-setting",
23
+ message: "sshConfigs is only read from user or managed settings; remove it from shared project settings",
24
+ },
25
+ {
26
+ key: "teammateMode",
27
+ kind: "invalid-project-setting",
28
+ message: "teammateMode belongs in the global ~/.claude.json config, not project settings.json",
29
+ },
30
+ ];
31
+
32
+ const MANAGED_ONLY_SETTINGS = [
33
+ "allowedChannelPlugins",
34
+ "allowedMcpServers",
35
+ "allowManagedHooksOnly",
36
+ "allowManagedMcpServersOnly",
37
+ "allowManagedPermissionRulesOnly",
38
+ "blockedMarketplaces",
39
+ "channelsEnabled",
40
+ "deniedMcpServers",
41
+ "forceRemoteSettingsRefresh",
42
+ "pluginTrustMessage",
43
+ "strictKnownMarketplaces",
44
+ ];
45
+
46
+ const SHARED_SCRIPT_SETTINGS = [
47
+ {
48
+ key: "apiKeyHelper",
49
+ kind: "shared-script-setting",
50
+ message: "apiKeyHelper runs a local script from shared project settings",
51
+ },
52
+ {
53
+ key: "awsAuthRefresh",
54
+ kind: "shared-script-setting",
55
+ message: "awsAuthRefresh runs a local credential-refresh script from shared project settings",
56
+ },
57
+ {
58
+ key: "awsCredentialExport",
59
+ kind: "shared-script-setting",
60
+ message: "awsCredentialExport runs a local credential-export script from shared project settings",
61
+ },
62
+ {
63
+ key: "otelHeadersHelper",
64
+ kind: "shared-script-setting",
65
+ message: "otelHeadersHelper runs a local telemetry script from shared project settings",
66
+ },
67
+ ];
68
+
69
+ const CURDX_FLOW_AGENT_TYPES = [
70
+ "flow-adversary",
71
+ "flow-architect",
72
+ "flow-brownfield-analyst",
73
+ "flow-debugger",
74
+ "flow-edge-hunter",
75
+ "flow-executor",
76
+ "flow-planner",
77
+ "flow-product-designer",
78
+ "flow-qa-engineer",
79
+ "flow-researcher",
80
+ "flow-reviewer",
81
+ "flow-security-auditor",
82
+ "flow-triage-analyst",
83
+ "flow-ui-researcher",
84
+ "flow-ux-designer",
85
+ "flow-verifier",
86
+ ];
87
+
88
+ const CURDX_FLOW_REQUIRED_MODEL_ALIASES = ["sonnet", "opus"];
89
+ const CURDX_FLOW_REQUIRED_TOOLS = [
90
+ "Agent",
91
+ "AskUserQuestion",
92
+ "Bash",
93
+ "Monitor",
94
+ "Read",
95
+ "Write",
96
+ "Edit",
97
+ "Grep",
98
+ "Glob",
99
+ ];
100
+ const CURDX_FLOW_PLUGIN_ID = "curdx-flow@curdx-flow-marketplace";
101
+ const CURDX_FLOW_REQUIRED_PLUGIN_IDS = ["context7-plugin@context7-marketplace"];
102
+ const HTTP_HOOK_SETTINGS = ["allowedHttpHookUrls", "httpHookAllowedEnvVars"];
103
+ const PERSISTED_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
104
+ const ENV_EFFORT_LEVELS = [...PERSISTED_EFFORT_LEVELS, "max", "auto"];
105
+
106
+ function envFlagEnabled(value) {
107
+ if (value === true || value === 1) return true;
108
+ if (typeof value !== "string") return false;
109
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
110
+ }
111
+
112
+ function normalizedEnvValue(value) {
113
+ if (typeof value !== "string") return null;
114
+ const normalized = value.trim();
115
+ return normalized.length > 0 ? normalized : null;
116
+ }
117
+
118
+ function parsePermissionRule(rule) {
119
+ if (typeof rule !== "string") return null;
120
+ const match = rule.match(/^([A-Za-z][A-Za-z0-9]*)(?:\((.*)\))?$/);
121
+ if (!match) return null;
122
+ return {
123
+ tool: match[1],
124
+ matcher: match[2] ?? null,
125
+ };
126
+ }
127
+
128
+ function isBroadToolDeny(rule) {
129
+ const parsed = parsePermissionRule(rule);
130
+ if (!parsed || !CURDX_FLOW_REQUIRED_TOOLS.includes(parsed.tool)) return false;
131
+ return parsed.matcher === null || parsed.matcher === "*" || parsed.matcher === "**";
132
+ }
133
+
134
+ function isCurdxFlowAgentDeny(rule) {
135
+ const parsed = parsePermissionRule(rule);
136
+ if (!parsed || parsed.tool !== "Agent") return false;
137
+ if (parsed.matcher === null || parsed.matcher === "*" || parsed.matcher === "**") return true;
138
+ return CURDX_FLOW_AGENT_TYPES.some(
139
+ (agentType) => parsed.matcher === agentType || parsed.matcher === `curdx-flow:${agentType}`
140
+ );
141
+ }
142
+
143
+ function disabledPluginIdsFromSettings(enabledPlugins) {
144
+ if (!enabledPlugins || typeof enabledPlugins !== "object" || Array.isArray(enabledPlugins)) {
145
+ return [];
146
+ }
147
+
148
+ return Object.entries(enabledPlugins)
149
+ .filter(([, enabled]) => enabled === false)
150
+ .map(([pluginId]) => pluginId);
151
+ }
152
+
153
+ function isNonArrayObject(value) {
154
+ return value && typeof value === "object" && !Array.isArray(value);
155
+ }
156
+
157
+ function hasProjectBlockingSandboxPath(values) {
158
+ if (!Array.isArray(values)) return false;
159
+
160
+ return values.some((value) => {
161
+ if (typeof value !== "string") return false;
162
+ const token = value.trim().replace(/\/\*{1,2}$/, "").replace(/\/$/, "");
163
+ return [".", ".flow", "./.flow", ".git", "./.git"].includes(token);
164
+ });
165
+ }
166
+
167
+ function pushScopedWarning(target, kind, message, scope = "project") {
168
+ const warning = { kind, message };
169
+ if (scope !== "project") warning.scope = scope;
170
+ target.push(warning);
171
+ }
172
+
173
+ function auditLocalClaudeSettings(parsed, warnings) {
174
+ const scope = "local";
175
+ const permissions = parsed?.permissions && typeof parsed.permissions === "object"
176
+ ? parsed.permissions
177
+ : {};
178
+ const deny = Array.isArray(permissions.deny) ? permissions.deny : [];
179
+
180
+ if ("enabledPlugins" in parsed && (
181
+ !parsed.enabledPlugins ||
182
+ typeof parsed.enabledPlugins !== "object" ||
183
+ Array.isArray(parsed.enabledPlugins)
184
+ )) {
185
+ pushScopedWarning(
186
+ warnings,
187
+ "invalid-local-setting",
188
+ "enabledPlugins in settings.local.json must be an object keyed by plugin@marketplace id",
189
+ scope
190
+ );
191
+ }
192
+
193
+ const disabledPluginIds = disabledPluginIdsFromSettings(parsed.enabledPlugins);
194
+ if (disabledPluginIds.includes(CURDX_FLOW_PLUGIN_ID)) {
195
+ pushScopedWarning(
196
+ warnings,
197
+ "flow-runtime-blocker",
198
+ `settings.local.json disables CurDX-Flow for this machine: ${CURDX_FLOW_PLUGIN_ID}`,
199
+ scope
200
+ );
201
+ }
202
+
203
+ for (const pluginId of CURDX_FLOW_REQUIRED_PLUGIN_IDS) {
204
+ if (disabledPluginIds.includes(pluginId)) {
205
+ pushScopedWarning(
206
+ warnings,
207
+ "required-plugin-disabled",
208
+ `settings.local.json disables required CurDX-Flow companion plugin: ${pluginId}`,
209
+ scope
210
+ );
211
+ }
212
+ }
213
+
214
+ if (permissions.defaultMode === "bypassPermissions") {
215
+ pushScopedWarning(
216
+ warnings,
217
+ "bypass-default-mode",
218
+ 'permissions.defaultMode in settings.local.json is "bypassPermissions"',
219
+ scope
220
+ );
221
+ }
222
+
223
+ if (permissions.defaultMode === "dontAsk") {
224
+ pushScopedWarning(
225
+ warnings,
226
+ "flow-runtime-blocker",
227
+ 'permissions.defaultMode in settings.local.json is "dontAsk"; CurDX-Flow clarification and Agent dispatch prompts may be auto-denied',
228
+ scope
229
+ );
230
+ }
231
+
232
+ for (const rule of deny) {
233
+ if (isCurdxFlowAgentDeny(rule)) {
234
+ pushScopedWarning(
235
+ warnings,
236
+ "flow-runtime-blocker",
237
+ `settings.local.json deny rule blocks CurDX-Flow subagent dispatch: ${rule}`,
238
+ scope
239
+ );
240
+ continue;
241
+ }
242
+
243
+ if (isBroadToolDeny(rule)) {
244
+ pushScopedWarning(
245
+ warnings,
246
+ "flow-runtime-blocker",
247
+ `settings.local.json deny rule blocks a tool CurDX-Flow workflows require: ${rule}`,
248
+ scope
249
+ );
250
+ }
251
+ }
252
+
253
+ if (parsed.env && typeof parsed.env === "object" && !Array.isArray(parsed.env)) {
254
+ if (envFlagEnabled(parsed.env.CLAUDE_CODE_SIMPLE)) {
255
+ pushScopedWarning(
256
+ warnings,
257
+ "flow-runtime-blocker",
258
+ "settings.local.json env.CLAUDE_CODE_SIMPLE=1 enables bare/simple mode, disabling CurDX-Flow plugin, hook, skill, MCP, and CLAUDE.md discovery",
259
+ scope
260
+ );
261
+ }
262
+
263
+ if (envFlagEnabled(parsed.env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT)) {
264
+ pushScopedWarning(
265
+ warnings,
266
+ "local-runtime-setting",
267
+ "settings.local.json env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=1 forces the minimal Claude system prompt for local sessions",
268
+ scope
269
+ );
270
+ }
271
+ }
272
+
273
+ if (parsed.disableAllHooks === true) {
274
+ pushScopedWarning(
275
+ warnings,
276
+ "flow-runtime-blocker",
277
+ "settings.local.json disableAllHooks disables CurDX-Flow stop/recovery hooks and plugin subagent status lines",
278
+ scope
279
+ );
280
+ }
281
+
282
+ if (parsed.disableSkillShellExecution === true) {
283
+ pushScopedWarning(
284
+ warnings,
285
+ "skill-shell-disabled",
286
+ "settings.local.json disableSkillShellExecution disables inline shell expansion in project/plugin skills and commands",
287
+ scope
288
+ );
289
+ }
290
+
291
+ if (Array.isArray(parsed.availableModels)) {
292
+ const missing = CURDX_FLOW_REQUIRED_MODEL_ALIASES.filter(
293
+ (modelAlias) => !parsed.availableModels.includes(modelAlias)
294
+ );
295
+ if (missing.length > 0) {
296
+ pushScopedWarning(
297
+ warnings,
298
+ "flow-runtime-blocker",
299
+ `settings.local.json availableModels excludes CurDX-Flow agent model aliases: ${missing.join(", ")}`,
300
+ scope
301
+ );
302
+ }
303
+ }
304
+
305
+ if ("agent" in parsed) {
306
+ if (typeof parsed.agent !== "string" || parsed.agent.trim().length === 0) {
307
+ pushScopedWarning(
308
+ warnings,
309
+ "invalid-local-setting",
310
+ "agent in settings.local.json must be a non-empty subagent name when set",
311
+ scope
312
+ );
313
+ } else {
314
+ pushScopedWarning(
315
+ warnings,
316
+ "flow-runtime-blocker",
317
+ `settings.local.json routes the main thread through subagent "${parsed.agent}", overriding CurDX-Flow prompt, tools, and model`,
318
+ scope
319
+ );
320
+ }
321
+ }
322
+
323
+ if ("sandbox" in parsed) {
324
+ if (!isNonArrayObject(parsed.sandbox)) {
325
+ pushScopedWarning(
326
+ warnings,
327
+ "invalid-local-setting",
328
+ "sandbox in settings.local.json must be an object when set",
329
+ scope
330
+ );
331
+ } else {
332
+ if (parsed.sandbox.failIfUnavailable === true) {
333
+ pushScopedWarning(
334
+ warnings,
335
+ "flow-runtime-blocker",
336
+ "settings.local.json sandbox.failIfUnavailable can prevent Claude Code startup on hosts where sandboxing is unavailable",
337
+ scope
338
+ );
339
+ }
340
+
341
+ const filesystem = isNonArrayObject(parsed.sandbox.filesystem)
342
+ ? parsed.sandbox.filesystem
343
+ : {};
344
+ if (hasProjectBlockingSandboxPath(filesystem.denyRead)) {
345
+ pushScopedWarning(
346
+ warnings,
347
+ "flow-runtime-blocker",
348
+ "settings.local.json sandbox.filesystem.denyRead blocks .flow/.git or the project root, which CurDX-Flow must inspect",
349
+ scope
350
+ );
351
+ }
352
+ if (hasProjectBlockingSandboxPath(filesystem.denyWrite)) {
353
+ pushScopedWarning(
354
+ warnings,
355
+ "flow-runtime-blocker",
356
+ "settings.local.json sandbox.filesystem.denyWrite blocks .flow/.git or the project root, which CurDX-Flow must update",
357
+ scope
358
+ );
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ export async function readProjectClaudeSettings(cwd = process.cwd()) {
365
+ const settingsPath = path.join(cwd, ".claude", "settings.json");
366
+ const localSettingsPath = path.join(cwd, ".claude", "settings.local.json");
367
+ const state = {
368
+ exists: false,
369
+ localExists: false,
370
+ invalid: false,
371
+ parseError: null,
372
+ warnings: [],
373
+ localInvalid: false,
374
+ localParseError: null,
375
+ localWarnings: [],
376
+ };
377
+
378
+ try {
379
+ const localStat = await fs.stat(localSettingsPath);
380
+ state.localExists = localStat.isFile();
381
+ } catch {
382
+ state.localExists = false;
383
+ }
384
+
385
+ let parsed;
386
+ try {
387
+ const stat = await fs.stat(settingsPath);
388
+ if (!stat.isFile()) {
389
+ parsed = null;
390
+ } else {
391
+ state.exists = true;
392
+ parsed = JSON.parse(await fs.readFile(settingsPath, "utf-8"));
393
+ }
394
+ } catch (error) {
395
+ if (error?.code !== "ENOENT") {
396
+ state.invalid = true;
397
+ state.parseError = error.message;
398
+ return state;
399
+ }
400
+ }
401
+
402
+ if (state.exists) {
403
+ const permissions = parsed?.permissions && typeof parsed.permissions === "object"
404
+ ? parsed.permissions
405
+ : {};
406
+ const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
407
+ const deny = Array.isArray(permissions.deny) ? permissions.deny : [];
408
+
409
+ if ("enabledPlugins" in parsed && (
410
+ !parsed.enabledPlugins ||
411
+ typeof parsed.enabledPlugins !== "object" ||
412
+ Array.isArray(parsed.enabledPlugins)
413
+ )) {
414
+ pushScopedWarning(
415
+ state.warnings,
416
+ "invalid-project-setting",
417
+ "enabledPlugins must be an object keyed by plugin@marketplace id"
418
+ );
419
+ }
420
+
421
+ const disabledPluginIds = disabledPluginIdsFromSettings(parsed.enabledPlugins);
422
+ if (disabledPluginIds.includes(CURDX_FLOW_PLUGIN_ID)) {
423
+ state.warnings.push({
424
+ kind: "flow-runtime-blocker",
425
+ message: `enabledPlugins disables CurDX-Flow in this project: ${CURDX_FLOW_PLUGIN_ID}`,
426
+ });
427
+ }
428
+
429
+ for (const pluginId of CURDX_FLOW_REQUIRED_PLUGIN_IDS) {
430
+ if (disabledPluginIds.includes(pluginId)) {
431
+ state.warnings.push({
432
+ kind: "required-plugin-disabled",
433
+ message: `enabledPlugins disables required CurDX-Flow companion plugin: ${pluginId}`,
434
+ });
435
+ }
436
+ }
437
+
438
+ if (permissions.defaultMode === "bypassPermissions") {
439
+ state.warnings.push({
440
+ kind: "bypass-default-mode",
441
+ message: 'permissions.defaultMode is "bypassPermissions"',
442
+ });
443
+ }
444
+
445
+ if (permissions.defaultMode === "dontAsk") {
446
+ state.warnings.push({
447
+ kind: "flow-runtime-blocker",
448
+ message: 'permissions.defaultMode is "dontAsk"; CurDX-Flow clarification and Agent dispatch prompts may be auto-denied',
449
+ });
450
+ }
451
+
452
+ if (permissions.skipDangerousModePermissionPrompt === true) {
453
+ state.warnings.push({
454
+ kind: "ignored-project-setting",
455
+ message: "permissions.skipDangerousModePermissionPrompt is ignored in project settings; move it to user or local settings",
456
+ });
457
+ }
458
+
459
+ for (const rule of allow) {
460
+ if (rule === "Bash" || rule === "Bash(*)" || rule === "*" || rule === "Read(**)") {
461
+ state.warnings.push({
462
+ kind: "broad-allow-rule",
463
+ message: `overbroad allow rule: ${rule}`,
464
+ });
465
+ }
466
+ }
467
+
468
+ for (const rule of deny) {
469
+ if (isCurdxFlowAgentDeny(rule)) {
470
+ state.warnings.push({
471
+ kind: "flow-runtime-blocker",
472
+ message: `deny rule blocks CurDX-Flow subagent dispatch: ${rule}`,
473
+ });
474
+ continue;
475
+ }
476
+
477
+ if (isBroadToolDeny(rule)) {
478
+ state.warnings.push({
479
+ kind: "flow-runtime-blocker",
480
+ message: `deny rule blocks a tool CurDX-Flow workflows require: ${rule}`,
481
+ });
482
+ }
483
+ }
484
+
485
+ const sensitiveDenyPatterns = ["Read(./.env)", "Read(./.env.*)"];
486
+ for (const pattern of sensitiveDenyPatterns) {
487
+ if (!deny.includes(pattern)) {
488
+ state.warnings.push({
489
+ kind: "missing-sensitive-deny",
490
+ message: `missing recommended deny rule: ${pattern}`,
491
+ });
492
+ }
493
+ }
494
+
495
+ for (const setting of PROJECT_ONLY_IGNORED_SETTINGS) {
496
+ if (setting.key in parsed) {
497
+ state.warnings.push({
498
+ kind: setting.kind,
499
+ message: setting.message,
500
+ });
501
+ }
502
+ }
503
+
504
+ for (const key of MANAGED_ONLY_SETTINGS) {
505
+ if (key in parsed) {
506
+ state.warnings.push({
507
+ kind: "managed-only-setting",
508
+ message: `${key} only applies from managed settings; remove it from shared project settings`,
509
+ });
510
+ }
511
+ }
512
+
513
+ for (const setting of SHARED_SCRIPT_SETTINGS) {
514
+ if (setting.key in parsed) {
515
+ state.warnings.push({
516
+ kind: setting.kind,
517
+ message: setting.message,
518
+ });
519
+ }
520
+ }
521
+
522
+ if (parsed.fileSuggestion?.type === "command" && parsed.fileSuggestion.command) {
523
+ state.warnings.push({
524
+ kind: "shared-script-setting",
525
+ message: "fileSuggestion.command runs a local script from shared project settings",
526
+ });
527
+ }
528
+
529
+ if (parsed.statusLine?.type === "command" && parsed.statusLine.command) {
530
+ state.warnings.push({
531
+ kind: "shared-script-setting",
532
+ message: "statusLine.command runs a local script from shared project settings",
533
+ });
534
+ }
535
+
536
+ if (parsed.env && typeof parsed.env === "object" && !Array.isArray(parsed.env)) {
537
+ state.warnings.push({
538
+ kind: "shared-env-setting",
539
+ message: "env injects shared environment variables into every session; avoid machine-specific values or secrets",
540
+ });
541
+
542
+ if (envFlagEnabled(parsed.env.CLAUDE_CODE_SIMPLE)) {
543
+ state.warnings.push({
544
+ kind: "flow-runtime-blocker",
545
+ message: "env.CLAUDE_CODE_SIMPLE=1 enables bare/simple mode, disabling CurDX-Flow plugin, hook, skill, MCP, and CLAUDE.md discovery",
546
+ });
547
+ }
548
+
549
+ if (envFlagEnabled(parsed.env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT)) {
550
+ state.warnings.push({
551
+ kind: "shared-env-setting",
552
+ message: "env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=1 forces the minimal Claude system prompt for every collaborator session",
553
+ });
554
+ }
555
+
556
+ const effortEnv = normalizedEnvValue(parsed.env.CLAUDE_CODE_EFFORT_LEVEL);
557
+ if (effortEnv && !ENV_EFFORT_LEVELS.includes(effortEnv)) {
558
+ state.warnings.push({
559
+ kind: "invalid-project-setting",
560
+ message: `env.CLAUDE_CODE_EFFORT_LEVEL must be one of ${ENV_EFFORT_LEVELS.join(", ")}`,
561
+ });
562
+ } else if (effortEnv === "low" || effortEnv === "medium") {
563
+ state.warnings.push({
564
+ kind: "low-effort-project-setting",
565
+ message: `env.CLAUDE_CODE_EFFORT_LEVEL=${effortEnv} lowers reasoning for every collaborator session`,
566
+ });
567
+ }
568
+
569
+ if ("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" in parsed.env) {
570
+ state.warnings.push({
571
+ kind: "shared-env-setting",
572
+ message: "env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS shares an experimental Claude runtime flag across this project",
573
+ });
574
+ }
575
+ }
576
+
577
+ if (parsed.disableAllHooks === true) {
578
+ state.warnings.push({
579
+ kind: "flow-runtime-blocker",
580
+ message: "disableAllHooks disables CurDX-Flow stop/recovery hooks and plugin subagent status lines",
581
+ });
582
+ }
583
+
584
+ if (parsed.disableSkillShellExecution === true) {
585
+ state.warnings.push({
586
+ kind: "skill-shell-disabled",
587
+ message: "disableSkillShellExecution disables inline shell expansion in project/plugin skills and commands",
588
+ });
589
+ }
590
+
591
+ for (const key of HTTP_HOOK_SETTINGS) {
592
+ if (!(key in parsed)) continue;
593
+ if (!Array.isArray(parsed[key])) {
594
+ state.warnings.push({
595
+ kind: "invalid-project-setting",
596
+ message: `${key} must be an array when set in settings.json`,
597
+ });
598
+ continue;
599
+ }
600
+ if (parsed[key].length === 0) {
601
+ state.warnings.push({
602
+ kind: "shared-hook-policy",
603
+ message: `${key} is an empty allowlist; matching HTTP hook behavior is blocked`,
604
+ });
605
+ }
606
+ }
607
+
608
+ if (Array.isArray(parsed.availableModels)) {
609
+ const missing = CURDX_FLOW_REQUIRED_MODEL_ALIASES.filter(
610
+ (modelAlias) => !parsed.availableModels.includes(modelAlias)
611
+ );
612
+ if (missing.length > 0) {
613
+ state.warnings.push({
614
+ kind: "flow-runtime-blocker",
615
+ message: `availableModels excludes CurDX-Flow agent model aliases: ${missing.join(", ")}`,
616
+ });
617
+ }
618
+ }
619
+
620
+ if ("agent" in parsed) {
621
+ if (typeof parsed.agent !== "string" || parsed.agent.trim().length === 0) {
622
+ state.warnings.push({
623
+ kind: "invalid-project-setting",
624
+ message: "agent must be a non-empty subagent name when set in settings.json",
625
+ });
626
+ } else {
627
+ state.warnings.push({
628
+ kind: "flow-runtime-blocker",
629
+ message: `agent routes the main thread through subagent "${parsed.agent}", overriding CurDX-Flow prompt, tools, and model`,
630
+ });
631
+ }
632
+ }
633
+
634
+ if ("effortLevel" in parsed) {
635
+ if (parsed.effortLevel === "max") {
636
+ state.warnings.push({
637
+ kind: "invalid-project-setting",
638
+ message:
639
+ "effortLevel in settings.json only supports low, medium, high, or xhigh; use /effort max or CLAUDE_CODE_EFFORT_LEVEL for session-only max",
640
+ });
641
+ } else if (!PERSISTED_EFFORT_LEVELS.includes(parsed.effortLevel)) {
642
+ state.warnings.push({
643
+ kind: "invalid-project-setting",
644
+ message:
645
+ "effortLevel in settings.json must be one of low, medium, high, or xhigh",
646
+ });
647
+ } else if (parsed.effortLevel === "low" || parsed.effortLevel === "medium") {
648
+ state.warnings.push({
649
+ kind: "low-effort-project-setting",
650
+ message: `effortLevel ${parsed.effortLevel} is shared at project scope; CurDX-Flow planning/review turns may need higher reasoning`,
651
+ });
652
+ }
653
+ }
654
+
655
+ if ("sandbox" in parsed) {
656
+ if (!isNonArrayObject(parsed.sandbox)) {
657
+ state.warnings.push({
658
+ kind: "invalid-project-setting",
659
+ message: "sandbox must be an object when set in settings.json",
660
+ });
661
+ } else {
662
+ if (parsed.sandbox.failIfUnavailable === true) {
663
+ state.warnings.push({
664
+ kind: "flow-runtime-blocker",
665
+ message: "sandbox.failIfUnavailable can prevent Claude Code startup on hosts where sandboxing is unavailable",
666
+ });
667
+ }
668
+ if (parsed.sandbox.allowUnsandboxedCommands === false) {
669
+ state.warnings.push({
670
+ kind: "shared-sandbox-policy",
671
+ message: "sandbox.allowUnsandboxedCommands=false disables the unsandboxed Bash escape hatch for this project",
672
+ });
673
+ }
674
+
675
+ const filesystem = isNonArrayObject(parsed.sandbox.filesystem)
676
+ ? parsed.sandbox.filesystem
677
+ : {};
678
+ if (hasProjectBlockingSandboxPath(filesystem.denyRead)) {
679
+ state.warnings.push({
680
+ kind: "flow-runtime-blocker",
681
+ message: "sandbox.filesystem.denyRead blocks .flow/.git or the project root, which CurDX-Flow must inspect",
682
+ });
683
+ }
684
+ if (hasProjectBlockingSandboxPath(filesystem.denyWrite)) {
685
+ state.warnings.push({
686
+ kind: "flow-runtime-blocker",
687
+ message: "sandbox.filesystem.denyWrite blocks .flow/.git or the project root, which CurDX-Flow must update",
688
+ });
689
+ }
690
+
691
+ const network = isNonArrayObject(parsed.sandbox.network)
692
+ ? parsed.sandbox.network
693
+ : {};
694
+ if (Array.isArray(network.allowedDomains) && network.allowedDomains.length === 0) {
695
+ state.warnings.push({
696
+ kind: "shared-sandbox-policy",
697
+ message: "sandbox.network.allowedDomains is empty; sandboxed commands have no outbound network access",
698
+ });
699
+ }
700
+ }
701
+ }
702
+
703
+ if (parsed.enableAllProjectMcpServers === true) {
704
+ state.warnings.push({
705
+ kind: "shared-mcp-auto-approve",
706
+ message: "enableAllProjectMcpServers auto-approves every project MCP server",
707
+ });
708
+ }
709
+
710
+ if (Array.isArray(parsed.enabledMcpjsonServers) && parsed.enabledMcpjsonServers.length > 0) {
711
+ state.warnings.push({
712
+ kind: "shared-mcp-auto-approve",
713
+ message: `enabledMcpjsonServers auto-approves project MCP servers: ${parsed.enabledMcpjsonServers.join(", ")}`,
714
+ });
715
+ }
716
+
717
+ if ("includeCoAuthoredBy" in parsed) {
718
+ state.warnings.push({
719
+ kind: "deprecated-setting",
720
+ message: "includeCoAuthoredBy is deprecated; migrate to attribution",
721
+ });
722
+ }
723
+ }
724
+
725
+ if (state.localExists) {
726
+ try {
727
+ const localParsed = JSON.parse(await fs.readFile(localSettingsPath, "utf-8"));
728
+ auditLocalClaudeSettings(localParsed, state.localWarnings);
729
+ } catch (error) {
730
+ state.localInvalid = true;
731
+ state.localParseError = error?.message || String(error);
732
+ }
733
+ }
734
+
735
+ return state;
736
+ }