@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
@@ -12,6 +12,110 @@ function pluginErrorDetails(plugin) {
12
12
  return Array.isArray(plugin?.errors) ? plugin.errors : [];
13
13
  }
14
14
 
15
+ function projectSettingsWarningDetails(warning) {
16
+ if (warning?.scope === "local") {
17
+ if (warning.kind === "invalid-local-setting") {
18
+ return [
19
+ "settings.local.json is the highest-precedence repo-scoped settings surface on this machine",
20
+ "fix the JSON shape or remove the local override if Claude behaves unexpectedly",
21
+ ];
22
+ }
23
+
24
+ if (warning.kind === "required-plugin-disabled") {
25
+ return [
26
+ "settings.local.json overrides project and user plugin preferences on this machine",
27
+ "remove the false entry or enable the required companion plugin locally",
28
+ ];
29
+ }
30
+
31
+ if (warning.kind === "flow-runtime-blocker") {
32
+ return [
33
+ "settings.local.json has higher precedence than .claude/settings.json and can break CurDX-Flow only on this machine",
34
+ "remove the local blocker or add explicit exceptions for curdx-flow workflows",
35
+ ];
36
+ }
37
+
38
+ return [
39
+ "settings.local.json overrides shared project settings on this machine",
40
+ "fix or remove the local override if Claude behaves differently from the rest of the team",
41
+ ];
42
+ }
43
+
44
+ if (!warning?.kind) {
45
+ return [
46
+ "project settings are shared with collaborators",
47
+ "prefer deny rules for .env/secrets and avoid bypassPermissions defaults",
48
+ ];
49
+ }
50
+
51
+ if (warning.kind === "ignored-project-setting" || warning.kind === "managed-only-setting") {
52
+ return [
53
+ "Claude Code will ignore this at project scope or only honor it from managed settings",
54
+ "move it to user settings, settings.local.json, or managed settings as appropriate",
55
+ ];
56
+ }
57
+
58
+ if (warning.kind === "invalid-project-setting") {
59
+ return [
60
+ "Claude Code expects this setting to follow the official settings.json shape",
61
+ "fix the value shape or remove the key from shared project settings",
62
+ ];
63
+ }
64
+
65
+ if (warning.kind === "required-plugin-disabled") {
66
+ return [
67
+ "project enabledPlugins has higher precedence than user plugin preferences",
68
+ "remove the false entry or enable the required companion plugin for this project",
69
+ ];
70
+ }
71
+
72
+ if (
73
+ warning.kind === "shared-script-setting" ||
74
+ warning.kind === "shared-env-setting" ||
75
+ warning.kind === "shared-mcp-auto-approve" ||
76
+ warning.kind === "shared-hook-policy" ||
77
+ warning.kind === "shared-sandbox-policy"
78
+ ) {
79
+ return [
80
+ "project settings are shared with collaborators",
81
+ "avoid shared settings that run scripts, inject env vars, narrow hooks, or change sandbox behavior for every collaborator",
82
+ ];
83
+ }
84
+
85
+ if (warning.kind === "skill-shell-disabled") {
86
+ return [
87
+ "Claude Code replaces inline skill shell output with a disabled placeholder when this policy is active",
88
+ "keep it only if your team intentionally bans dynamic shell-backed skill content",
89
+ ];
90
+ }
91
+
92
+ if (warning.kind === "low-effort-project-setting") {
93
+ return [
94
+ "project effortLevel applies to main-thread planning and review turns",
95
+ "prefer high/xhigh for CurDX-Flow planning and verification-heavy workflows",
96
+ ];
97
+ }
98
+
99
+ if (warning.kind === "flow-runtime-blocker") {
100
+ return [
101
+ "CurDX-Flow relies on Claude Code hooks, Agent dispatch, AskUserQuestion, Monitor plus Bash/Read/Edit tooling, and sonnet/opus model aliases",
102
+ "move restrictive policy to a narrower scope or add explicit exceptions for curdx-flow workflows",
103
+ ];
104
+ }
105
+
106
+ if (warning.kind === "deprecated-setting") {
107
+ return [
108
+ "deprecated settings are still accepted for compatibility but should be migrated",
109
+ "prefer the current official replacement before the old key is removed",
110
+ ];
111
+ }
112
+
113
+ return [
114
+ "project settings are shared with collaborators",
115
+ "prefer deny rules for .env/secrets and avoid bypassPermissions defaults",
116
+ ];
117
+ }
118
+
15
119
  export function buildDoctorReport({
16
120
  claudeVersionValue,
17
121
  nodeVersion,
@@ -20,8 +124,12 @@ export function buildDoctorReport({
20
124
  mcps = [],
21
125
  userMcpConfig,
22
126
  runtimeStatus,
127
+ runtimeEnvironment,
23
128
  cwd,
24
129
  projectState,
130
+ projectMcpConfig,
131
+ projectTeamConfig,
132
+ projectClaudeSettings,
25
133
  }) {
26
134
  const lines = [];
27
135
  const sections = [];
@@ -191,6 +299,16 @@ export function buildDoctorReport({
191
299
  for (const [name, status] of Object.entries(runtimeStatus)) {
192
300
  if (status.status === "ok") {
193
301
  pushSectionLine(runtimeSection, "ok", `${name.padEnd(22)} visible on PATH`);
302
+ } else if (status.status === "linkable") {
303
+ pushSectionLine(
304
+ runtimeSection,
305
+ "warn",
306
+ `${name.padEnd(22)} installed but not on PATH`,
307
+ [
308
+ `detected at ${status.path}`,
309
+ "run: npx @curdx/flow doctor --fix",
310
+ ]
311
+ );
194
312
  } else if (status.status === "linked") {
195
313
  pushSectionLine(runtimeSection, "ok", `${name.padEnd(22)} auto-linked ${status.link} → ${status.path}`);
196
314
  } else if (status.status === "missing") {
@@ -206,12 +324,27 @@ export function buildDoctorReport({
206
324
  runtimeSection,
207
325
  "err",
208
326
  `${name.padEnd(22)} installed but not on PATH`,
209
- [`add export PATH="${dir}:$PATH" to your shell rc`]
327
+ [
328
+ `add export PATH="${dir}:$PATH" to your shell rc`,
329
+ "then rerun: npx @curdx/flow doctor",
330
+ ]
210
331
  );
211
332
  }
212
333
  }
213
334
  }
214
335
 
336
+ if (runtimeEnvironment?.entries?.length > 0) {
337
+ const runtimeEnvSection = createSection("Runtime environment:");
338
+ for (const entry of runtimeEnvironment.entries) {
339
+ pushSectionLine(
340
+ runtimeEnvSection,
341
+ entry.level || "info",
342
+ entry.text,
343
+ entry.details || []
344
+ );
345
+ }
346
+ }
347
+
215
348
  const localProjectSection = createSection("Local project:");
216
349
  if (projectState?.exists) {
217
350
  pushSectionLine(localProjectSection, "ok", `.flow/ ${cwd}`);
@@ -224,5 +357,127 @@ export function buildDoctorReport({
224
357
  pushSectionLine(localProjectSection, "info", ".flow/ not a curdx-flow project (run: curdx-flow init)");
225
358
  }
226
359
 
360
+ const projectMcpSection = createSection("Project MCP config:");
361
+ if (projectMcpConfig?.misplacedExists) {
362
+ pushSectionLine(
363
+ projectMcpSection,
364
+ projectMcpConfig.exists ? "warn" : "err",
365
+ `.claude/.mcp.json ignored by Claude Code`,
366
+ [
367
+ "project MCP config must live at repo root as .mcp.json",
368
+ "move .claude/.mcp.json → .mcp.json, then reopen /mcp or rerun doctor",
369
+ ]
370
+ );
371
+ }
372
+
373
+ if (projectMcpConfig?.exists) {
374
+ if (projectMcpConfig.invalid) {
375
+ pushSectionLine(
376
+ projectMcpSection,
377
+ "err",
378
+ `.mcp.json invalid JSON`,
379
+ [
380
+ projectMcpConfig.parseError,
381
+ "fix the JSON syntax, then run /mcp or npx @curdx/flow doctor again",
382
+ ]
383
+ );
384
+ } else if (projectMcpConfig.shapeError) {
385
+ pushSectionLine(
386
+ projectMcpSection,
387
+ "err",
388
+ `.mcp.json unsupported shape`,
389
+ [
390
+ projectMcpConfig.shapeError,
391
+ 'expected: { "mcpServers": { "<name>": { ... } } }',
392
+ ]
393
+ );
394
+ } else {
395
+ pushSectionLine(
396
+ projectMcpSection,
397
+ "ok",
398
+ `.mcp.json ${projectMcpConfig.serverCount} server(s) declared`
399
+ );
400
+
401
+ for (const warning of projectMcpConfig.relativePathWarnings || []) {
402
+ pushSectionLine(
403
+ projectMcpSection,
404
+ "warn",
405
+ `${warning.serverName.padEnd(22)} relative path in ${warning.field}`,
406
+ [
407
+ `value: ${warning.value}`,
408
+ "Claude Code resolves relative MCP paths against the launch directory, not .mcp.json",
409
+ "use an absolute path or a PATH executable such as npx / uvx",
410
+ "debug: claude --debug mcp",
411
+ ]
412
+ );
413
+ }
414
+ }
415
+ } else if (!projectMcpConfig?.misplacedExists) {
416
+ pushSectionLine(projectMcpSection, "info", ".mcp.json not present");
417
+ }
418
+
419
+ const projectTeamsSection = createSection("Project agent teams:");
420
+ if (projectTeamConfig?.exists) {
421
+ pushSectionLine(
422
+ projectTeamsSection,
423
+ "warn",
424
+ `.claude/teams/teams.json ignored by Claude Code`,
425
+ [
426
+ "official agent-teams docs say project directories do not have a recognized team config surface",
427
+ "remove the file or move team configuration to the supported user-level agent-teams runtime",
428
+ ]
429
+ );
430
+ } else {
431
+ pushSectionLine(projectTeamsSection, "info", ".claude/teams/teams.json not present");
432
+ }
433
+
434
+ const projectSettingsSection = createSection("Project Claude settings:");
435
+ if (projectClaudeSettings?.exists) {
436
+ if (projectClaudeSettings.invalid) {
437
+ pushSectionLine(
438
+ projectSettingsSection,
439
+ "err",
440
+ `.claude/settings.json invalid JSON`,
441
+ [projectClaudeSettings.parseError]
442
+ );
443
+ } else if ((projectClaudeSettings.warnings || []).length > 0) {
444
+ pushSectionLine(projectSettingsSection, "warn", ".claude/settings.json needs review");
445
+ for (const warning of projectClaudeSettings.warnings) {
446
+ pushSectionLine(
447
+ projectSettingsSection,
448
+ "warn",
449
+ warning.message,
450
+ projectSettingsWarningDetails(warning)
451
+ );
452
+ }
453
+ } else {
454
+ pushSectionLine(projectSettingsSection, "ok", ".claude/settings.json conservative");
455
+ }
456
+ } else {
457
+ pushSectionLine(projectSettingsSection, "info", ".claude/settings.json not present");
458
+ }
459
+
460
+ if (projectClaudeSettings?.localExists) {
461
+ pushSectionLine(projectSettingsSection, "info", ".claude/settings.local.json present (local overrides)");
462
+ if (projectClaudeSettings.localInvalid) {
463
+ pushSectionLine(
464
+ projectSettingsSection,
465
+ "err",
466
+ ".claude/settings.local.json invalid JSON",
467
+ [projectClaudeSettings.localParseError]
468
+ );
469
+ } else if ((projectClaudeSettings.localWarnings || []).length > 0) {
470
+ pushSectionLine(projectSettingsSection, "warn", ".claude/settings.local.json affects the local runtime");
471
+ for (const warning of projectClaudeSettings.localWarnings) {
472
+ pushSectionLine(
473
+ projectSettingsSection,
474
+ "warn",
475
+ warning.message,
476
+ projectSettingsWarningDetails(warning)
477
+ );
478
+ }
479
+ }
480
+ }
481
+
227
482
  return { lines, sections, errors, warnings };
228
483
  }
@@ -0,0 +1,196 @@
1
+ const ENV_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max", "auto"];
2
+ const PINNED_MODEL_ENV_FAMILIES = [
3
+ {
4
+ modelVar: "ANTHROPIC_DEFAULT_OPUS_MODEL",
5
+ capsVar: "ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES",
6
+ label: "Opus",
7
+ },
8
+ {
9
+ modelVar: "ANTHROPIC_DEFAULT_SONNET_MODEL",
10
+ capsVar: "ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES",
11
+ label: "Sonnet",
12
+ },
13
+ {
14
+ modelVar: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
15
+ capsVar: "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES",
16
+ label: "Haiku",
17
+ },
18
+ {
19
+ modelVar: "ANTHROPIC_CUSTOM_MODEL_OPTION",
20
+ capsVar: "ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES",
21
+ label: "Custom model option",
22
+ },
23
+ ];
24
+
25
+ function envFlagEnabled(value) {
26
+ if (value === true || value === 1) return true;
27
+ if (typeof value !== "string") return false;
28
+ return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
29
+ }
30
+
31
+ function normalizedEnvValue(value) {
32
+ if (typeof value !== "string") return null;
33
+ const normalized = value.trim();
34
+ return normalized.length > 0 ? normalized : null;
35
+ }
36
+
37
+ function stripExtendedContextSuffix(modelId) {
38
+ return modelId.replace(/\[1m\]$/i, "");
39
+ }
40
+
41
+ function looksProviderSpecificModelId(modelId) {
42
+ const normalized = stripExtendedContextSuffix(modelId);
43
+ if (normalized.includes(":") || normalized.includes("/")) return true;
44
+ if (/^(?:us\.)?anthropic\./i.test(normalized)) return true;
45
+ if (!/^claude-(?:opus|sonnet|haiku)-/i.test(normalized)) return true;
46
+ return false;
47
+ }
48
+
49
+ function positiveIntegerFromEnv(value) {
50
+ const normalized = normalizedEnvValue(value);
51
+ if (!normalized) return null;
52
+ if (!/^[0-9]+$/.test(normalized)) return Number.NaN;
53
+ const parsed = Number(normalized);
54
+ return parsed > 0 ? parsed : Number.NaN;
55
+ }
56
+
57
+ export function inspectRuntimeEnvironment(env = process.env) {
58
+ const entries = [];
59
+ const inCi = envFlagEnabled(env.CI);
60
+
61
+ if (envFlagEnabled(env.CLAUDE_CODE_SIMPLE)) {
62
+ entries.push({
63
+ level: "err",
64
+ text: "CLAUDE_CODE_SIMPLE enabled (bare/simple mode)",
65
+ details: [
66
+ "official docs: this disables auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md",
67
+ "CurDX-Flow cannot load correctly in this mode; unset it before launching Claude Code",
68
+ ],
69
+ });
70
+ }
71
+
72
+ if (envFlagEnabled(env.CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT)) {
73
+ entries.push({
74
+ level: "warn",
75
+ text: "CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT enabled",
76
+ details: [
77
+ "official docs: discovery still works, but Claude runs with the minimal system prompt and collapsed tool descriptions",
78
+ "CurDX-Flow may still load, but planning/review behavior can degrade versus the normal Claude Code prompt",
79
+ ],
80
+ });
81
+ }
82
+
83
+ const effortLevel = normalizedEnvValue(env.CLAUDE_CODE_EFFORT_LEVEL);
84
+ if (effortLevel && !ENV_EFFORT_LEVELS.includes(effortLevel)) {
85
+ entries.push({
86
+ level: "warn",
87
+ text: `CLAUDE_CODE_EFFORT_LEVEL invalid (${effortLevel})`,
88
+ details: [
89
+ `expected one of: ${ENV_EFFORT_LEVELS.join(", ")}`,
90
+ "invalid effort env values can make sessions harder to reason about; remove or correct it",
91
+ ],
92
+ });
93
+ } else if (effortLevel === "low" || effortLevel === "medium") {
94
+ entries.push({
95
+ level: "warn",
96
+ text: `CLAUDE_CODE_EFFORT_LEVEL ${effortLevel}`,
97
+ details: [
98
+ "this takes precedence over /effort and settings effortLevel",
99
+ "CurDX-Flow planning, verification, and review-heavy turns usually work better at high or xhigh",
100
+ ],
101
+ });
102
+ } else if (effortLevel) {
103
+ entries.push({
104
+ level: "info",
105
+ text: `CLAUDE_CODE_EFFORT_LEVEL ${effortLevel}`,
106
+ details: [
107
+ "session effort is pinned through the environment for this process",
108
+ ],
109
+ });
110
+ }
111
+
112
+ if (envFlagEnabled(env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)) {
113
+ entries.push({
114
+ level: "info",
115
+ text: "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS enabled",
116
+ details: [
117
+ "official docs: this enables experimental team surfaces such as SendMessage / TeamCreate / TeamDelete",
118
+ "CurDX-Flow does not depend on these runtime-gated tools, but this explains why teammate features may appear in this session",
119
+ ],
120
+ });
121
+ }
122
+
123
+ const syncPluginInstall = envFlagEnabled(env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL);
124
+ const syncPluginInstallTimeout = positiveIntegerFromEnv(
125
+ env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS
126
+ );
127
+ const pluginSeedDir = normalizedEnvValue(env.CLAUDE_CODE_PLUGIN_SEED_DIR);
128
+
129
+ if (normalizedEnvValue(env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS)) {
130
+ if (Number.isNaN(syncPluginInstallTimeout)) {
131
+ entries.push({
132
+ level: "warn",
133
+ text: "CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS invalid",
134
+ details: [
135
+ "expected a positive integer timeout in milliseconds",
136
+ "invalid timeout values can make headless plugin-install behavior harder to reason about",
137
+ ],
138
+ });
139
+ } else if (!syncPluginInstall) {
140
+ entries.push({
141
+ level: "warn",
142
+ text: "CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS set without CLAUDE_CODE_SYNC_PLUGIN_INSTALL",
143
+ details: [
144
+ "official docs: the timeout only applies when synchronous plugin installation is enabled",
145
+ "set CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 or remove the timeout override",
146
+ ],
147
+ });
148
+ }
149
+ }
150
+
151
+ if (pluginSeedDir) {
152
+ entries.push({
153
+ level: "info",
154
+ text: "CLAUDE_CODE_PLUGIN_SEED_DIR configured",
155
+ details: [
156
+ `seed dir: ${pluginSeedDir}`,
157
+ "official docs: pre-populated plugin seeds let containers and CI start with marketplaces/plugins already available",
158
+ ],
159
+ });
160
+ }
161
+
162
+ if (inCi && !syncPluginInstall && !pluginSeedDir) {
163
+ entries.push({
164
+ level: "warn",
165
+ text: "CI environment without synchronous or seeded plugin availability",
166
+ details: [
167
+ "prefer claude --bare -p for CI so runs do not inherit local hooks, skills, plugins, MCP discovery, or CLAUDE.md unexpectedly",
168
+ "official docs: in non-interactive/headless mode, marketplace plugins may install in the background and miss the first turn",
169
+ "set CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 for headless runs that depend on plugin availability on turn one",
170
+ "or pre-populate plugins with CLAUDE_CODE_PLUGIN_SEED_DIR in containers/CI images",
171
+ "if the run needs project assets in bare mode, pass them explicitly with --plugin-dir, --settings, or --mcp-config",
172
+ ],
173
+ });
174
+ }
175
+
176
+ for (const family of PINNED_MODEL_ENV_FAMILIES) {
177
+ const modelId = normalizedEnvValue(env[family.modelVar]);
178
+ if (!modelId) continue;
179
+
180
+ const caps = normalizedEnvValue(env[family.capsVar]);
181
+ if (!looksProviderSpecificModelId(modelId) || caps) continue;
182
+
183
+ entries.push({
184
+ level: "warn",
185
+ text: `${family.modelVar} uses a provider-specific/custom model id`,
186
+ details: [
187
+ `${family.label} pinned to: ${modelId}`,
188
+ `${family.capsVar} is unset`,
189
+ "official docs: custom/provider model IDs can disable Claude Code feature detection for effort and thinking",
190
+ `declare ${family.capsVar} when pinning custom Bedrock / Vertex / Foundry / gateway model IDs`,
191
+ ],
192
+ });
193
+ }
194
+
195
+ return { entries };
196
+ }
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from "node:fs";
2
+ import YAML from "yaml";
3
+
4
+ function formatYamlErrors(errors) {
5
+ return errors.map((error) => error.message).join("; ");
6
+ }
7
+
8
+ export function extractFrontmatterBlock(text, sourceLabel = "frontmatter") {
9
+ const match = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
10
+ if (!match) {
11
+ throw new Error(`${sourceLabel}: missing YAML frontmatter`);
12
+ }
13
+
14
+ return match[1];
15
+ }
16
+
17
+ export function parseFrontmatterBlock(block, sourceLabel = "frontmatter") {
18
+ const document = YAML.parseDocument(block, {
19
+ strict: true,
20
+ uniqueKeys: true,
21
+ });
22
+
23
+ if (document.errors.length > 0) {
24
+ throw new Error(`${sourceLabel}: invalid YAML (${formatYamlErrors(document.errors)})`);
25
+ }
26
+
27
+ const value = document.toJS();
28
+ if (value == null) return {};
29
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
30
+ throw new Error(`${sourceLabel}: frontmatter must parse to an object`);
31
+ }
32
+
33
+ return value;
34
+ }
35
+
36
+ export function readFrontmatter(filePath) {
37
+ const text = readFileSync(filePath, "utf-8");
38
+ const block = extractFrontmatterBlock(text, filePath);
39
+ return parseFrontmatterBlock(block, filePath);
40
+ }
41
+
42
+ export function readFrontmatterFields(filePath) {
43
+ return Object.keys(readFrontmatter(filePath));
44
+ }
@@ -0,0 +1,57 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import Ajv from "ajv";
4
+
5
+ const ajv = new Ajv({
6
+ allErrors: true,
7
+ allowUnionTypes: true,
8
+ strict: false,
9
+ });
10
+
11
+ const validatorCache = new Map();
12
+
13
+ function formatParams(params = {}) {
14
+ if (params.missingProperty) return ` missingProperty=${params.missingProperty}`;
15
+ if (params.additionalProperty) return ` additionalProperty=${params.additionalProperty}`;
16
+ return "";
17
+ }
18
+
19
+ export function readJsonFile(filePath) {
20
+ return JSON.parse(readFileSync(filePath, "utf-8"));
21
+ }
22
+
23
+ export function formatAjvErrors(errors = []) {
24
+ return errors.map((error) => {
25
+ const where = error.instancePath || error.schemaPath || "(root)";
26
+ return `${where}: ${error.message}${formatParams(error.params)}`;
27
+ });
28
+ }
29
+
30
+ function getSchemaValidator(schemaPath) {
31
+ const absPath = resolve(schemaPath);
32
+ if (validatorCache.has(absPath)) return validatorCache.get(absPath);
33
+
34
+ const schema = readJsonFile(absPath);
35
+ const validate = ajv.compile(schema);
36
+ const entry = { absPath, schema, validate };
37
+ validatorCache.set(absPath, entry);
38
+ return entry;
39
+ }
40
+
41
+ export function validateAgainstSchemaFile(schemaPath, data) {
42
+ const { validate } = getSchemaValidator(schemaPath);
43
+ const valid = Boolean(validate(data));
44
+ return {
45
+ valid,
46
+ errors: valid ? [] : formatAjvErrors(validate.errors),
47
+ };
48
+ }
49
+
50
+ export function validateSchemaFile(schemaPath) {
51
+ const schema = readJsonFile(resolve(schemaPath));
52
+ const valid = ajv.validateSchema(schema);
53
+ return {
54
+ valid,
55
+ errors: valid ? [] : formatAjvErrors(ajv.errors),
56
+ };
57
+ }
@@ -52,15 +52,18 @@ function findSymlinkDir() {
52
52
  return null;
53
53
  }
54
54
 
55
- export function ensureRuntimeInPath(cmd, candidates) {
55
+ function getRuntimeStatus(cmd, candidates, { repair = false } = {}) {
56
56
  if (has(cmd)) return { status: "ok" };
57
-
58
57
  const realPath = findRuntime(candidates);
59
58
  if (!realPath) return { status: "missing" };
60
59
 
61
60
  const linkDir = findSymlinkDir();
62
61
  if (!linkDir) return { status: "path-unwritable", path: realPath };
63
62
 
63
+ if (!repair) {
64
+ return { status: "linkable", path: realPath, link: join(linkDir, cmd) };
65
+ }
66
+
64
67
  const linkPath = join(linkDir, cmd);
65
68
  if (existsSync(linkPath)) {
66
69
  try {
@@ -81,6 +84,21 @@ export function ensureRuntimeInPath(cmd, candidates) {
81
84
  }
82
85
  }
83
86
 
87
+ export function inspectRuntimeInPath(cmd, candidates) {
88
+ return getRuntimeStatus(cmd, candidates, { repair: false });
89
+ }
90
+
91
+ export function ensureRuntimeInPath(cmd, candidates) {
92
+ return getRuntimeStatus(cmd, candidates, { repair: true });
93
+ }
94
+
95
+ export function inspectClaudeMemRuntimes() {
96
+ return {
97
+ bun: inspectRuntimeInPath("bun", BUN_CANDIDATES),
98
+ uv: inspectRuntimeInPath("uv", UV_CANDIDATES),
99
+ };
100
+ }
101
+
84
102
  export function ensureClaudeMemRuntimes() {
85
103
  return {
86
104
  bun: ensureRuntimeInPath("bun", BUN_CANDIDATES),
package/cli/lib/semver.js CHANGED
@@ -93,3 +93,17 @@ export function isVersionNewer(latestVersion, currentVersion) {
93
93
  export function isVersionAtLeast(version, minimumVersion) {
94
94
  return compareVersions(version, minimumVersion) >= 0;
95
95
  }
96
+
97
+ export function deriveNpmDistTag(version) {
98
+ const { prerelease } = parseVersion(version);
99
+
100
+ if (prerelease.length === 0) {
101
+ return "latest";
102
+ }
103
+
104
+ const namedChannel = prerelease.find(
105
+ (token) => typeof token === "string" && token.length > 0
106
+ );
107
+
108
+ return namedChannel || "prerelease";
109
+ }