@curdx/flow 7.1.19 → 7.1.21

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/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to `@curdx/flow` are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/) and the project follows [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 7.1.21 — 2026-05-11
6
+
7
+ ### Added
8
+
9
+ - **Plugin-local runtime CLI and workflow snapshot surface.** `plugins/curdx-flow/bin/curdx-flow` now exposes deterministic `route`, `snapshot`, `specs`, `state`, `tasks`, `verify-blocks`, and `doctor` commands that skills and smoke tests can call without reaching into bundled hook internals.
10
+ - **True Claude Code end-to-end flow test.** New `npm run test:claudecc:e2e` creates a temporary failing Node project, runs `/curdx-flow:start` through the real Claude Code CLI with the local plugin, verifies `npm test`, validates spec/task/snapshot invariants, captures `--debug-file`, and writes an `analyze` report.
11
+ - **Prompt/batch runtime context hooks.** Added generated hook entrypoints for user-prompt expansion and post-tool-batch snapshots so Claude sees active-spec gates and next actions while a real session is running.
12
+
13
+ ### Changed
14
+
15
+ - **Quick lite-spec is now a first-class completed workflow.** Runtime snapshots accept completed `spec-lite` quick specs with `tasks.md`, `.progress.md`, and `.curdx-state.json` without requiring full-spec `research.md` / `requirements.md` / `design.md` artifacts.
16
+ - **Active spec marker contract is explicit.** `/curdx-flow:start` now instructs Claude to write `$defaultDir/.current-spec` with a bare name for default-root specs, and never to write a project-root `.current-spec`.
17
+ - **Hook freshness checking is dirty-worktree friendly.** `check-hooks-fresh` now compares generated hook tree hashes before/after rebuild, preserving CI stale-bundle detection without failing merely because fresh bundles are already uncommitted.
18
+
19
+ ### Fixed
20
+
21
+ - **`.current-spec` path resolution.** Runtime resolvers now treat relative marker values containing `/` as paths rather than double-prefixing them under `specs/`, and defensively accept a project-root marker if an older run wrote one.
22
+ - **Small `npm test` implementation goals no longer route as publish-critical work.** Auto-policy no longer treats the word `npm` itself as high/critical release risk, while release/publish/tag/plugin/hook surfaces remain protected.
23
+ - **Claude Code transcript lookup follows current project-dir encoding.** `analyze` now resolves `~/.claude/projects` directories where Claude Code hyphenates non-alphanumeric path characters, with legacy slash-only encoding retained as fallback.
24
+ - **`analyze --out` writes the report file.** Markdown/JSON report output now respects `--out` in both fresh and cached-report paths.
25
+ - **`analyze` scopes sidecar hook errors to the active project.** Global `~/.claude/curdx-flow/errors.jsonl` rows are now included only when their `cwd` or `transcript_path` matches the analyzed source, preventing stale errors from other sessions from polluting E2E reports.
26
+
27
+ ### Tests
28
+
29
+ - Added regression coverage for quick lite-spec snapshots, active-spec marker placement, transcript encoding, scoped sidecar hook errors, `analyze --out`, route classification for `npm test`, and the new real Claude Code E2E script.
30
+ - Verified with `npm run verify`, `npm run test:claudecc`, and `CURDX_FLOW_E2E_KEEP_TMP=1 npm run test:claudecc:e2e`.
31
+
32
+ ## 7.1.20 — 2026-05-11
33
+
34
+ ### Changed
35
+
36
+ - **Companion capabilities are now the default required bundle.** `pua`, `claude-mem`, `chrome-devtools-mcp`, `frontend-design`, `sequential-thinking`, and `context7` are marked `required` in the installer alongside `curdx-flow`, so interactive installs show them as always installed and `--ids` installs auto-include missing required companions.
37
+ - **Official plugin dependency metadata added.** `curdx-flow` now declares plugin dependencies for `pua`, `claude-mem`, `chrome-devtools-mcp`, and `frontend-design`, and the root marketplace explicitly allows those cross-marketplace dependencies.
38
+ - **Capability routing marks bundled companions as `core-required`.** Smart-route recommendations still trigger only when relevant, but recommended bundled capabilities are no longer suppressed by narrow availability filters.
39
+
40
+ ### Tests
41
+
42
+ - Added registry and manifest regression coverage for the required bundle, expanded capability-router expectations, rebuilt hook bundles, and tightened `claudecc` smoke checks for `core-required` recommendations.
43
+
5
44
  ## 7.1.19 — 2026-05-11
6
45
 
7
46
  ### Added
package/README.md CHANGED
@@ -123,7 +123,7 @@ The same npm package delivers both products. The installer reads its descriptor
123
123
 
124
124
  - **Node.js** ≥ 20.12 ([download](https://nodejs.org))
125
125
  - **Claude Code CLI** installed and on `PATH` ([install instructions](https://docs.anthropic.com/en/docs/claude-code))
126
- - *(Optional)* **Bun** ≥ 1.0 — auto-detected; the installer offers to install it if you choose `claude-mem`
126
+ - *(Optional)* **Bun** ≥ 1.0 — auto-detected; the installer offers to install it for the default `claude-mem` companion
127
127
 
128
128
  ### Install
129
129
 
@@ -131,7 +131,7 @@ The same npm package delivers both products. The installer reads its descriptor
131
131
  npx @curdx/flow
132
132
  ```
133
133
 
134
- On first run, you pick a language (中文 / English), then select what to install. The bundled `curdx-flow` plugin (the spec workflow itself) is always installed. Everything else is opt-in.
134
+ On first run, you pick a language (中文 / English). The full curdx-flow bundle is installed by default: `curdx-flow`, `pua`, `claude-mem`, `chrome-devtools-mcp`, `frontend-design`, `sequential-thinking`, and `context7`. If a companion has a local prerequisite that is missing, the installer reports the skip reason and continues so the core workflow remains usable.
135
135
 
136
136
  That language choice also controls the managed `~/.claude/CLAUDE.md` block that flow writes. The block is rendered in the selected language, and when you choose `zh` it additionally injects a language policy telling Claude to keep tool/model interaction in English while replying to the user in Simplified Chinese.
137
137
 
@@ -428,9 +428,9 @@ Most workflow frameworks address this by adding more agents, more orchestration,
428
428
 
429
429
  No. flow runs **inside** Claude Code as a plugin. You still use the Claude Code CLI / IDE extension to chat; flow adds the `/curdx-flow:*` commands, subagents, hooks, and the bundled marketplace.
430
430
 
431
- #### Can I use flow without installing the marketplace plugins?
431
+ #### Can I use flow without the companion capabilities?
432
432
 
433
- Yes. The bundled `curdx-flow` plugin is the spec workflow itself; everything else (claude-mem, chrome-devtools-mcp, …) is opt-in. `npx @curdx/flow install` lets you pick.
433
+ The default installer treats the companion capabilities as required because routing depends on them for memory, current docs, frontend design, browser verification, structured reasoning, and retry/parallel recovery. If a local prerequisite is missing, that item may be skipped with a visible reason, but the next `npx @curdx/flow install` run will try to restore it.
434
434
 
435
435
  #### Do I need to commit the spec files?
436
436
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/analyze/index.ts
4
- import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3, writeFileSync } from "fs";
4
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync as realpathSync2, statSync as statSync3, writeFileSync } from "fs";
5
5
  import { homedir as homedir2 } from "os";
6
6
  import path4 from "path";
7
7
 
@@ -1617,8 +1617,17 @@ function resolveRealCwd(cwd) {
1617
1617
  return real;
1618
1618
  }
1619
1619
  function encodeCwd(realCwd) {
1620
+ return realCwd.replace(/[^A-Za-z0-9]/g, "-");
1621
+ }
1622
+ function encodeLegacyCwd(realCwd) {
1620
1623
  return realCwd.replace(/\//g, "-");
1621
1624
  }
1625
+ function candidateProjectDirs(home, realCwd) {
1626
+ const encoded = encodeCwd(realCwd);
1627
+ const legacy = encodeLegacyCwd(realCwd);
1628
+ const names = encoded === legacy ? [encoded] : [encoded, legacy];
1629
+ return names.map((name) => path3.join(home, ".claude", "projects", name));
1630
+ }
1622
1631
  function resolveTranscriptSource(opts = {}) {
1623
1632
  const cwd = opts.cwd ?? process.cwd();
1624
1633
  if (opts.fixtureOverride) {
@@ -1642,12 +1651,20 @@ function resolveTranscriptSource(opts = {}) {
1642
1651
  }
1643
1652
  const home = opts.homedir ?? homedir();
1644
1653
  const realCwd = resolveRealCwd(cwd);
1645
- const encoded = encodeCwd(realCwd);
1646
- const encodedDir = path3.join(home, ".claude", "projects", encoded);
1654
+ const candidates = candidateProjectDirs(home, realCwd);
1655
+ let encodedDir = candidates[0];
1647
1656
  let entries = [];
1648
- try {
1649
- entries = readdirSync(encodedDir, { withFileTypes: true });
1650
- } catch {
1657
+ let foundDir = false;
1658
+ for (const candidate of candidates) {
1659
+ try {
1660
+ entries = readdirSync(candidate, { withFileTypes: true });
1661
+ encodedDir = candidate;
1662
+ foundDir = true;
1663
+ break;
1664
+ } catch {
1665
+ }
1666
+ }
1667
+ if (!foundDir) {
1651
1668
  throw new TranscriptNotFoundError(
1652
1669
  encodedDir,
1653
1670
  `no Claude Code project dir for cwd ${cwd} \u2014 run \`claude\` here at least once, or pass CURDX_TRANSCRIPT_FIXTURE=\u2026 for tests`
@@ -1684,6 +1701,15 @@ function writeState(state) {
1684
1701
  if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
1685
1702
  writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
1686
1703
  }
1704
+ function emitReport(bytes, out) {
1705
+ if (!out) {
1706
+ process.stdout.write(bytes);
1707
+ return;
1708
+ }
1709
+ const target = path4.resolve(out);
1710
+ mkdirSync(path4.dirname(target), { recursive: true });
1711
+ writeFileSync(target, bytes, "utf8");
1712
+ }
1687
1713
  function cleanupOrphanState(state, currentPaths) {
1688
1714
  const now = Date.now();
1689
1715
  const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -1740,7 +1766,26 @@ function loadSpecStates() {
1740
1766
  }
1741
1767
  return out;
1742
1768
  }
1743
- function loadErrorEntries() {
1769
+ function normalizePathForScope(p) {
1770
+ try {
1771
+ return realpathSync2(p);
1772
+ } catch {
1773
+ return path4.resolve(p);
1774
+ }
1775
+ }
1776
+ function isErrorEntryInScope(entry, source) {
1777
+ const transcriptPaths = new Set(source.paths.map((p) => normalizePathForScope(p)));
1778
+ if (entry.transcript_path) {
1779
+ if (transcriptPaths.has(normalizePathForScope(entry.transcript_path))) return true;
1780
+ }
1781
+ if (entry.cwd) {
1782
+ const sourceCwds = /* @__PURE__ */ new Set([normalizePathForScope(source.cwd)]);
1783
+ if (source.kind === "real") sourceCwds.add(normalizePathForScope(source.realCwd));
1784
+ if (sourceCwds.has(normalizePathForScope(entry.cwd))) return true;
1785
+ }
1786
+ return false;
1787
+ }
1788
+ function loadErrorEntries(source) {
1744
1789
  if (!existsSync(ERRORS_LOG_PATH)) return [];
1745
1790
  let raw;
1746
1791
  try {
@@ -1757,7 +1802,7 @@ function loadErrorEntries() {
1757
1802
  const kind = typeof parsed.kind === "string" ? parsed.kind : "unknown";
1758
1803
  const payload = parsed.payload && typeof parsed.payload === "object" && !Array.isArray(parsed.payload) ? parsed.payload : void 0;
1759
1804
  const correlationId = typeof parsed.correlationId === "string" ? parsed.correlationId : void 0;
1760
- out.push({
1805
+ const entry = {
1761
1806
  ts: typeof parsed.ts === "string" ? parsed.ts : "",
1762
1807
  ...typeof parsed.hook === "string" ? { hook: parsed.hook } : {},
1763
1808
  ...typeof parsed.event === "string" ? { event: parsed.event } : {},
@@ -1768,7 +1813,8 @@ function loadErrorEntries() {
1768
1813
  kind,
1769
1814
  ...payload !== void 0 ? { payload } : {},
1770
1815
  ...correlationId !== void 0 ? { correlationId } : {}
1771
- });
1816
+ };
1817
+ if (isErrorEntryInScope(entry, source)) out.push(entry);
1772
1818
  } catch {
1773
1819
  continue;
1774
1820
  }
@@ -1797,12 +1843,12 @@ async function runAnalyzeInner(opts) {
1797
1843
  const cacheCompatible = (state.lastIncludePrompts ?? false) === includePrompts && (state.lastCostSummary ?? false) === costSummary;
1798
1844
  if (allCachedReady && pathStats.length > 0 && cacheCompatible && (state.lastReportJson || state.lastReportMarkdown)) {
1799
1845
  if (opts.json && state.lastReportJson) {
1800
- process.stdout.write(state.lastReportJson);
1846
+ emitReport(state.lastReportJson, opts.out);
1801
1847
  writeState(state);
1802
1848
  return;
1803
1849
  }
1804
1850
  if (!opts.json && state.lastReportMarkdown) {
1805
- process.stdout.write(state.lastReportMarkdown);
1851
+ emitReport(state.lastReportMarkdown, opts.out);
1806
1852
  writeState(state);
1807
1853
  return;
1808
1854
  }
@@ -1822,7 +1868,7 @@ async function runAnalyzeInner(opts) {
1822
1868
  if (r) redacted.push(r);
1823
1869
  }
1824
1870
  const filtered = filterEvents(redacted, { ...opts, limit });
1825
- const errorEntries = loadErrorEntries();
1871
+ const errorEntries = loadErrorEntries(source);
1826
1872
  const specStates = loadSpecStates();
1827
1873
  let costBreakdown;
1828
1874
  let recommendations = [];
@@ -1890,7 +1936,6 @@ async function runAnalyzeInner(opts) {
1890
1936
  ...recommendations.length > 0 ? { recommendations } : {}
1891
1937
  });
1892
1938
  const safeJson = redactReportFields(json, { includePrompts });
1893
- void opts.out;
1894
1939
  const markdownStr = markdown;
1895
1940
  let jsonObj = safeJson;
1896
1941
  if (opts.costSummary === true && costBreakdown) {
@@ -1908,9 +1953,9 @@ async function runAnalyzeInner(opts) {
1908
1953
  state.lastIncludePrompts = includePrompts;
1909
1954
  state.lastCostSummary = costSummary;
1910
1955
  if (opts.json) {
1911
- process.stdout.write(jsonStr);
1956
+ emitReport(jsonStr, opts.out);
1912
1957
  } else {
1913
- process.stdout.write(markdownStr);
1958
+ emitReport(markdownStr, opts.out);
1914
1959
  }
1915
1960
  void safeJson;
1916
1961
  if (counters.parse_error || counters.unknown_type) {
package/dist/index.mjs CHANGED
@@ -514,6 +514,7 @@ var pua = {
514
514
  name: "pua",
515
515
  description: "tanweai/pua \u2014 Chinese Claude Code skills bundle",
516
516
  type: "plugin",
517
+ required: true,
517
518
  slashNamespace: "/pua:*",
518
519
  whenToUse: "auto-fires on 2+ failures or user frustration; sub-modes p7 / p9 / pro / loop. Skip on first-attempt failures or when a known fix is executing.",
519
520
  marketplaces: () => [MARKETPLACE_NAME],
@@ -609,6 +610,7 @@ var claudeMem = {
609
610
  name: "claude-mem",
610
611
  description: "thedotmack/claude-mem \u2014 persistent cross-session memory for Claude Code",
611
612
  type: "plugin",
613
+ required: true,
612
614
  slashNamespace: "/claude-mem:*",
613
615
  whenToUse: 'for cross-session memory search ("did we solve this before?"), phased planning (`make-plan`), or phased execution (`do`).',
614
616
  marketplaces: () => [MARKETPLACE_NAME2],
@@ -668,6 +670,7 @@ var chromeDevtoolsMcp = {
668
670
  name: "chrome-devtools-mcp",
669
671
  description: "ChromeDevTools/chrome-devtools-mcp \u2014 drive a real Chrome from Claude Code",
670
672
  type: "plugin",
673
+ required: true,
671
674
  whenToUse: "when debugging code that runs in a browser: perf traces, network / console inspection, DOM / CSS issues. Prefer snapshot over screenshot.",
672
675
  prereqCheck: async (t2) => {
673
676
  const major = Number(process.versions.node.split(".")[0] ?? "0");
@@ -697,6 +700,7 @@ var frontendDesign = {
697
700
  name: "frontend-design",
698
701
  description: "Anthropic official \u2014 UI/frontend design helpers",
699
702
  type: "plugin",
703
+ required: true,
700
704
  whenToUse: "auto-fires when building UI / web components / pages. Best where visual personality matters (landing, marketing, portfolio).",
701
705
  isInstalled: () => isPluginInstalled(PLUGIN_ID4),
702
706
  install: (ctx) => installPluginById(PLUGIN_ID4, ctx),
@@ -742,6 +746,7 @@ var sequentialThinking = {
742
746
  name: "sequential-thinking",
743
747
  description: "modelcontextprotocol/server-sequential-thinking \u2014 structured reasoning helper",
744
748
  type: "mcp",
749
+ required: true,
745
750
  whenToUse: "for complex multi-step problems where assumptions may shift (architecture comparison, risk-assessed migrations, prod-only debugging). Skip for simple queries.",
746
751
  isInstalled: () => isMcpInstalled(MCP_NAME),
747
752
  install: async (ctx) => {
@@ -783,6 +788,7 @@ var context7 = {
783
788
  name: "context7",
784
789
  description: "upstash/context7 \u2014 up-to-date docs from any library (HTTP MCP, optional API key)",
785
790
  type: "mcp",
791
+ required: true,
786
792
  whenToUse: "for any library / SDK / framework / API / Claude Code docs lookup. Use instead of web search.",
787
793
  isInstalled: () => isMcpInstalled(MCP_NAME2),
788
794
  configPrompts: async ({ t: t2 }) => {
@@ -913,7 +919,15 @@ var ORDER = [
913
919
  "sequential-thinking",
914
920
  "pua"
915
921
  ];
916
- var DOCS_RE = /\b(api|sdk|library|libraries|framework|docs?|documentation|version|upgrade|dependency|dependencies|claude code|plugin|mcp|hook|hooks|skill|skills|agent|agents|react|vue|spring|spring boot|spring cloud|next\.?js|vite|webpack|npm|node)\b|最新|文档|依赖|框架|插件|官方|联网|搜索/i;
922
+ var CORE_REQUIRED = /* @__PURE__ */ new Set([
923
+ "context7",
924
+ "claude-mem",
925
+ "frontend-design",
926
+ "chrome-devtools-mcp",
927
+ "sequential-thinking",
928
+ "pua"
929
+ ]);
930
+ var EXTERNAL_DOCS_RE = /\b(api|sdk|library|libraries|framework|version|upgrade|dependency|dependencies|official docs?|latest docs?|claude code|plugin|mcp|hook|hooks|skill|skills|agent|agents|react|vue|spring|spring boot|spring cloud|next\.?js|vite|webpack|npm|node)\b|最新|依赖|框架|插件|官方|联网|搜索|文档.*(最新|官方|API|SDK|框架|插件|依赖)/i;
917
931
  var MEMORY_RE = /\b(previous|before|again|remember|memory|history|similar|repeated|regression|already solved|same bug|past decision)\b|之前|上次|记得|历史|做过|又|重复|老问题/i;
918
932
  var UI_RE = /\b(ui|ux|frontend|front-end|browser|chrome|dom|css|html|layout|component|page|form|modal|responsive|visual|render|react|vue|vite|next\.?js|screenshot|interaction)\b|前端|页面|浏览器|样式|交互|组件|布局|视觉|截图/i;
919
933
  var BROWSER_VERIFY_RE = /\b(browser|chrome|dom|css|network|console|performance|render|screenshot|e2e|playwright|visual regression|interaction)\b|浏览器|控制台|网络|性能|渲染|截图|端到端/i;
@@ -928,11 +942,14 @@ function hasAny(values, candidates) {
928
942
  const set = new Set((values ?? []).map((v) => v.toLowerCase()));
929
943
  return candidates.some((candidate) => set.has(candidate.toLowerCase()));
930
944
  }
931
- function capabilityAllowed(id, available) {
932
- return available === null || available.has(id);
945
+ function capabilityAvailability(id, available) {
946
+ if (CORE_REQUIRED.has(id)) return "core-required";
947
+ if (available === null) return "check-if-installed";
948
+ return available.has(id) ? "known-available" : null;
933
949
  }
934
950
  function pushRecommendation(out, available, id, phase, reason, instruction) {
935
- if (!capabilityAllowed(id, available)) return;
951
+ const availability = capabilityAvailability(id, available);
952
+ if (availability === null) return;
936
953
  if (out.some((rec) => rec.id === id)) return;
937
954
  const cap = CAPABILITIES[id];
938
955
  out.push({
@@ -941,6 +958,7 @@ function pushRecommendation(out, available, id, phase, reason, instruction) {
941
958
  type: cap.type,
942
959
  invocation: cap.invocation,
943
960
  phase,
961
+ availability,
944
962
  reason,
945
963
  instruction
946
964
  });
@@ -960,16 +978,17 @@ function recommendToolCapabilities(input) {
960
978
  if (missingRoots > 0) {
961
979
  return recs;
962
980
  }
963
- const localLowRisk = LOW_RISK_LOCAL_RE.test(goal) && route === "direct-change";
981
+ const externalDocsRelevant = EXTERNAL_DOCS_RE.test(goal);
982
+ const localLowRisk = LOW_RISK_LOCAL_RE.test(goal) && route === "direct-change" && !externalDocsRelevant;
964
983
  if (localLowRisk) {
965
984
  return recs;
966
985
  }
967
986
  const hasFrontend = UI_RE.test(goal) || hasAny(topologyKinds, ["frontend-app"]) || hasAny(topologyFrameworks, ["react", "vue", "next.js", "vite"]);
968
987
  const browserRuntime = BROWSER_VERIFY_RE.test(goal) || hasFrontend;
969
- const complex = COMPLEX_RE.test(goal) || risk === "high" || risk === "critical" || route === "full-spec" || route === "epic-split";
988
+ const complex = COMPLEX_RE.test(goal) && route !== "direct-change" || risk === "high" || risk === "critical" || route === "full-spec" || route === "epic-split";
970
989
  const stuck = STUCK_RE.test(goal);
971
990
  const parallel = PARALLEL_RE.test(goal) || route === "epic-split";
972
- if (DOCS_RE.test(goal)) {
991
+ if (externalDocsRelevant) {
973
992
  pushRecommendation(
974
993
  recs,
975
994
  available,
@@ -1045,25 +1064,25 @@ function renderInstalledCapabilityRules(availableCapabilities) {
1045
1064
  }
1046
1065
  function renderCapabilityDecisionTree(availableCapabilities) {
1047
1066
  const available = new Set(availableCapabilities);
1048
- const lines = [
1049
- "1. Can the edit be finished safely from local code in 1-2 steps? -> Do it directly."
1067
+ const rules = [
1068
+ "Can the edit be finished safely from local code in 1-2 steps? -> Do it directly."
1050
1069
  ];
1051
1070
  if (available.has("context7")) {
1052
- lines.push("2. Does correctness depend on external docs, SDKs, APIs, or Claude Code behavior? -> use the Context7 MCP before editing.");
1071
+ rules.push("Does correctness depend on external docs, SDKs, APIs, or Claude Code behavior? -> use the Context7 MCP before editing.");
1053
1072
  }
1054
1073
  if (available.has("claude-mem")) {
1055
- lines.push("3. Might similar work, a prior decision, or a repeated failure exist? -> Start with `/claude-mem:mem-search`.");
1074
+ rules.push("Might similar work, a prior decision, or a repeated failure exist? -> Start with `/claude-mem:mem-search`.");
1056
1075
  }
1057
1076
  if (available.has("frontend-design") || available.has("chrome-devtools-mcp")) {
1058
- lines.push("4. Is visible frontend behavior in scope? -> Use frontend-design for UI decisions and Chrome DevTools MCP for runtime proof when installed.");
1077
+ rules.push("Is visible frontend behavior in scope? -> Use frontend-design for UI decisions and Chrome DevTools MCP for runtime proof when installed.");
1059
1078
  }
1060
1079
  if (available.has("sequential-thinking")) {
1061
- lines.push("5. Is the work high-risk, architectural, or assumption-heavy? -> Use sequential-thinking after reading the relevant code.");
1080
+ rules.push("Is the work high-risk, architectural, or assumption-heavy? -> Use sequential-thinking after reading the relevant code.");
1062
1081
  }
1063
1082
  if (available.has("pua")) {
1064
- lines.push("6. Are there multiple failed attempts or truly independent parallel slices? -> Use `/pua:pua-loop` for recovery or `/pua:p9` for bounded parallel planning.");
1083
+ rules.push("Are there multiple failed attempts or truly independent parallel slices? -> Use `/pua:pua-loop` for recovery or `/pua:p9` for bounded parallel planning.");
1065
1084
  }
1066
- return lines;
1085
+ return rules.map((rule, idx) => `${idx + 1}. ${rule}`);
1067
1086
  }
1068
1087
  function parseList(value) {
1069
1088
  if (!value) return [];
@@ -1133,7 +1152,7 @@ function buildSkipRules(ids) {
1133
1152
  function buildDecisionTree(ids) {
1134
1153
  const out = renderCapabilityDecisionTree([...ids]);
1135
1154
  if (ids.has("curdx-flow")) {
1136
- out.push("7. Is the request ambiguous, cross-cutting, phase-based, or multi-root? -> Run /curdx-flow:start.");
1155
+ out.push(`${out.length + 1}. Is the request ambiguous, cross-cutting, phase-based, or multi-root? -> Run /curdx-flow:start.`);
1137
1156
  }
1138
1157
  return out;
1139
1158
  }
@@ -1851,7 +1870,7 @@ var analyzeCmd = defineCommand({
1851
1870
  const limit = typeof limitRaw === "string" && limitRaw.length > 0 ? Number(limitRaw) : void 0;
1852
1871
  const topRaw = args.top;
1853
1872
  const top = typeof topRaw === "string" && topRaw.length > 0 ? Number(topRaw) : void 0;
1854
- const { runAnalyze } = await import("./analyze-FX2PCSL6.mjs");
1873
+ const { runAnalyze } = await import("./analyze-I4TXK4S7.mjs");
1855
1874
  await runAnalyze({
1856
1875
  out: typeof args.out === "string" ? args.out : void 0,
1857
1876
  json: Boolean(args.json),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "7.1.19",
3
+ "version": "7.1.21",
4
4
  "description": "Interactive installer for Claude Code plugins and MCP servers",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.mjs",
@@ -21,6 +21,7 @@
21
21
  "test:analyze": "vitest run tests/analyze",
22
22
  "test:runner": "vitest run tests/runner",
23
23
  "test:claudecc": "npm run build:hooks && node scripts/claudecc-smoke.mjs",
24
+ "test:claudecc:e2e": "npm run build && npm run build:hooks && node scripts/claudecc-e2e-flow.mjs",
24
25
  "start": "node ./dist/index.mjs",
25
26
  "typecheck": "tsc --noEmit",
26
27
  "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 && npm run test:runner && node scripts/check-verification-blocks.mjs",