@curdx/flow 7.1.20 → 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 +27 -0
- package/dist/{analyze-FX2PCSL6.mjs → analyze-I4TXK4S7.mjs} +60 -15
- package/dist/index.mjs +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
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
|
+
|
|
5
32
|
## 7.1.20 — 2026-05-11
|
|
6
33
|
|
|
7
34
|
### 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
|
|
1646
|
-
|
|
1654
|
+
const candidates = candidateProjectDirs(home, realCwd);
|
|
1655
|
+
let encodedDir = candidates[0];
|
|
1647
1656
|
let entries = [];
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1846
|
+
emitReport(state.lastReportJson, opts.out);
|
|
1801
1847
|
writeState(state);
|
|
1802
1848
|
return;
|
|
1803
1849
|
}
|
|
1804
1850
|
if (!opts.json && state.lastReportMarkdown) {
|
|
1805
|
-
|
|
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
|
-
|
|
1956
|
+
emitReport(jsonStr, opts.out);
|
|
1912
1957
|
} else {
|
|
1913
|
-
|
|
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-
|
|
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.
|
|
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",
|