@curdx/flow 2.0.18 → 2.0.20

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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code Discipline Layer — spec-driven workflow + goal-backward verification + Karpathy 4 principles enforced via gates. Stops Claude from faking \"done\" on non-trivial features.",
9
- "version": "2.0.18"
9
+ "version": "2.0.20"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "curdx-flow",
3
- "version": "2.0.18",
3
+ "version": "2.0.20",
4
4
  "description": "Claude Code Discipline Layer — spec-driven workflow + goal-backward verification + Karpathy 4 principles enforced via gates. Stops Claude from faking \"done\" on non-trivial features.",
5
5
  "author": {
6
6
  "name": "wdx",
package/README.zh.md CHANGED
@@ -19,11 +19,11 @@ CurdX-Flow 是一个 Claude Code 插件,把 6 个验证过的 AI 工程工作
19
19
  ## 一览(v2)
20
20
 
21
21
  - **9 个命令** — 初始化 / 启动规格 / 规格 / 执行 / 验证 / 审查 / 调试 / fast / 帮助
22
- - **15 个内部代理** — 由命令调度,不再暴露 v1 的人格角色
22
+ - **15 个内部代理** — 由命令调度,按职能分工执行
23
23
  - **8 个可组合 Gate** — Karpathy / Verification / TDD / Coverage / Adversarial / Edge-Case / Security / DevEx
24
24
  - **4 种执行策略** — linear / subagent / stop-hook / wave(自动路由)
25
25
  - **10 个知识文档** — 规格驱动 / POC-First / 原子提交 / 执行策略 / ...
26
- - **5 个 hook 事件** — SessionStart / InstructionsLoaded / PostToolUseFailure / Stop / PreToolUse
26
+ - **4 个 hook 事件** — SessionStart / SessionStart(startup|clear|compact) / Stop / PreToolUse
27
27
  - **必需文档/推理工具 + 4 个推荐插件** — Context7 官方插件 / sequential-thinking MCP + pua / claude-mem / frontend-design / chrome-devtools-mcp
28
28
  - **优雅降级** — 依赖缺失时进入 fallback 模式并清晰告知
29
29
 
@@ -37,7 +37,7 @@ CurdX-Flow 是一个 Claude Code 插件,把 6 个验证过的 AI 工程工作
37
37
  | 无工程纪律(AI 跳步、幻觉) | Karpathy L1 + verification-gate + TDD-gate |
38
38
  | 最佳实践分散 | 6 个工作流蒸馏到一个元框架 |
39
39
  | 验证薄弱(无证据声称"完成") | 目标反向验证 + 禁用词检测 |
40
- | 单视角决策 | Party Mode(真正多代理,非角色扮演)+ 对抗审查 |
40
+ | 单视角决策 | 多代理协作 + 对抗审查 |
41
41
 
42
42
  ## 安装
43
43
 
@@ -91,7 +91,7 @@ claude --plugin-dir ./curdx-flow
91
91
 
92
92
  1. **Mind(L1 基线)** — Karpathy 4 原则 + 必用工具规则 + 三条红线
93
93
  2. **Spec(规格)** — 研究 → 需求 → 设计 → 任务(可伸缩,4 文件 lite 到 12 文件 enterprise)
94
- 3. **Agents(代理)** — 15 职能 + 9 人格(Mary / John / Winston / ...)
94
+ 3. **Agents(代理)** — 15 个内部代理,按命令和流程调度
95
95
  4. **Flow(流程)** — 4 种执行策略,自动路由
96
96
  5. **Memory(记忆)** — claude-mem(隐式)+ `.flow/STATE.md`(显式决策 D-NN)
97
97
 
@@ -126,7 +126,7 @@ CurdX-Flow 是蒸馏,不是原创。深深致谢:
126
126
  - [**andrej-karpathy-skills**](https://github.com/forrestchang/andrej-karpathy-skills) — 4 原则
127
127
  - [**smart-ralph**](https://github.com/Nibzard/smart-ralph) — 规格引擎 + stop-hook 循环
128
128
  - [**superpowers**](https://github.com/obra/superpowers) — Subagent 纪律 + TDD + 两阶段审查
129
- - [**BMAD-METHOD**](https://github.com/bmad-code-org/BMAD-METHOD) — 人格 + Party Mode + 对抗审查
129
+ - [**BMAD-METHOD**](https://github.com/bmad-code-org/BMAD-METHOD) — 协作流程 + 对抗审查
130
130
  - [**get-shit-done**](https://github.com/ryangentry/get-shit-done) — 波形执行 + 决策锁定 + 多源审计
131
131
  - [**gstack**](https://github.com/garrytan/gstack)(Garry Tan) — 规划审查 + DX 哲学
132
132
  - [**pua**](https://github.com/tanweai/pua) — 持续性 + 三条红线
@@ -0,0 +1,121 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ claudeVersion,
6
+ color,
7
+ ensureClaudeMemRuntimes,
8
+ listMcps,
9
+ listPluginMarketplaces,
10
+ listPlugins,
11
+ log,
12
+ readUserMcpConfig,
13
+ runSync,
14
+ } from "./utils.js";
15
+
16
+ export function createDoctorContext(args = []) {
17
+ return {
18
+ verbose: args.includes("--verbose") || args.includes("-v"),
19
+ };
20
+ }
21
+
22
+ export async function readProjectState(cwd = process.cwd()) {
23
+ const flowDir = path.join(cwd, ".flow");
24
+ try {
25
+ const stat = await fs.stat(flowDir);
26
+ if (!stat.isDirectory()) {
27
+ return { exists: false, activeSpec: null };
28
+ }
29
+
30
+ try {
31
+ const activeSpec = await fs.readFile(path.join(flowDir, ".active-spec"), "utf-8");
32
+ return { exists: true, activeSpec: activeSpec.trim() };
33
+ } catch {
34
+ return { exists: true, activeSpec: null };
35
+ }
36
+ } catch {
37
+ return { exists: false, activeSpec: null };
38
+ }
39
+ }
40
+
41
+ export async function collectDoctorData(
42
+ {
43
+ cwd = process.cwd(),
44
+ } = {},
45
+ {
46
+ claudeVersionImpl = claudeVersion,
47
+ listPluginsImpl = listPlugins,
48
+ listPluginMarketplacesImpl = listPluginMarketplaces,
49
+ listMcpsImpl = listMcps,
50
+ readUserMcpConfigImpl = readUserMcpConfig,
51
+ ensureClaudeMemRuntimesImpl = ensureClaudeMemRuntimes,
52
+ readProjectStateImpl = readProjectState,
53
+ } = {}
54
+ ) {
55
+ const claudeVersionValue = claudeVersionImpl();
56
+ const plugins = claudeVersionValue ? listPluginsImpl() : [];
57
+ const marketplaces = claudeVersionValue ? listPluginMarketplacesImpl() : [];
58
+ const mcps = claudeVersionValue ? listMcpsImpl() : [];
59
+ const userMcpConfig = claudeVersionValue ? readUserMcpConfigImpl() : new Map();
60
+ const runtimeStatus = plugins.some(
61
+ (plugin) => plugin.name === "claude-mem" && plugin.status === "enabled"
62
+ )
63
+ ? ensureClaudeMemRuntimesImpl()
64
+ : null;
65
+
66
+ return {
67
+ claudeVersionValue,
68
+ nodeVersion: process.version,
69
+ plugins,
70
+ marketplaces,
71
+ mcps,
72
+ userMcpConfig,
73
+ runtimeStatus,
74
+ cwd,
75
+ projectState: await readProjectStateImpl(cwd),
76
+ };
77
+ }
78
+
79
+ export function renderReportLines(lines, { logImpl = log } = {}) {
80
+ for (const line of lines) {
81
+ if (line.level === "ok") {
82
+ logImpl.ok(line.text);
83
+ } else if (line.level === "warn") {
84
+ logImpl.warn(line.text);
85
+ } else if (line.level === "err") {
86
+ logImpl.err(line.text);
87
+ } else {
88
+ logImpl.info(line.text);
89
+ }
90
+
91
+ for (const detail of line.details || []) {
92
+ console.log(color.dim(` → ${detail}`));
93
+ }
94
+ }
95
+ }
96
+
97
+ export function printDoctorSummary(
98
+ report,
99
+ { logImpl = log, exitImpl = process.exit } = {}
100
+ ) {
101
+ console.log();
102
+ if (report.errors > 0) {
103
+ console.log(color.red(`Summary: ${report.errors} error(s), ${report.warnings} warning(s)`));
104
+ console.log(color.dim("Fix errors and re-run curdx-flow doctor"));
105
+ exitImpl(1);
106
+ return;
107
+ }
108
+
109
+ if (report.warnings > 0) {
110
+ console.log(color.yellow(`Summary: ${report.warnings} warning(s). Usable, but worth addressing.`));
111
+ return;
112
+ }
113
+
114
+ console.log(color.green("Summary: all healthy ✓"));
115
+ }
116
+
117
+ export function printVerboseDoctorDetails({ runSyncImpl = runSync } = {}) {
118
+ console.log(`\n${color.bold("Details:")}`);
119
+ console.log(color.dim(" Plugins raw:"));
120
+ console.log(runSyncImpl("claude", ["plugin", "list"]).stdout);
121
+ }
package/cli/doctor.js CHANGED
@@ -2,115 +2,35 @@
2
2
  * doctor command — external health check (no need to enter Claude Code).
3
3
  */
4
4
 
5
- import fs from "node:fs/promises";
6
- import path from "node:path";
7
-
8
5
  import {
9
6
  color,
10
7
  log,
11
- runSync,
12
- claudeVersion,
13
- listPlugins,
14
- listPluginMarketplaces,
15
- listMcps,
16
- ensureClaudeMemRuntimes,
17
- readUserMcpConfig,
18
8
  } from "./utils.js";
19
9
  import { buildDoctorReport } from "./lib/doctor-report.js";
10
+ import {
11
+ collectDoctorData,
12
+ createDoctorContext,
13
+ printDoctorSummary,
14
+ printVerboseDoctorDetails,
15
+ renderReportLines,
16
+ } from "./doctor-workflow.js";
20
17
 
21
18
  export async function doctor(args = []) {
22
- const verbose = args.includes("--verbose") || args.includes("-v");
23
- const claudeVersionValue = claudeVersion();
24
- const plugins = claudeVersionValue ? listPlugins() : [];
25
- const marketplaces = claudeVersionValue ? listPluginMarketplaces() : [];
26
- const mcps = claudeVersionValue ? listMcps() : [];
27
- const userMcpConfig = claudeVersionValue ? readUserMcpConfig() : new Map();
28
- const runtimeStatus = plugins.some((plugin) => plugin.name === "claude-mem" && plugin.status === "enabled")
29
- ? ensureClaudeMemRuntimes()
30
- : null;
19
+ const context = createDoctorContext(args);
31
20
 
32
21
  log.title("🏥 CurdX-Flow Health Check");
33
22
 
34
- const report = buildDoctorReport({
35
- claudeVersionValue,
36
- nodeVersion: process.version,
37
- plugins,
38
- marketplaces,
39
- mcps,
40
- userMcpConfig,
41
- runtimeStatus,
42
- cwd: process.cwd(),
43
- projectState: await readProjectState(),
44
- });
23
+ const doctorData = await collectDoctorData();
24
+ const report = buildDoctorReport(doctorData);
45
25
 
46
- renderLines(report.lines);
26
+ renderReportLines(report.lines);
47
27
  for (const section of report.sections) {
48
28
  console.log(`\n${color.bold(section.title)}`);
49
- renderLines(section.lines);
50
- }
51
-
52
- printSummary(report);
53
- if (verbose && claudeVersionValue) {
54
- printVerboseDetails();
55
- }
56
- }
57
-
58
- async function readProjectState() {
59
- const cwd = process.cwd();
60
- const flowDir = path.join(cwd, ".flow");
61
- try {
62
- const stat = await fs.stat(flowDir);
63
- if (!stat.isDirectory()) {
64
- return { exists: false, activeSpec: null };
65
- }
66
-
67
- try {
68
- const activeSpec = await fs.readFile(path.join(flowDir, ".active-spec"), "utf-8");
69
- return { exists: true, activeSpec: activeSpec.trim() };
70
- } catch {
71
- return { exists: true, activeSpec: null };
72
- }
73
- } catch {
74
- return { exists: false, activeSpec: null };
75
- }
76
- }
77
-
78
- function renderLines(lines) {
79
- for (const line of lines) {
80
- if (line.level === "ok") {
81
- log.ok(line.text);
82
- } else if (line.level === "warn") {
83
- log.warn(line.text);
84
- } else if (line.level === "err") {
85
- log.err(line.text);
86
- } else {
87
- log.info(line.text);
88
- }
89
-
90
- for (const detail of line.details || []) {
91
- console.log(color.dim(` → ${detail}`));
92
- }
29
+ renderReportLines(section.lines);
93
30
  }
94
- }
95
31
 
96
- function printSummary(report) {
97
- console.log();
98
- if (report.errors > 0) {
99
- console.log(color.red(`Summary: ${report.errors} error(s), ${report.warnings} warning(s)`));
100
- console.log(color.dim("Fix errors and re-run curdx-flow doctor"));
101
- process.exit(1);
32
+ printDoctorSummary(report);
33
+ if (context.verbose && doctorData.claudeVersionValue) {
34
+ printVerboseDoctorDetails();
102
35
  }
103
-
104
- if (report.warnings > 0) {
105
- console.log(color.yellow(`Summary: ${report.warnings} warning(s). Usable, but worth addressing.`));
106
- return;
107
- }
108
-
109
- console.log(color.green("Summary: all healthy ✓"));
110
- }
111
-
112
- function printVerboseDetails() {
113
- console.log(`\n${color.bold("Details:")}`);
114
- console.log(color.dim(" Plugins raw:"));
115
- console.log(runSync("claude", ["plugin", "list"]).stdout);
116
36
  }
@@ -5,6 +5,46 @@ import {
5
5
  removePluginMarketplace,
6
6
  } from "./lib/claude-ops.js";
7
7
 
8
+ const CURDX_FLOW_INSTALL_ENTRY = {
9
+ scope: "user",
10
+ installSpec: "curdx-flow@curdx-flow-marketplace",
11
+ };
12
+
13
+ export function resolveCurdxFlowInstallPlan({ prevCurdxFlow, shippedVersion }) {
14
+ if (prevCurdxFlow && shippedVersion && prevCurdxFlow.version === shippedVersion) {
15
+ return {
16
+ intro: `curdx-flow already at v${prevCurdxFlow.version}, re-registering...`,
17
+ success: `curdx-flow re-registered at v${shippedVersion}`,
18
+ };
19
+ }
20
+
21
+ if (prevCurdxFlow && shippedVersion) {
22
+ return {
23
+ intro: `curdx-flow v${prevCurdxFlow.version} → v${shippedVersion}, installing...`,
24
+ success: `curdx-flow upgraded to v${shippedVersion}`,
25
+ };
26
+ }
27
+
28
+ if (prevCurdxFlow) {
29
+ return {
30
+ intro: `curdx-flow v${prevCurdxFlow.version} detected, re-registering...`,
31
+ success: "curdx-flow re-registered",
32
+ };
33
+ }
34
+
35
+ if (shippedVersion) {
36
+ return {
37
+ intro: null,
38
+ success: `curdx-flow v${shippedVersion} installed`,
39
+ };
40
+ }
41
+
42
+ return {
43
+ intro: null,
44
+ success: "curdx-flow installed",
45
+ };
46
+ }
47
+
8
48
  export async function addCurdxMarketplace({ marketplaceSource, marketplaceLabel, useOffline }) {
9
49
  log.blank();
10
50
  log.step(2, 5, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
@@ -24,72 +64,33 @@ export async function addCurdxMarketplace({ marketplaceSource, marketplaceLabel,
24
64
  }
25
65
  }
26
66
 
27
- export async function installCurdxFlowPlugin({ prevCurdxFlow, shippedVersion }) {
28
- log.blank();
29
- log.step(3, 5, "Installing curdx-flow plugin...");
67
+ export async function installCurdxFlowPlugin(
68
+ { prevCurdxFlow, shippedVersion },
69
+ {
70
+ installPluginImpl = installPlugin,
71
+ exitImpl = process.exit,
72
+ logImpl = log,
73
+ } = {}
74
+ ) {
75
+ logImpl.blank();
76
+ logImpl.step(3, 5, "Installing curdx-flow plugin...");
30
77
 
31
78
  // Use the pre-Step-2 snapshot — by this point `claude plugin marketplace
32
79
  // remove` has already evicted the plugin, so listPlugins() here would
33
80
  // always return undefined for curdx-flow and we'd mis-report "installed"
34
81
  // when we actually upgraded (the bug reported by @wdx's beta.14 log).
35
- const already = prevCurdxFlow;
82
+ const plan = resolveCurdxFlowInstallPlan({ prevCurdxFlow, shippedVersion });
36
83
 
37
- if (already && shippedVersion && already.version === shippedVersion) {
38
- // Step 2 removes and re-adds the marketplace to rebind it to the current
39
- // source. Claude Code removes plugins installed from that marketplace as
40
- // part of `marketplace remove`, so even a same-version install must be
41
- // re-registered here.
42
- log.info(
43
- `curdx-flow already at v${already.version}, re-registering...`
44
- );
45
- const r = await installPlugin({
46
- scope: "user",
47
- installSpec: "curdx-flow@curdx-flow-marketplace",
48
- });
49
- if (r.code !== 0) {
50
- log.err(`Install failed: ${resultOutput(r)}`);
51
- process.exit(1);
52
- }
53
- log.ok(`curdx-flow re-registered at v${shippedVersion}`);
54
- } else if (already && shippedVersion) {
55
- log.info(
56
- `curdx-flow v${already.version} → v${shippedVersion}, installing...`
57
- );
58
- const r = await installPlugin({
59
- scope: "user",
60
- installSpec: "curdx-flow@curdx-flow-marketplace",
61
- });
62
- if (r.code !== 0) {
63
- log.err(`Install failed: ${resultOutput(r)}`);
64
- process.exit(1);
65
- }
66
- log.ok(`curdx-flow upgraded to v${shippedVersion}`);
67
- } else if (already) {
68
- log.info(
69
- `curdx-flow v${already.version} detected, re-registering...`
70
- );
71
- const r = await installPlugin({
72
- scope: "user",
73
- installSpec: "curdx-flow@curdx-flow-marketplace",
74
- });
75
- if (r.code !== 0) {
76
- log.err(`Install failed: ${resultOutput(r)}`);
77
- process.exit(1);
78
- }
79
- log.ok("curdx-flow re-registered");
80
- } else {
81
- const r = await installPlugin({
82
- scope: "user",
83
- installSpec: "curdx-flow@curdx-flow-marketplace",
84
- });
85
- if (r.code !== 0) {
86
- log.err(`Install failed: ${resultOutput(r)}`);
87
- process.exit(1);
88
- }
89
- if (shippedVersion) {
90
- log.ok(`curdx-flow v${shippedVersion} installed`);
91
- } else {
92
- log.ok("curdx-flow installed");
93
- }
84
+ if (plan.intro) {
85
+ logImpl.info(plan.intro);
86
+ }
87
+
88
+ const result = await installPluginImpl(CURDX_FLOW_INSTALL_ENTRY);
89
+ if (result.code !== 0) {
90
+ logImpl.err(`Install failed: ${resultOutput(result)}`);
91
+ exitImpl(1);
92
+ return;
94
93
  }
94
+
95
+ logImpl.ok(plan.success);
95
96
  }
@@ -3,6 +3,98 @@ import { join } from "node:path";
3
3
 
4
4
  import { log, run, runSync, VERSION } from "./utils.js";
5
5
 
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
+ }
97
+
6
98
  /**
7
99
  * Check for CLI updates and auto-update if available.
8
100
  * @returns {Promise<{updated: boolean, version?: string}>}
@@ -31,9 +123,7 @@ export async function checkAndUpdateSelf() {
31
123
  return { updated: false };
32
124
  }
33
125
 
34
- // Existing behavior used simple string comparison; keep it unchanged for
35
- // this structural refactor.
36
- if (latestVersion > currentVersion) {
126
+ if (isVersionNewer(latestVersion, currentVersion)) {
37
127
  log.info(`New version available: v${currentVersion} → v${latestVersion}`);
38
128
  log.info("Updating CLI...");
39
129