@curdx/flow 7.1.6 → 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
@@ -1626,17 +1626,49 @@ var analyzeCmd = defineCommand({
1626
1626
  "include-prompts": {
1627
1627
  type: "boolean",
1628
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)"
1629
1653
  }
1630
1654
  },
1631
1655
  async run({ args }) {
1632
1656
  const limitRaw = args.limit;
1633
1657
  const limit = typeof limitRaw === "string" && limitRaw.length > 0 ? Number(limitRaw) : void 0;
1634
- 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");
1635
1661
  await runAnalyze({
1636
1662
  out: typeof args.out === "string" ? args.out : void 0,
1637
1663
  json: Boolean(args.json),
1638
1664
  limit: Number.isFinite(limit) ? limit : void 0,
1639
- 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
1640
1672
  });
1641
1673
  }
1642
1674
  });
@@ -1770,6 +1802,17 @@ var updateCmd = defineCommand2({
1770
1802
  p10.outro(t("app.outro"));
1771
1803
  }
1772
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
+ });
1773
1816
  var statusCmd = defineCommand2({
1774
1817
  meta: { name: "status", description: "Show install status" },
1775
1818
  args: {
@@ -1783,7 +1826,27 @@ var statusCmd = defineCommand2({
1783
1826
  if (!args.json) p10.outro(t("app.outro"));
1784
1827
  }
1785
1828
  });
1786
- 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
+ `;
1787
1850
  var root = defineCommand2({
1788
1851
  meta: {
1789
1852
  name: "@curdx/flow",
@@ -1796,7 +1859,8 @@ var root = defineCommand2({
1796
1859
  uninstall: uninstallCmd,
1797
1860
  update: updateCmd,
1798
1861
  status: statusCmd,
1799
- analyze: analyze_default
1862
+ analyze: analyze_default,
1863
+ check: checkCmd
1800
1864
  }
1801
1865
  // No root run() — citty 0.1.6 calls parent.run AFTER a matching subcommand,
1802
1866
  // which would render the menu after a subcommand finishes. We dispatch the
@@ -1830,6 +1894,16 @@ async function runInteractive(argv2) {
1830
1894
  var argv = process.argv.slice(2);
1831
1895
  var first = firstNonFlag(argv);
1832
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
+ }
1833
1907
  if (first === void 0 || first !== void 0 && !SUBCOMMANDS.has(first) && first !== "--help" && first !== "-h") {
1834
1908
  if (first === void 0) {
1835
1909
  runInteractive(argv).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "7.1.6",
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"