@curdx/flow 7.1.5 → 7.1.7

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.
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/lib/check-verification-blocks.ts
4
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
5
+ import path from "path";
6
+ var VERIFICATION_PHASES = [
7
+ "research",
8
+ "requirements",
9
+ "design",
10
+ "tasks",
11
+ "execution"
12
+ ];
13
+ async function runVerificationCheck(opts = {}) {
14
+ const repoRoot = opts.repoRoot ?? process.cwd();
15
+ const env = opts.env ?? process.env;
16
+ const specsDir = path.join(repoRoot, "specs");
17
+ const specDir = resolveActiveSpecDir(specsDir);
18
+ if (!specDir) {
19
+ return {
20
+ ok: true,
21
+ code: 0,
22
+ skipped: true,
23
+ message: "check-verification-blocks: no active spec found, skipping.\n"
24
+ };
25
+ }
26
+ if (env.CURDX_VERIFY_SKIP_BLOCKS === "1") {
27
+ return {
28
+ ok: true,
29
+ code: 0,
30
+ skipped: true,
31
+ specDir,
32
+ message: "[check-verification-blocks] CURDX_VERIFY_SKIP_BLOCKS=1 \u2014 skipping gate.\n"
33
+ };
34
+ }
35
+ const stateFile = path.join(specDir, ".curdx-state.json");
36
+ let state;
37
+ try {
38
+ state = JSON.parse(readFileSync(stateFile, "utf8"));
39
+ } catch (err) {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ return {
42
+ ok: false,
43
+ code: 2,
44
+ specDir,
45
+ message: `\u2717 failed to read ${path.relative(repoRoot, stateFile)}: ${msg}
46
+ `
47
+ };
48
+ }
49
+ if (typeof state !== "object" || state === null || !("verificationBlocks" in state)) {
50
+ const rel2 = path.relative(repoRoot, specDir);
51
+ return {
52
+ ok: true,
53
+ code: 0,
54
+ skipped: true,
55
+ specDir,
56
+ message: `[check-verification-blocks] No verificationBlocks defined \u2014 skipping (treat as initial state)
57
+ Active spec: ${rel2}
58
+ `
59
+ };
60
+ }
61
+ const blocks = state.verificationBlocks;
62
+ const blocksObj = blocks && typeof blocks === "object" && !Array.isArray(blocks) ? blocks : null;
63
+ const presentPhases = blocksObj ? Object.keys(blocksObj).filter(
64
+ (p) => blocksObj[p] !== void 0 && blocksObj[p] !== null
65
+ ) : [];
66
+ if (!blocksObj || presentPhases.length === 0) {
67
+ const rel2 = path.relative(repoRoot, specDir);
68
+ return {
69
+ ok: false,
70
+ code: 2,
71
+ specDir,
72
+ message: `\u2717 No verificationBlocks found. Run the appropriate phase verification command.
73
+ Active spec: ${rel2}
74
+ Hint: each phase must record an entry in .curdx-state.json::verificationBlocks
75
+ (see plugins/curdx-flow/references/iron-law-verification.md).
76
+ `
77
+ };
78
+ }
79
+ const failures = [];
80
+ for (const phase of presentPhases) {
81
+ if (!VERIFICATION_PHASES.includes(phase)) {
82
+ failures.push({
83
+ phase,
84
+ reason: `unknown phase key "${phase}"`,
85
+ command: "(remove from state)"
86
+ });
87
+ continue;
88
+ }
89
+ const raw = blocksObj[phase];
90
+ if (typeof raw !== "object" || raw === null) {
91
+ failures.push({
92
+ phase,
93
+ reason: "block is not an object",
94
+ command: "(rewrite block)"
95
+ });
96
+ continue;
97
+ }
98
+ const block = raw;
99
+ const command = typeof block.command === "string" ? block.command : "(unknown command)";
100
+ const exitCode = block.exitCode;
101
+ const timestamp = block.timestamp;
102
+ const srcMtime = block.srcMtime;
103
+ const failedReason = block.failedReason;
104
+ if (exitCode !== 0) {
105
+ failures.push({
106
+ phase,
107
+ reason: typeof failedReason === "string" && failedReason.length > 0 ? `verification failed: ${failedReason} (exitCode=${String(exitCode)})` : `verification failed (exitCode=${String(exitCode)})`,
108
+ command
109
+ });
110
+ continue;
111
+ }
112
+ const ts = typeof timestamp === "string" ? Date.parse(timestamp) : NaN;
113
+ if (Number.isNaN(ts)) {
114
+ failures.push({
115
+ phase,
116
+ reason: `invalid timestamp "${String(timestamp)}"`,
117
+ command
118
+ });
119
+ continue;
120
+ }
121
+ if (typeof srcMtime !== "number" || !Number.isFinite(srcMtime) || srcMtime < 0) {
122
+ failures.push({
123
+ phase,
124
+ reason: `invalid srcMtime ${String(srcMtime)}`,
125
+ command
126
+ });
127
+ continue;
128
+ }
129
+ if (ts < srcMtime) {
130
+ const srcIso = new Date(srcMtime).toISOString();
131
+ failures.push({
132
+ phase,
133
+ reason: `stale evidence: src changed at ${srcIso}, last verified at ${String(timestamp)}`,
134
+ command
135
+ });
136
+ }
137
+ }
138
+ if (failures.length > 0) {
139
+ const rel2 = path.relative(repoRoot, specDir);
140
+ let message = "\u2717 verificationBlocks gate failed:\n";
141
+ message += ` Active spec: ${rel2}
142
+ `;
143
+ for (const f of failures) {
144
+ message += ` - phase "${f.phase}": ${f.reason}
145
+ `;
146
+ message += ` Re-run: ${f.command}
147
+ `;
148
+ }
149
+ message += "\n";
150
+ message += "See plugins/curdx-flow/references/iron-law-verification.md for the full checklist.\n";
151
+ return { ok: false, code: 2, specDir, message };
152
+ }
153
+ const rel = path.relative(repoRoot, specDir);
154
+ return {
155
+ ok: true,
156
+ code: 0,
157
+ specDir,
158
+ message: `All verificationBlocks valid.
159
+ Active spec: ${rel}
160
+ Phases verified: ${presentPhases.join(", ")}
161
+ `
162
+ };
163
+ }
164
+ function resolveActiveSpecDir(specsDir) {
165
+ const pointer = path.join(specsDir, ".current-spec");
166
+ if (existsSync(pointer)) {
167
+ try {
168
+ const name = readFileSync(pointer, "utf8").trim();
169
+ if (name) {
170
+ const dir = path.join(specsDir, name);
171
+ if (existsSync(path.join(dir, ".curdx-state.json"))) return dir;
172
+ }
173
+ } catch {
174
+ }
175
+ }
176
+ if (!existsSync(specsDir)) return null;
177
+ let entries;
178
+ try {
179
+ entries = readdirSync(specsDir, { withFileTypes: true });
180
+ } catch {
181
+ return null;
182
+ }
183
+ let latest = null;
184
+ let latestMtime = 0;
185
+ for (const e of entries) {
186
+ if (!e.isDirectory()) continue;
187
+ if (e.name.startsWith(".") || e.name.startsWith("_")) continue;
188
+ const stateFile = path.join(specsDir, e.name, ".curdx-state.json");
189
+ if (!existsSync(stateFile)) continue;
190
+ try {
191
+ const st = statSync(stateFile);
192
+ if (st.mtimeMs > latestMtime) {
193
+ latestMtime = st.mtimeMs;
194
+ latest = path.join(specsDir, e.name);
195
+ }
196
+ } catch {
197
+ }
198
+ }
199
+ return latest;
200
+ }
201
+
202
+ // src/cli/commands/check.ts
203
+ async function runCheckCommand(args) {
204
+ void args;
205
+ const result = await runVerificationCheck();
206
+ if (result.ok) {
207
+ process.stdout.write("All verificationBlocks valid.\n");
208
+ if (result.specDir !== void 0) {
209
+ const rest = result.message.replace(
210
+ /^All verificationBlocks valid\.\n/,
211
+ ""
212
+ );
213
+ if (rest.length > 0) process.stderr.write(rest);
214
+ }
215
+ process.exit(0);
216
+ }
217
+ process.stderr.write(result.message);
218
+ process.exit(2);
219
+ }
220
+ export {
221
+ runCheckCommand
222
+ };
package/dist/index.mjs CHANGED
@@ -851,110 +851,61 @@ var BLOCK_RE = /<!-- BEGIN @curdx\/flow v\d+[^>]*-->[\s\S]*?<!-- END @curdx\/flo
851
851
  function claudeMdPath() {
852
852
  return path4.join(os2.homedir(), ".claude", "CLAUDE.md");
853
853
  }
854
- function isZh() {
855
- return getLang() === "zh";
856
- }
857
854
  function buildCombinationPatterns(ids) {
858
855
  const has = (k) => ids.has(k);
859
856
  const out = [
860
- isZh() ? "\u6309\u573A\u666F\u4E32\u8054\uFF0C\u4F18\u5148\u6309\u80FD\u529B\u8FB9\u754C\u8C03\u7528\uFF1Aslash command\u3001MCP\u3001\u63D2\u4EF6 skill \u4E0D\u8981\u6DF7\u5199\u3002" : "Combine tools by capability. Keep slash commands, MCP tools, and plugin skills distinct.",
857
+ "Combine tools by capability. Keep slash commands, MCP tools, and plugin skills distinct.",
861
858
  ""
862
859
  ];
863
- if (has("claude-mem") || has("context7") || has("curdx-flow")) {
864
- out.push(isZh() ? "- **\u63A5\u5230\u65B0\u9700\u6C42 / \u65B0 feature**" : "- **Starting a new feature**");
860
+ if (has("context7") || has("curdx-flow") || has("claude-mem")) {
861
+ out.push("- **Starting a new feature**");
865
862
  let step = 1;
866
- if (has("claude-mem")) {
867
- out.push(
868
- isZh() ? ` ${step++}. \u5148\u7528 \`/claude-mem:mem-search\` \u67E5\u5386\u53F2\uFF0C\u786E\u8BA4\u4E4B\u524D\u6709\u6CA1\u6709\u7C7B\u4F3C\u5B9E\u73B0\u3001\u8E29\u5751\u6216\u51B3\u7B56\u3002` : ` ${step++}. Start with \`/claude-mem:mem-search\` to check whether this work, decision, or pitfall already exists in memory.`
869
- );
870
- }
871
863
  if (has("context7")) {
872
- out.push(
873
- isZh() ? ` ${step++}. \u6D89\u53CA\u5916\u90E8\u5E93\u3001SDK\u3001\u6846\u67B6\u6216 API \u65F6\uFF0C\u4F7F\u7528 Context7 MCP \u62C9\u5B98\u65B9\u6700\u65B0\u6587\u6863\u3002` : ` ${step++}. If external libraries, SDKs, frameworks, or APIs are involved, use the Context7 MCP to pull current official docs.`
874
- );
864
+ out.push(` ${step++}. If external libraries, SDKs, frameworks, or APIs are involved, use the Context7 MCP to pull current official docs.`);
875
865
  }
876
866
  const planners = [];
877
- if (has("claude-mem")) {
878
- planners.push(
879
- isZh() ? "`/claude-mem:make-plan` \u4EA7\u51FA\u5206\u9636\u6BB5\u8BA1\u5212" : "`/claude-mem:make-plan` for a phased plan"
880
- );
881
- }
882
- if (has("curdx-flow")) {
883
- planners.push(
884
- isZh() ? "`/curdx-flow:new` \u6216\u76F8\u5173 spec flow \u8D77\u5B8C\u6574\u89C4\u683C" : "`/curdx-flow:new` or the spec flow for a full specification"
885
- );
886
- }
867
+ if (has("claude-mem")) planners.push("`/claude-mem:make-plan` for a phased plan");
868
+ if (has("curdx-flow")) planners.push("`/curdx-flow:new` or the spec flow for a full specification");
887
869
  if (planners.length > 0) {
888
- out.push(
889
- isZh() ? ` ${step++}. \u591A\u6B65\u9AA4\u3001\u9AD8\u4E0D\u786E\u5B9A\u6027\u3001\u8DE8\u6A21\u5757\u65F6\uFF0C\u518D\u8FDB\u5165 ${planners.join("\uFF0C\u6216 ")}\u3002` : ` ${step++}. Only move into ${planners.join(" or ")} when the work is multi-step, cross-cutting, or uncertain.`
890
- );
870
+ out.push(` ${step++}. Only move into ${planners.join(" or ")} when the work is multi-step, cross-cutting, or uncertain.`);
891
871
  }
892
- out.push(
893
- isZh() ? ` ${step++}. \u7B80\u5355\u3001\u8303\u56F4\u660E\u786E\u7684\u4E00\u6B21\u6027\u6539\u52A8\uFF0C\u76F4\u63A5\u505A\uFF0C\u4E0D\u8981\u5148\u628A\u6D41\u7A0B\u62C9\u6EE1\u3002` : ` ${step++}. For small, clear one-shot changes, implement directly instead of forcing the full workflow.`
894
- );
872
+ out.push(` ${step++}. For small, clear one-shot changes, implement directly instead of forcing the full workflow.`);
895
873
  out.push("");
896
874
  }
897
875
  const stuckLines = [];
898
876
  let s = 1;
899
- if (has("claude-mem")) {
900
- stuckLines.push(
901
- isZh() ? ` ${s++}. \u5148\u7528 \`/claude-mem:mem-search\` \u67E5\u8BB0\u5FC6\uFF0C\u786E\u8BA4\u662F\u4E0D\u662F\u4EE5\u524D\u89E3\u8FC7\u540C\u7C7B bug\u3002` : ` ${s++}. Check \`/claude-mem:mem-search\` first to see whether the same bug was solved before.`
902
- );
903
- }
904
877
  if (has("chrome-devtools-mcp")) {
905
- stuckLines.push(
906
- isZh() ? ` ${s++}. \u6D4F\u89C8\u5668\u4FA7\u95EE\u9898\u4F7F\u7528 Chrome DevTools MCP\uFF1A\u770B network\u3001console\u3001performance\u3001DOM snapshot\u3002` : ` ${s++}. For browser-side issues, use the Chrome DevTools MCP for network, console, performance, and DOM snapshots.`
907
- );
878
+ stuckLines.push(` ${s++}. For browser-side issues, use the Chrome DevTools MCP for network, console, performance, and DOM snapshots.`);
908
879
  }
909
880
  if (has("context7")) {
910
- stuckLines.push(
911
- isZh() ? ` ${s++}. \u5982\u679C\u6000\u7591\u662F\u5E93 / API \u884C\u4E3A\u53D8\u5316\uFF0C\u4F7F\u7528 Context7 MCP \u67E5\u5B98\u65B9\u6587\u6863\uFF0C\u4E0D\u8981\u51ED\u8BB0\u5FC6\u3002` : ` ${s++}. If the issue may come from library or API behavior, use the Context7 MCP instead of relying on memory.`
912
- );
881
+ stuckLines.push(` ${s++}. If the issue may come from library or API behavior, use the Context7 MCP instead of relying on memory.`);
913
882
  }
914
883
  const stillStuck = [];
915
- if (has("sequential-thinking")) {
916
- stillStuck.push(
917
- isZh() ? "\u4F7F\u7528 sequential-thinking MCP \u62C6\u5047\u8BBE" : "use the sequential-thinking MCP to break down hypotheses"
918
- );
919
- }
920
- if (has("pua")) {
921
- stillStuck.push(
922
- isZh() ? "\u518D\u8FDB\u5165 `/pua:pua-loop` \u8FED\u4EE3" : "then enter `/pua:pua-loop` for structured retries"
923
- );
924
- }
884
+ if (has("sequential-thinking")) stillStuck.push("switch to the sequential-thinking MCP to break down hypotheses");
885
+ if (has("pua")) stillStuck.push("enter `/pua:pua-loop` for structured retries");
925
886
  if (stillStuck.length > 0) {
926
- stuckLines.push(
927
- isZh() ? ` ${s++}. \u4E24\u8F6E\u4EE5\u4E0A\u4ECD\u5361\u4F4F\uFF0C\u518D ${stillStuck.join("\uFF0C\u6216 ")}\u3002` : ` ${s++}. If you are still stuck after multiple attempts, ${stillStuck.join(" or ")}.`
928
- );
887
+ stuckLines.push(` ${s++}. If you are still stuck after multiple attempts, ${stillStuck.join(" or ")}.`);
929
888
  }
930
889
  if (stuckLines.length > 0) {
931
- out.push(isZh() ? "- **\u9047\u5230 bug / \u8FDE\u7EED\u5361\u4F4F**" : "- **Debugging and repeated failures**", ...stuckLines, "");
890
+ out.push("- **Debugging and repeated failures**", ...stuckLines, "");
932
891
  }
933
892
  if (has("frontend-design") || has("chrome-devtools-mcp")) {
934
- out.push(isZh() ? "- **\u505A UI / \u524D\u7AEF\u9875\u9762**" : "- **UI and frontend work**");
893
+ out.push("- **UI and frontend work**");
935
894
  if (has("frontend-design")) {
936
- out.push(
937
- isZh() ? " - \u4F18\u5148\u4F7F\u7528 `frontend-design` \u63D2\u4EF6\u7684 UI skills\uFF1B\u82E5\u672A\u81EA\u52A8\u89E6\u53D1\uFF0C\u518D\u663E\u5F0F\u8C03\u7528\u5BF9\u5E94 skill\u3002" : " - Prioritize the `frontend-design` plugin skills for UI work; if they do not trigger automatically, invoke the relevant skill explicitly."
938
- );
895
+ out.push(" - Prioritize the `frontend-design` plugin skills for UI work; if they do not trigger automatically, invoke the relevant skill explicitly.");
939
896
  }
940
897
  if (has("chrome-devtools-mcp")) {
941
- out.push(
942
- isZh() ? " - \u6E32\u67D3\u5F02\u5E38\u3001\u4EA4\u4E92\u95EE\u9898\u6216\u89C6\u89C9\u56DE\u5F52\uFF0C\u4F7F\u7528 Chrome DevTools MCP \u9A8C\u8BC1\uFF0C\u4E0D\u8981\u53EA\u9760\u8089\u773C\u731C\u3002" : " - For rendering issues, interaction bugs, or visual regressions, verify with the Chrome DevTools MCP instead of relying on visual guesswork alone."
943
- );
898
+ out.push(" - For rendering issues, interaction bugs, or visual regressions, verify with the Chrome DevTools MCP instead of relying on visual guesswork alone.");
944
899
  }
945
900
  out.push("");
946
901
  }
947
902
  if (has("pua") || has("curdx-flow")) {
948
- out.push(isZh() ? "- **\u5927\u578B / \u8DE8\u6A21\u5757 / \u591A agent \u534F\u4F5C**" : "- **Large, cross-cutting, or multi-agent work**");
903
+ out.push("- **Large, cross-cutting, or multi-agent work**");
949
904
  if (has("pua")) {
950
- out.push(
951
- isZh() ? " - \u9700\u8981\u5E76\u884C\u62C6\u89E3\u4E0E\u56E2\u961F\u534F\u4F5C\u65F6\uFF0C\u7528 `/pua:p9`\uFF1B\u66F4\u9AD8\u5C42\u6218\u7565\u89C4\u5212\u518D\u8003\u8651 `/pua:p10`\u3002" : " - Use `/pua:p9` for parallel task decomposition and team coordination; reserve `/pua:p10` for higher-level strategy work."
952
- );
905
+ out.push(" - Use `/pua:p9` for parallel task decomposition and team coordination; reserve `/pua:p10` for higher-level strategy work.");
953
906
  }
954
907
  if (has("curdx-flow")) {
955
- out.push(
956
- isZh() ? " - \u4E00\u4E2A\u5927\u529F\u80FD\u9700\u8981\u62C6\u6210\u591A\u4E2A\u76F8\u4E92\u4F9D\u8D56\u7684\u89C4\u683C\u65F6\uFF0C\u4F7F\u7528 `/curdx-flow:triage`\u3002" : " - Use `/curdx-flow:triage` when one large feature needs to be split into multiple dependent specs."
957
- );
908
+ out.push(" - Use `/curdx-flow:triage` when one large feature needs to be split into multiple dependent specs.");
958
909
  }
959
910
  }
960
911
  while (out.length > 0 && out[out.length - 1] === "") out.pop();
@@ -963,46 +914,32 @@ function buildCombinationPatterns(ids) {
963
914
  function buildSkipRules(ids) {
964
915
  const has = (k) => ids.has(k);
965
916
  const out = [];
966
- out.push(
967
- isZh() ? "- \u4E00\u884C\u6539\u52A8\u3001typo\u3001\u7EAF\u91CD\u547D\u540D\u53D8\u91CF\uFF1A\u4E0D\u8981\u5148 plan\uFF0C\u4E0D\u8981\u5148\u5F00 spec\uFF0C\u76F4\u63A5\u6539\u3002" : "- For one-line changes, typos, or pure renames, skip planning and spec flow. Just make the edit."
968
- );
917
+ out.push("- For one-line changes, typos, or pure renames, skip planning and spec flow. Just make the edit.");
969
918
  const skips = [];
970
- if (has("pua")) skips.push("`/pua:pua`");
919
+ if (has("pua")) skips.push("`/pua:pua-loop`");
971
920
  if (has("sequential-thinking")) skips.push("`sequential-thinking`");
972
921
  if (skips.length > 0) {
973
- out.push(
974
- isZh() ? `- \u5DF2\u77E5\u786E\u5B9A\u7684 fix\uFF1A\u4E0D\u8981\u5148\u4E0A ${skips.join("\u3001")}\u3002` : `- For a known, deterministic fix, do not reach for ${skips.join(" or ")} first.`
975
- );
922
+ out.push(`- For a known, deterministic fix, do not reach for ${skips.join(" or ")} first.`);
976
923
  }
977
- out.push(
978
- isZh() ? "- \u7EAF\u6982\u5FF5\u89E3\u91CA\u9898\u53EF\u4EE5\u76F4\u63A5\u7B54\uFF1B\u5982\u679C\u662F\u5728\u95EE\u5F53\u524D\u4ED3\u5E93\u91CC\u7684\u4EE3\u7801\u542B\u4E49\uFF0C\u5148\u8BFB\u76F8\u5173\u6587\u4EF6\u518D\u89E3\u91CA\u3002" : "- Answer pure conceptual explanation questions directly. If the question is about code in this repository, read the relevant files first."
979
- );
924
+ out.push("- Answer pure conceptual explanation questions directly. If the question is about code in this repository, read the relevant files first.");
980
925
  if (has("curdx-flow")) {
981
- out.push(
982
- isZh() ? "- \u5355\u6587\u4EF6\u5C40\u90E8\u91CD\u6784\u6216\u5F88\u5C0F\u8303\u56F4\u7684\u6574\u7406\uFF1A\u901A\u5E38\u4E0D\u8981\u8FDB\u5165 curdx-flow spec \u6D41\u3002" : "- For a single-file refactor or a very local cleanup, usually do not enter the curdx-flow spec workflow."
983
- );
926
+ out.push("- For a single-file refactor or a very local cleanup, usually do not enter the curdx-flow spec workflow.");
984
927
  }
985
928
  return out;
986
929
  }
987
930
  function buildDecisionTree(ids) {
988
931
  const has = (k) => ids.has(k);
989
932
  const out = [];
990
- out.push(isZh() ? "1. \u80FD 1\u20132 \u6B65\u641E\u5B9A\uFF1F\u2192 \u76F4\u63A5\u505A\u3002" : "1. Can it be finished in 1-2 steps? -> Do it directly.");
991
- out.push(
992
- isZh() ? "2. \u591A\u6B65\u9AA4\u4F46\u8DEF\u5F84\u6E05\u6670\uFF1F\u2192 \u62C6\u6210\u7B80\u77ED\u4EFB\u52A1\u5217\u8868\u9010\u4E2A\u63A8\u8FDB\uFF0C\u4E0D\u8981\u9ED8\u8BA4\u8FDB\u5165\u5B8C\u6574 spec \u6D41\u3002" : "2. Is it multi-step but still clear? -> Break it into a short task list and execute without defaulting to the full spec flow."
993
- );
933
+ out.push("1. Can it be finished in 1-2 steps? -> Do it directly.");
934
+ out.push("2. Is it multi-step but still clear? -> Break it into a short task list and execute without defaulting to the full spec flow.");
994
935
  const planners = [];
995
936
  if (has("curdx-flow")) planners.push("`/curdx-flow:new`");
996
937
  if (has("claude-mem")) planners.push("`/claude-mem:make-plan`");
997
938
  if (planners.length > 0) {
998
- out.push(
999
- isZh() ? `3. \u9700\u6C42\u6A21\u7CCA\u3001\u8DE8\u6A21\u5757\u3001\u9700\u8981\u5206\u9636\u6BB5\u4EA4\u4ED8\uFF1F\u2192 ${planners.join(" \u6216 ")}\u3002` : `3. Is the request ambiguous, cross-cutting, or phase-based? -> ${planners.join(" or ")}.`
1000
- );
939
+ out.push(`3. Is the request ambiguous, cross-cutting, or phase-based? -> ${planners.join(" or ")}.`);
1001
940
  }
1002
941
  if (has("claude-mem")) {
1003
- out.push(
1004
- isZh() ? "4. \u8FD9\u7C7B\u6D3B\u4EE5\u524D\u53EF\u80FD\u505A\u8FC7\uFF1F\u2192 \u5148 `/claude-mem:mem-search`\u3002" : "4. Might this work have been done before? -> Start with `/claude-mem:mem-search`."
1005
- );
942
+ out.push("4. Might this work have been done before? -> Start with `/claude-mem:mem-search`.");
1006
943
  }
1007
944
  return out;
1008
945
  }
@@ -1016,23 +953,17 @@ function buildLanguagePolicy() {
1016
953
  function renderBlock(items) {
1017
954
  const installedIds = new Set(items.map((i) => i.id));
1018
955
  const sections = [
1019
- ["## Language Policy\uFF08\u8BED\u8A00\u89C4\u5219\uFF09", buildLanguagePolicy()],
1020
- [
1021
- isZh() ? "## Tool Combination Patterns\uFF08\u7EC4\u5408\u5DE5\u4F5C\u6D41\uFF09" : "## Tool Combination Patterns",
1022
- buildCombinationPatterns(installedIds)
1023
- ],
1024
- [isZh() ? "## Skip Rules\uFF08\u9632\u8FC7\u5EA6\u5DE5\u5177\u5316\uFF09" : "## Skip Rules", buildSkipRules(installedIds)],
1025
- [isZh() ? "## Decision Tree\uFF08\u9047\u5230\u6A21\u7CCA\u8BF7\u6C42\u65F6\uFF09" : "## Decision Tree", buildDecisionTree(installedIds)]
956
+ ["## Language Policy", buildLanguagePolicy()],
957
+ ["## Tool Combination Patterns", buildCombinationPatterns(installedIds)],
958
+ ["## Skip Rules", buildSkipRules(installedIds)],
959
+ ["## Decision Tree", buildDecisionTree(installedIds)]
1026
960
  ];
1027
961
  const lines = [BEGIN_MARKER];
1028
962
  for (const [heading, body] of sections) {
1029
963
  if (body.length === 0) continue;
1030
964
  lines.push(heading, "", ...body, "");
1031
965
  }
1032
- lines.push(
1033
- isZh() ? "\u8FD0\u884C `npx @curdx/flow` \u4EE5 install / update / uninstall\u3002" : "Run `npx @curdx/flow` to install / update / uninstall.",
1034
- END_MARKER
1035
- );
966
+ lines.push(END_MARKER);
1036
967
  return lines.join("\n");
1037
968
  }
1038
969
  function withEol(s, eol) {
@@ -1695,17 +1626,49 @@ var analyzeCmd = defineCommand({
1695
1626
  "include-prompts": {
1696
1627
  type: "boolean",
1697
1628
  description: "Skip prompt redaction (D-9 white-list passthrough disabled \u2014 local debugging only)"
1629
+ },
1630
+ session: {
1631
+ type: "string",
1632
+ description: "Filter to single session UUID (matches <uuid>.jsonl in encoded project dir)"
1633
+ },
1634
+ "cost-summary": {
1635
+ type: "boolean",
1636
+ description: "Emit OB-3 cost analytics: totalCost.usd top-level + ## Cost Summary markdown (opt-in, default false)"
1637
+ },
1638
+ "by-spec": {
1639
+ type: "boolean",
1640
+ description: "OB-3 R1: aggregate cost by spec dimension (opt-in, default false; with --cost-summary alone all 3 dims emit)"
1641
+ },
1642
+ "by-phase": {
1643
+ type: "boolean",
1644
+ description: "OB-3 R2: aggregate cost by phase dimension (opt-in, default false; with --cost-summary alone all 3 dims emit)"
1645
+ },
1646
+ "by-task": {
1647
+ type: "boolean",
1648
+ description: "OB-3 R7: aggregate cost by task (correlationId) dimension, top-N truncated by --top (opt-in, default false)"
1649
+ },
1650
+ top: {
1651
+ type: "string",
1652
+ description: "OB-3 R7 top-N truncation for --by-task buckets (default: 10)"
1698
1653
  }
1699
1654
  },
1700
1655
  async run({ args }) {
1701
1656
  const limitRaw = args.limit;
1702
1657
  const limit = typeof limitRaw === "string" && limitRaw.length > 0 ? Number(limitRaw) : void 0;
1703
- const { runAnalyze } = await import("./analyze-4DE3HVCA.mjs");
1658
+ const topRaw = args.top;
1659
+ const top = typeof topRaw === "string" && topRaw.length > 0 ? Number(topRaw) : void 0;
1660
+ const { runAnalyze } = await import("./analyze-FX2PCSL6.mjs");
1704
1661
  await runAnalyze({
1705
1662
  out: typeof args.out === "string" ? args.out : void 0,
1706
1663
  json: Boolean(args.json),
1707
1664
  limit: Number.isFinite(limit) ? limit : void 0,
1708
- includePrompts: Boolean(args["include-prompts"])
1665
+ includePrompts: Boolean(args["include-prompts"]),
1666
+ session: typeof args.session === "string" && args.session.length > 0 ? args.session : void 0,
1667
+ costSummary: Boolean(args["cost-summary"]),
1668
+ bySpec: Boolean(args["by-spec"]),
1669
+ byPhase: Boolean(args["by-phase"]),
1670
+ byTask: Boolean(args["by-task"]),
1671
+ top: Number.isFinite(top) ? top : void 0
1709
1672
  });
1710
1673
  }
1711
1674
  });
@@ -1839,6 +1802,17 @@ var updateCmd = defineCommand2({
1839
1802
  p10.outro(t("app.outro"));
1840
1803
  }
1841
1804
  });
1805
+ var checkCmd = defineCommand2({
1806
+ meta: {
1807
+ name: "check",
1808
+ description: "Verify active spec verificationBlocks (iron-law gate)"
1809
+ },
1810
+ args: {},
1811
+ async run() {
1812
+ const { runCheckCommand } = await import("./check-TJPGCG3Z.mjs");
1813
+ await runCheckCommand([]);
1814
+ }
1815
+ });
1842
1816
  var statusCmd = defineCommand2({
1843
1817
  meta: { name: "status", description: "Show install status" },
1844
1818
  args: {
@@ -1852,7 +1826,27 @@ var statusCmd = defineCommand2({
1852
1826
  if (!args.json) p10.outro(t("app.outro"));
1853
1827
  }
1854
1828
  });
1855
- var SUBCOMMANDS = /* @__PURE__ */ new Set(["install", "uninstall", "update", "status", "analyze"]);
1829
+ var SUBCOMMANDS = /* @__PURE__ */ new Set(["install", "uninstall", "update", "status", "analyze", "check"]);
1830
+ var CHECK_HELP = `USAGE \`@curdx/flow check\`
1831
+
1832
+ DESCRIPTION
1833
+ Verify the active spec's verificationBlocks (iron-law gate) \u2014 the same
1834
+ check enforced by the Stop hook and \`npm run verify\` release gate.
1835
+
1836
+ OPTIONS
1837
+ --help, -h Show this help message and exit.
1838
+
1839
+ EXIT CODES
1840
+ 0 All verificationBlocks valid (or no spec is active \u2014 graceful no-op).
1841
+ 2 At least one phase block is missing, stale, or failed.
1842
+
1843
+ ENV
1844
+ CURDX_VERIFY_SKIP_BLOCKS=1 Skip the gate (human escape hatch; never set
1845
+ in CI / release).
1846
+
1847
+ SEE ALSO
1848
+ plugins/curdx-flow/references/iron-law-verification.md
1849
+ `;
1856
1850
  var root = defineCommand2({
1857
1851
  meta: {
1858
1852
  name: "@curdx/flow",
@@ -1865,7 +1859,8 @@ var root = defineCommand2({
1865
1859
  uninstall: uninstallCmd,
1866
1860
  update: updateCmd,
1867
1861
  status: statusCmd,
1868
- analyze: analyze_default
1862
+ analyze: analyze_default,
1863
+ check: checkCmd
1869
1864
  }
1870
1865
  // No root run() — citty 0.1.6 calls parent.run AFTER a matching subcommand,
1871
1866
  // which would render the menu after a subcommand finishes. We dispatch the
@@ -1899,6 +1894,16 @@ async function runInteractive(argv2) {
1899
1894
  var argv = process.argv.slice(2);
1900
1895
  var first = firstNonFlag(argv);
1901
1896
  assertFreshLocalBuild();
1897
+ if (process.argv[2] === "check") {
1898
+ const rest = process.argv.slice(3);
1899
+ if (rest.includes("--help") || rest.includes("-h")) {
1900
+ process.stdout.write(CHECK_HELP);
1901
+ process.exit(0);
1902
+ }
1903
+ const { runCheckCommand } = await import("./check-TJPGCG3Z.mjs");
1904
+ await runCheckCommand(rest);
1905
+ process.exit(0);
1906
+ }
1902
1907
  if (first === void 0 || first !== void 0 && !SUBCOMMANDS.has(first) && first !== "--help" && first !== "-h") {
1903
1908
  if (first === void 0) {
1904
1909
  runInteractive(argv).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "7.1.5",
3
+ "version": "7.1.7",
4
4
  "description": "Interactive installer for Claude Code plugins and MCP servers",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.mjs",
@@ -21,7 +21,7 @@
21
21
  "test:analyze": "vitest run tests/analyze",
22
22
  "start": "node ./dist/index.mjs",
23
23
  "typecheck": "tsc --noEmit",
24
- "verify": "npm run typecheck && npm run check-versions && npm run check:hooks-fresh && npm run build && npm run check:bundle && npm run test:hooks && npm run test:analyze",
24
+ "verify": "npm run typecheck && npm run check-versions && npm run check:hooks-fresh && npm run build && npm run check:bundle && npm run test:hooks && npm run test:analyze && node scripts/check-verification-blocks.mjs",
25
25
  "check-versions": "node scripts/check-versions.mjs",
26
26
  "bump-version": "node scripts/bump-version.mjs",
27
27
  "prepublishOnly": "node scripts/check-versions.mjs && npm run typecheck && npm run check:hooks-fresh && npm run build"