@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.
- package/.claude-plugin/marketplace.json +25 -2
- package/.claude-plugin/plugin.json +27 -1
- package/CHANGELOG.md +32 -0
- package/README.md +18 -8
- package/README.zh.md +8 -3
- package/agent-preamble/preamble.md +35 -2
- package/agents/flow-adversary.md +1 -1
- package/agents/flow-architect.md +2 -1
- package/agents/flow-brownfield-analyst.md +153 -0
- package/agents/flow-debugger.md +6 -11
- package/agents/flow-edge-hunter.md +1 -1
- package/agents/flow-executor.md +30 -8
- package/agents/flow-planner.md +38 -5
- package/agents/flow-product-designer.md +2 -1
- package/agents/flow-qa-engineer.md +25 -20
- package/agents/flow-researcher.md +2 -1
- package/agents/flow-reviewer.md +23 -5
- package/agents/flow-security-auditor.md +5 -3
- package/agents/flow-triage-analyst.md +5 -24
- package/agents/flow-ui-researcher.md +6 -5
- package/agents/flow-ux-designer.md +12 -39
- package/agents/flow-verifier.md +38 -6
- package/bin/curdx-flow +5 -0
- package/cli/README.md +13 -10
- package/cli/doctor-workflow.js +1074 -2
- package/cli/doctor.js +8 -0
- package/cli/help.js +2 -0
- package/cli/install-companions.js +4 -1
- package/cli/install-required-plugins.js +18 -5
- package/cli/install-self-update.js +2 -91
- package/cli/install.js +12 -1
- package/cli/lib/claude.js +42 -11
- package/cli/lib/doctor-report.js +303 -9
- package/cli/lib/frontmatter.js +44 -0
- package/cli/lib/json-schema.js +57 -0
- package/cli/lib/runtime.js +20 -2
- package/cli/lib/semver.js +95 -0
- package/cli/utils.js +7 -1
- package/gates/adversarial-review-gate.md +1 -1
- package/gates/security-gate.md +2 -2
- package/gates/test-quality-gate.md +59 -0
- package/hooks/hooks.json +16 -2
- package/hooks/scripts/common.sh +4 -0
- package/hooks/scripts/quick-mode-guard.sh +6 -7
- package/hooks/scripts/session-start.sh +17 -2
- package/hooks/scripts/stop-watcher.sh +69 -18
- package/hooks/scripts/subagent-artifact-guard.sh +159 -0
- package/hooks/scripts/subagent-statusline.sh +105 -0
- package/knowledge/atomic-commits.md +1 -1
- package/knowledge/claude-code-runtime-contracts.md +203 -0
- package/knowledge/epic-decomposition.md +1 -1
- package/knowledge/execution-strategies.md +28 -6
- package/knowledge/planning-reviews.md +4 -4
- package/knowledge/poc-first-workflow.md +8 -8
- package/knowledge/review-feedback-intake.md +57 -0
- package/knowledge/two-stage-review.md +19 -6
- package/knowledge/wave-execution.md +33 -18
- package/output-styles/curdx-evidence-first.md +34 -0
- package/package.json +9 -2
- package/schemas/agent-frontmatter.schema.json +59 -0
- package/schemas/config.schema.json +37 -3
- package/schemas/gate-frontmatter.schema.json +30 -0
- package/schemas/hooks.schema.json +115 -0
- package/schemas/output-style-frontmatter.schema.json +22 -0
- package/schemas/plugin-manifest.schema.json +436 -0
- package/schemas/plugin-settings.schema.json +29 -0
- package/schemas/skill-frontmatter.schema.json +177 -0
- package/schemas/spec-state.schema.json +35 -5
- package/settings.json +6 -0
- package/skills/brownfield-index/SKILL.md +33 -36
- package/skills/browser-qa/SKILL.md +16 -7
- package/skills/cancel/SKILL.md +82 -0
- package/skills/debug/SKILL.md +7 -2
- package/skills/epic/SKILL.md +7 -4
- package/skills/fast/SKILL.md +3 -1
- package/skills/help/SKILL.md +18 -7
- package/skills/implement/SKILL.md +44 -12
- package/skills/implement/references/wave-execution.md +9 -9
- package/skills/init/SKILL.md +3 -1
- package/skills/review/SKILL.md +6 -2
- package/skills/security-audit/SKILL.md +19 -4
- package/skills/spec/SKILL.md +6 -4
- package/skills/start/SKILL.md +20 -19
- package/skills/status/SKILL.md +85 -0
- package/skills/ui-sketch/SKILL.md +13 -4
- package/skills/verify/SKILL.md +15 -2
- package/templates/CONTEXT.md.tmpl +1 -1
- package/templates/PROJECT.md.tmpl +1 -1
- package/templates/config.json.tmpl +9 -6
- package/templates/progress.md.tmpl +21 -2
- 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 {
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
|
56
|
+
if (j.code === 0) {
|
|
20
57
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
}
|
package/cli/lib/doctor-report.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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("
|
|
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
|
-
[
|
|
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
|
}
|