@curdx/flow 2.1.0 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.claude-plugin/marketplace.json +25 -2
  2. package/.claude-plugin/plugin.json +27 -1
  3. package/CHANGELOG.md +32 -0
  4. package/README.md +18 -8
  5. package/README.zh.md +8 -3
  6. package/agent-preamble/preamble.md +35 -2
  7. package/agents/flow-adversary.md +1 -1
  8. package/agents/flow-architect.md +2 -1
  9. package/agents/flow-brownfield-analyst.md +153 -0
  10. package/agents/flow-debugger.md +6 -11
  11. package/agents/flow-edge-hunter.md +1 -1
  12. package/agents/flow-executor.md +30 -8
  13. package/agents/flow-planner.md +38 -5
  14. package/agents/flow-product-designer.md +2 -1
  15. package/agents/flow-qa-engineer.md +25 -20
  16. package/agents/flow-researcher.md +2 -1
  17. package/agents/flow-reviewer.md +23 -5
  18. package/agents/flow-security-auditor.md +5 -3
  19. package/agents/flow-triage-analyst.md +5 -24
  20. package/agents/flow-ui-researcher.md +6 -5
  21. package/agents/flow-ux-designer.md +12 -39
  22. package/agents/flow-verifier.md +38 -6
  23. package/bin/curdx-flow +5 -0
  24. package/cli/README.md +13 -10
  25. package/cli/doctor-workflow.js +1074 -2
  26. package/cli/doctor.js +8 -0
  27. package/cli/help.js +2 -0
  28. package/cli/install-companions.js +4 -1
  29. package/cli/install-required-plugins.js +18 -5
  30. package/cli/install-self-update.js +2 -91
  31. package/cli/install.js +12 -1
  32. package/cli/lib/claude.js +42 -11
  33. package/cli/lib/doctor-report.js +303 -9
  34. package/cli/lib/frontmatter.js +44 -0
  35. package/cli/lib/json-schema.js +57 -0
  36. package/cli/lib/runtime.js +20 -2
  37. package/cli/lib/semver.js +95 -0
  38. package/cli/utils.js +7 -1
  39. package/gates/adversarial-review-gate.md +1 -1
  40. package/gates/security-gate.md +2 -2
  41. package/gates/test-quality-gate.md +59 -0
  42. package/hooks/hooks.json +16 -2
  43. package/hooks/scripts/common.sh +4 -0
  44. package/hooks/scripts/quick-mode-guard.sh +6 -7
  45. package/hooks/scripts/session-start.sh +17 -2
  46. package/hooks/scripts/stop-watcher.sh +69 -18
  47. package/hooks/scripts/subagent-artifact-guard.sh +159 -0
  48. package/hooks/scripts/subagent-statusline.sh +105 -0
  49. package/knowledge/atomic-commits.md +1 -1
  50. package/knowledge/claude-code-runtime-contracts.md +203 -0
  51. package/knowledge/epic-decomposition.md +1 -1
  52. package/knowledge/execution-strategies.md +28 -6
  53. package/knowledge/planning-reviews.md +4 -4
  54. package/knowledge/poc-first-workflow.md +8 -8
  55. package/knowledge/review-feedback-intake.md +57 -0
  56. package/knowledge/two-stage-review.md +19 -6
  57. package/knowledge/wave-execution.md +33 -18
  58. package/output-styles/curdx-evidence-first.md +34 -0
  59. package/package.json +9 -2
  60. package/schemas/agent-frontmatter.schema.json +59 -0
  61. package/schemas/config.schema.json +37 -3
  62. package/schemas/gate-frontmatter.schema.json +30 -0
  63. package/schemas/hooks.schema.json +115 -0
  64. package/schemas/output-style-frontmatter.schema.json +22 -0
  65. package/schemas/plugin-manifest.schema.json +436 -0
  66. package/schemas/plugin-settings.schema.json +29 -0
  67. package/schemas/skill-frontmatter.schema.json +177 -0
  68. package/schemas/spec-state.schema.json +35 -5
  69. package/settings.json +6 -0
  70. package/skills/brownfield-index/SKILL.md +33 -36
  71. package/skills/browser-qa/SKILL.md +16 -7
  72. package/skills/cancel/SKILL.md +82 -0
  73. package/skills/debug/SKILL.md +7 -2
  74. package/skills/epic/SKILL.md +7 -4
  75. package/skills/fast/SKILL.md +3 -1
  76. package/skills/help/SKILL.md +18 -7
  77. package/skills/implement/SKILL.md +44 -12
  78. package/skills/implement/references/wave-execution.md +9 -9
  79. package/skills/init/SKILL.md +3 -1
  80. package/skills/review/SKILL.md +6 -2
  81. package/skills/security-audit/SKILL.md +19 -4
  82. package/skills/spec/SKILL.md +6 -4
  83. package/skills/start/SKILL.md +20 -19
  84. package/skills/status/SKILL.md +85 -0
  85. package/skills/ui-sketch/SKILL.md +13 -4
  86. package/skills/verify/SKILL.md +15 -2
  87. package/templates/CONTEXT.md.tmpl +1 -1
  88. package/templates/PROJECT.md.tmpl +1 -1
  89. package/templates/config.json.tmpl +9 -6
  90. package/templates/progress.md.tmpl +21 -2
  91. package/templates/tasks.md.tmpl +26 -3
package/cli/doctor.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "./utils.js";
9
9
  import { buildDoctorReport } from "./lib/doctor-report.js";
10
10
  import {
11
+ applyDoctorFixes,
11
12
  collectDoctorData,
12
13
  createDoctorContext,
13
14
  printDoctorSummary,
@@ -21,6 +22,13 @@ export async function doctor(args = []) {
21
22
  log.title("🏥 CurdX-Flow Health Check");
22
23
 
23
24
  const doctorData = await collectDoctorData();
25
+ if (context.fix) {
26
+ log.info("Applying safe fixes...");
27
+ const fixes = await applyDoctorFixes(doctorData);
28
+ if (fixes.length === 0) {
29
+ log.info("No automatic fixes available for the current environment");
30
+ }
31
+ }
24
32
  const report = buildDoctorReport(doctorData);
25
33
 
26
34
  renderReportLines(report.lines);
package/cli/help.js CHANGED
@@ -17,6 +17,8 @@ ${color.bold("COMMANDS")}
17
17
  when the plugin body is bundled)
18
18
 
19
19
  ${color.cyan("doctor")} Check health (claude CLI, plugin, MCPs, recommended)
20
+ --fix Apply safe runtime fixes (bun/uv PATH symlinks)
21
+ --verbose Show raw plugin list details
20
22
 
21
23
  ${color.cyan("upgrade")} Update curdx-flow and recommended plugins to latest
22
24
 
@@ -11,6 +11,9 @@
11
11
  * install-context7-config.js — context7 API-key prompt (private to required)
12
12
  */
13
13
 
14
- export { installRequiredPlugins } from "./install-required-plugins.js";
14
+ export {
15
+ addRequiredPluginMarketplaces,
16
+ installRequiredPlugins,
17
+ } from "./install-required-plugins.js";
15
18
  export { registerBundledMcps } from "./install-bundled-mcps.js";
16
19
  export { installRecommendedPlugins } from "./install-recommended-plugins.js";
@@ -8,15 +8,28 @@ import { REQUIRED_PLUGINS } from "./registry.js";
8
8
  import { color, log, resultLastLine } from "./utils.js";
9
9
  import { installContext7Config } from "./install-context7-config.js";
10
10
 
11
- export async function installRequiredPlugins({ yes, language, config }) {
12
- log.blank();
13
- log.info("Installing required Claude Code plugins...");
11
+ export async function addRequiredPluginMarketplaces({ logWarnings = true } = {}) {
14
12
  for (const plugin of REQUIRED_PLUGINS) {
15
- console.log(` ${color.cyan("▸")} Installing ${color.bold(plugin.name)}...`);
16
13
  const ma = await addPluginMarketplace(plugin);
17
- if (ma.code !== 0 && !ma.stderr.includes("already")) {
14
+ if (ma.code !== 0 && !ma.stderr.includes("already") && logWarnings) {
18
15
  log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
19
16
  }
17
+ }
18
+ }
19
+
20
+ export async function installRequiredPlugins({
21
+ yes,
22
+ language,
23
+ config,
24
+ skipMarketplaceAdd = false,
25
+ }) {
26
+ log.blank();
27
+ log.info("Installing required Claude Code plugins...");
28
+ if (!skipMarketplaceAdd) {
29
+ await addRequiredPluginMarketplaces();
30
+ }
31
+ for (const plugin of REQUIRED_PLUGINS) {
32
+ console.log(` ${color.cyan("▸")} Installing ${color.bold(plugin.name)}...`);
20
33
 
21
34
  const ir = await installPlugin(plugin);
22
35
  if (ir.code === 0) {
@@ -2,98 +2,9 @@ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { log, run, runSync, VERSION } from "./utils.js";
5
+ import { compareVersions, isVersionNewer } from "./lib/semver.js";
5
6
 
6
- function normalizeVersionToken(token) {
7
- return /^\d+$/.test(token) ? Number(token) : token;
8
- }
9
-
10
- function parseVersion(version) {
11
- const normalized = String(version || "").trim().replace(/^v/i, "");
12
- const [coreRaw = "0", prereleaseRaw] = normalized.split("-", 2);
13
- const core = coreRaw.split(".").map((part) => Number.parseInt(part, 10) || 0);
14
- const prerelease = prereleaseRaw
15
- ? prereleaseRaw.split(/[.-]/).filter(Boolean).map(normalizeVersionToken)
16
- : [];
17
-
18
- return { core, prerelease };
19
- }
20
-
21
- function compareIdentifier(left, right) {
22
- if (left === right) {
23
- return 0;
24
- }
25
-
26
- const leftIsNumber = typeof left === "number";
27
- const rightIsNumber = typeof right === "number";
28
-
29
- if (leftIsNumber && rightIsNumber) {
30
- return left > right ? 1 : -1;
31
- }
32
-
33
- if (leftIsNumber) {
34
- return -1;
35
- }
36
-
37
- if (rightIsNumber) {
38
- return 1;
39
- }
40
-
41
- return left > right ? 1 : -1;
42
- }
43
-
44
- export function compareVersions(leftVersion, rightVersion) {
45
- const left = parseVersion(leftVersion);
46
- const right = parseVersion(rightVersion);
47
- const coreLength = Math.max(left.core.length, right.core.length);
48
-
49
- for (let index = 0; index < coreLength; index += 1) {
50
- const leftPart = left.core[index] ?? 0;
51
- const rightPart = right.core[index] ?? 0;
52
- if (leftPart !== rightPart) {
53
- return leftPart > rightPart ? 1 : -1;
54
- }
55
- }
56
-
57
- const leftHasPrerelease = left.prerelease.length > 0;
58
- const rightHasPrerelease = right.prerelease.length > 0;
59
-
60
- if (!leftHasPrerelease && !rightHasPrerelease) {
61
- return 0;
62
- }
63
-
64
- if (!leftHasPrerelease) {
65
- return 1;
66
- }
67
-
68
- if (!rightHasPrerelease) {
69
- return -1;
70
- }
71
-
72
- const prereleaseLength = Math.max(left.prerelease.length, right.prerelease.length);
73
- for (let index = 0; index < prereleaseLength; index += 1) {
74
- const leftPart = left.prerelease[index];
75
- const rightPart = right.prerelease[index];
76
-
77
- if (leftPart === undefined) {
78
- return -1;
79
- }
80
-
81
- if (rightPart === undefined) {
82
- return 1;
83
- }
84
-
85
- const comparison = compareIdentifier(leftPart, rightPart);
86
- if (comparison !== 0) {
87
- return comparison;
88
- }
89
- }
90
-
91
- return 0;
92
- }
93
-
94
- export function isVersionNewer(latestVersion, currentVersion) {
95
- return compareVersions(latestVersion, currentVersion) > 0;
96
- }
7
+ export { compareVersions, isVersionNewer };
97
8
 
98
9
  /**
99
10
  * Check for CLI updates and auto-update if available.
package/cli/install.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import {
6
+ addRequiredPluginMarketplaces,
6
7
  installRequiredPlugins,
7
8
  registerBundledMcps,
8
9
  } from "./install-companions.js";
@@ -60,6 +61,11 @@ export async function install(args = []) {
60
61
 
61
62
  await addCurdxMarketplace(context);
62
63
 
64
+ // Claude Code resolves plugin dependencies during install. Register
65
+ // required companion marketplaces before installing curdx-flow so its
66
+ // cross-marketplace dependency on Context7 can be satisfied immediately.
67
+ await addRequiredPluginMarketplaces({ logWarnings: false });
68
+
63
69
  // ---------- Step 3: Install curdx-flow plugin ----------
64
70
  // Read the version the marketplace is shipping so we can decide whether an
65
71
  // already-installed plugin needs an update (same name but stale version
@@ -68,7 +74,12 @@ export async function install(args = []) {
68
74
  await installCurdxFlowPlugin({ prevCurdxFlow, shippedVersion });
69
75
 
70
76
  // ---------- Step 3.5: Install required plugins + register user-level MCPs ----------
71
- await installRequiredPlugins({ yes: context.yes, language, config });
77
+ await installRequiredPlugins({
78
+ yes: context.yes,
79
+ language,
80
+ config,
81
+ skipMarketplaceAdd: true,
82
+ });
72
83
 
73
84
  // Beta.12: direct MCPs migrated from plugin.json bundling. See cli/registry.js
74
85
  // for the rationale. Context7 now uses Upstash's official plugin instead.
package/cli/lib/claude.js CHANGED
@@ -14,20 +14,51 @@ export function claudeVersion() {
14
14
  return m ? m[1] : res.stdout.trim().split("\n")[0];
15
15
  }
16
16
 
17
+ function normalizePluginError(error) {
18
+ if (typeof error === "string") {
19
+ return error;
20
+ }
21
+ if (error && typeof error === "object") {
22
+ return error.message || error.code || JSON.stringify(error);
23
+ }
24
+ return String(error);
25
+ }
26
+
27
+ export function parsePluginListJson(output) {
28
+ const trimmed = String(output || "").trim();
29
+ if (!trimmed.startsWith("[")) {
30
+ return null;
31
+ }
32
+
33
+ const arr = JSON.parse(trimmed);
34
+ return arr.map((p) => {
35
+ const id = String(p.id || p.name || "");
36
+ const errors = Array.isArray(p.errors)
37
+ ? p.errors.map(normalizePluginError).filter(Boolean)
38
+ : [];
39
+ const enabled = p.enabled !== false;
40
+
41
+ return {
42
+ id,
43
+ name: id.split("@")[0],
44
+ marketplaceId: id.split("@")[1] || undefined,
45
+ version: p.version,
46
+ status: errors.length > 0 ? "failed" : enabled ? "enabled" : "disabled",
47
+ scope: p.scope,
48
+ errors,
49
+ raw: JSON.stringify(p),
50
+ };
51
+ });
52
+ }
53
+
17
54
  export function listPlugins() {
18
55
  const j = runSync("claude", ["plugin", "list", "--json"]);
19
- if (j.code === 0 && j.stdout.trim().startsWith("[")) {
56
+ if (j.code === 0) {
20
57
  try {
21
- const arr = JSON.parse(j.stdout);
22
- return arr.map((p) => ({
23
- id: String(p.id || ""),
24
- name: String(p.id || "").split("@")[0],
25
- marketplaceId: String(p.id || "").split("@")[1] || undefined,
26
- version: p.version,
27
- status: p.enabled === false ? "disabled" : "enabled",
28
- scope: p.scope,
29
- raw: JSON.stringify(p),
30
- }));
58
+ const plugins = parsePluginListJson(j.stdout);
59
+ if (plugins) {
60
+ return plugins;
61
+ }
31
62
  } catch {
32
63
  // JSON parse failed; fall through to legacy text parser.
33
64
  }
@@ -4,6 +4,117 @@ import {
4
4
  findPluginByRegistryEntry,
5
5
  hasMarketplace,
6
6
  } from "./claude.js";
7
+ import { isVersionAtLeast } from "./semver.js";
8
+
9
+ export const MIN_CLAUDE_VERSION = "2.1.110";
10
+
11
+ function pluginErrorDetails(plugin) {
12
+ return Array.isArray(plugin?.errors) ? plugin.errors : [];
13
+ }
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
+ }
7
118
 
8
119
  export function buildDoctorReport({
9
120
  claudeVersionValue,
@@ -13,8 +124,12 @@ export function buildDoctorReport({
13
124
  mcps = [],
14
125
  userMcpConfig,
15
126
  runtimeStatus,
127
+ runtimeEnvironment,
16
128
  cwd,
17
129
  projectState,
130
+ projectMcpConfig,
131
+ projectTeamConfig,
132
+ projectClaudeSettings,
18
133
  }) {
19
134
  const lines = [];
20
135
  const sections = [];
@@ -37,6 +152,17 @@ export function buildDoctorReport({
37
152
 
38
153
  if (claudeVersionValue) {
39
154
  pushLine(lines, "ok", `claude CLI ${claudeVersionValue}`);
155
+ if (!isVersionAtLeast(claudeVersionValue, MIN_CLAUDE_VERSION)) {
156
+ pushLine(
157
+ lines,
158
+ "warn",
159
+ `claude CLI ${claudeVersionValue} below recommended ${MIN_CLAUDE_VERSION}`,
160
+ [
161
+ "curdx-flow uses modern Claude Code plugin dependency resolution and plugin bin/ PATH support",
162
+ "run: claude update",
163
+ ]
164
+ );
165
+ }
40
166
  } else {
41
167
  pushLine(lines, "err", "claude CLI not found (install Claude Code)");
42
168
  }
@@ -47,7 +173,12 @@ export function buildDoctorReport({
47
173
  if (curdx.status === "enabled") {
48
174
  pushLine(lines, "ok", `curdx-flow v${curdx.version} (enabled)`);
49
175
  } else {
50
- pushLine(lines, "err", `curdx-flow v${curdx.version} (${curdx.status})`);
176
+ pushLine(
177
+ lines,
178
+ "err",
179
+ `curdx-flow v${curdx.version} (${curdx.status})`,
180
+ pluginErrorDetails(curdx)
181
+ );
51
182
  }
52
183
  } else {
53
184
  pushLine(lines, "warn", "curdx-flow not installed → run curdx-flow install");
@@ -67,7 +198,12 @@ export function buildDoctorReport({
67
198
  if (plugin && plugin.status === "enabled") {
68
199
  pushSectionLine(requiredSection, "ok", `${entry.name.padEnd(22)} v${plugin.version || "unknown"}`);
69
200
  } else if (plugin && plugin.status === "failed") {
70
- pushSectionLine(requiredSection, "err", `${entry.name.padEnd(22)} load failed`);
201
+ pushSectionLine(
202
+ requiredSection,
203
+ "err",
204
+ `${entry.name.padEnd(22)} load failed`,
205
+ pluginErrorDetails(plugin)
206
+ );
71
207
  } else {
72
208
  pushSectionLine(
73
209
  requiredSection,
@@ -120,7 +256,12 @@ export function buildDoctorReport({
120
256
  pushSectionLine(recommendedSection, "ok", `${entry.name.padEnd(22)} v${plugin.version}`);
121
257
  if (entry.postInstall === "claude-mem-runtimes") claudeMemEnabled = true;
122
258
  } else if (plugin && plugin.status === "failed") {
123
- pushSectionLine(recommendedSection, "err", `${entry.name.padEnd(22)} load failed`);
259
+ pushSectionLine(
260
+ recommendedSection,
261
+ "err",
262
+ `${entry.name.padEnd(22)} load failed`,
263
+ pluginErrorDetails(plugin)
264
+ );
124
265
  } else {
125
266
  pushSectionLine(
126
267
  recommendedSection,
@@ -133,16 +274,22 @@ export function buildDoctorReport({
133
274
 
134
275
  const duplicates = findDuplicateMcps(mcps, userMcpConfig);
135
276
  if (duplicates.length > 0) {
136
- const duplicateSection = createSection("Legacy plugin-bundled MCPs still present:");
277
+ const duplicateSection = createSection("Duplicate MCP registrations:");
137
278
  for (const duplicate of duplicates) {
279
+ const details = duplicate.pluginEntry.plugin === "curdx-flow"
280
+ ? [
281
+ "migration: claude plugin update curdx-flow@curdx-flow-marketplace",
282
+ "then restart Claude Code",
283
+ ]
284
+ : [
285
+ `remove the duplicate user-level server if plugin:${duplicate.pluginEntry.plugin} should own it`,
286
+ `run: claude mcp remove --scope user ${duplicate.name}`,
287
+ ];
138
288
  pushSectionLine(
139
289
  duplicateSection,
140
290
  "warn",
141
291
  `${duplicate.name.padEnd(22)} both user-level AND plugin:${duplicate.pluginEntry.plugin} active`,
142
- [
143
- "migration: claude plugin update curdx-flow@curdx-flow-marketplace",
144
- "then restart Claude Code",
145
- ]
292
+ details
146
293
  );
147
294
  }
148
295
  }
@@ -152,6 +299,16 @@ export function buildDoctorReport({
152
299
  for (const [name, status] of Object.entries(runtimeStatus)) {
153
300
  if (status.status === "ok") {
154
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
+ );
155
312
  } else if (status.status === "linked") {
156
313
  pushSectionLine(runtimeSection, "ok", `${name.padEnd(22)} auto-linked ${status.link} → ${status.path}`);
157
314
  } else if (status.status === "missing") {
@@ -167,12 +324,27 @@ export function buildDoctorReport({
167
324
  runtimeSection,
168
325
  "err",
169
326
  `${name.padEnd(22)} installed but not on PATH`,
170
- [`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
+ ]
171
331
  );
172
332
  }
173
333
  }
174
334
  }
175
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
+
176
348
  const localProjectSection = createSection("Local project:");
177
349
  if (projectState?.exists) {
178
350
  pushSectionLine(localProjectSection, "ok", `.flow/ ${cwd}`);
@@ -185,5 +357,127 @@ export function buildDoctorReport({
185
357
  pushSectionLine(localProjectSection, "info", ".flow/ not a curdx-flow project (run: curdx-flow init)");
186
358
  }
187
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
+
188
482
  return { lines, sections, errors, warnings };
189
483
  }