@curdx/flow 7.1.20 → 7.1.22

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,51 @@
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.22 — 2026-05-11
6
+
7
+ ### Added
8
+
9
+ - **Browser verification policy for full-stack delivery.** Added a plugin reference that standardizes Playwright CLI as the default repeatable E2E path and Chrome DevTools MCP as the high-fidelity path for GIS, WebGL, canvas, map, GPU, console, network, performance, and flaky Playwright cases.
10
+ - **Browser readiness in `curdx-flow doctor`.** Doctor output now reports detected dev-server scripts, E2E scripts, browser automation dependencies, Playwright config files, chrome-devtools-mcp dependency declaration, and local Chrome availability.
11
+
12
+ ### Changed
13
+
14
+ - **Task planning now requires an explicit Browser Verify decision.** `task-planner`, `/curdx-flow:tasks`, VE references, and task templates now require `playwright`, `chrome-devtools-mcp`, `none`, or `blocked` before implementation tasks.
15
+ - **Executor and QA prompts enforce browser evidence.** `spec-executor`, `qa-engineer`, and `verification-before-completion` now reject browser-facing completion claims without fresh Playwright or Chrome DevTools MCP evidence, and explicitly avoid `/ultrareview` as a verification path.
16
+ - **Research output feeds browser verification.** `research-analyst` now records Browser Verify strategy alongside dev server, E2E config, browser dependency, port, and health endpoint discovery.
17
+
18
+ ### Tests
19
+
20
+ - Added runtime CLI coverage for browser readiness in `doctor` output and manifest-integrity coverage that keeps the browser verification policy linked into planning, execution, QA, and skill surfaces.
21
+ - Verified with `npm run verify`.
22
+
23
+ ## 7.1.21 — 2026-05-11
24
+
25
+ ### Added
26
+
27
+ - **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.
28
+ - **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.
29
+ - **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.
30
+
31
+ ### Changed
32
+
33
+ - **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.
34
+ - **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`.
35
+ - **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.
36
+
37
+ ### Fixed
38
+
39
+ - **`.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.
40
+ - **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.
41
+ - **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.
42
+ - **`analyze --out` writes the report file.** Markdown/JSON report output now respects `--out` in both fresh and cached-report paths.
43
+ - **`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.
44
+
45
+ ### Tests
46
+
47
+ - 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.
48
+ - Verified with `npm run verify`, `npm run test:claudecc`, and `CURDX_FLOW_E2E_KEEP_TMP=1 npm run test:claudecc:e2e`.
49
+
5
50
  ## 7.1.20 — 2026-05-11
6
51
 
7
52
  ### Changed
@@ -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
@@ -1870,7 +1870,7 @@ var analyzeCmd = defineCommand({
1870
1870
  const limit = typeof limitRaw === "string" && limitRaw.length > 0 ? Number(limitRaw) : void 0;
1871
1871
  const topRaw = args.top;
1872
1872
  const top = typeof topRaw === "string" && topRaw.length > 0 ? Number(topRaw) : void 0;
1873
- const { runAnalyze } = await import("./analyze-FX2PCSL6.mjs");
1873
+ const { runAnalyze } = await import("./analyze-I4TXK4S7.mjs");
1874
1874
  await runAnalyze({
1875
1875
  out: typeof args.out === "string" ? args.out : void 0,
1876
1876
  json: Boolean(args.json),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "7.1.20",
3
+ "version": "7.1.22",
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",