claudemd-cli 0.68.4 → 0.69.1

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/README.md CHANGED
@@ -215,6 +215,19 @@ export DISABLE_BOOTSTRAP_FAIL_BANNER=1 # v0.50.0+ — only the SessionStart
215
215
  # sentinel + bootstrap.log trail keep being
216
216
  # written so the state stays diagnosable.
217
217
 
218
+ export DISABLE_SPEC_DRIFT_BANNER=1 # only the SessionStart banner reporting
219
+ # installed-spec vs plugin-spec drift; the
220
+ # version check itself still runs, so
221
+ # /claudemd-doctor keeps reporting the drift.
222
+
223
+ export DISABLE_RULE_HITS_LOG=1 # stop every hook from appending to
224
+ # ~/.claude/logs/claudemd.jsonl. Enforcement
225
+ # is unaffected (a deny still denies); what
226
+ # stops is the telemetry /claudemd-audit,
227
+ # /claudemd-rules and the §13.1 demote loop
228
+ # read. Set it while hand-probing a hook so
229
+ # the probe does not land in the real corpus.
230
+
218
231
  export DISABLE_BATCH_CADENCE_ADVISORY=1 # v0.19.2+ — only the §13.2 batch-review
219
232
  # cadence advisory inside session-end-check;
220
233
  # mid-SPINE warn-on-unvalidated-mutation
@@ -353,7 +366,7 @@ claudemd/
353
366
  ├── .claude-plugin/
354
367
  │ ├── plugin.json # minimal manifest (name, version, author, license, keywords)
355
368
  │ └── marketplace.json # marketplace catalog entry
356
- ├── hooks/ # 15 shell hooks + hooks/lib/ (hook-common, rule-hits, platform)
369
+ ├── hooks/ # 15 shell hooks + hooks/lib/ (hook-common, rule-hits, platform, memory-tags)
357
370
  │ └── hooks.json # authoritative hook registration (v0.1.5+); CC expands ${CLAUDE_PLUGIN_ROOT} here
358
371
  ├── commands/ # 16 slash-command markdown files
359
372
  ├── bin/ # standalone CLI entrypoint (claudemd-lint.js → `npx claudemd-cli` on npmjs.org)
@@ -368,12 +381,18 @@ claudemd/
368
381
 
369
382
  ## Extending
370
383
 
371
- - **Add a new hook** — see [`docs/ADDING-NEW-HOOK.md`](docs/ADDING-NEW-HOOK.md) for the 5-step guide (hook script + test + plugin registration + doc + version bump).
384
+ - **Add a new hook** — see [`docs/ADDING-NEW-HOOK.md`](docs/ADDING-NEW-HOOK.md) for the full checklist (hook script + test + plugin registration + telemetry gates + docs + the content gates a new hook trips + version bump).
372
385
  - **Rule-hits log schema** — [`docs/RULE-HITS-SCHEMA.md`](docs/RULE-HITS-SCHEMA.md) for the JSONL row format used by `/claudemd-audit`.
373
386
  - **Design rationale + decisions log** — [`docs/superpowers/specs/2026-04-21-claudemd-plugin-design.md`](docs/superpowers/specs/2026-04-21-claudemd-plugin-design.md).
374
387
 
375
388
  ---
376
389
 
390
+ ## Changelog
391
+
392
+ Every release is recorded in [`CHANGELOG.md`](https://github.com/sdsrss/claudemd/blob/main/CHANGELOG.md) on GitHub. It is not shipped in the npm tarball — at 727 KB it was 89% of a package whose runtime is 95 KB unpacked (35 KB compressed), and `npx claudemd-cli` re-downloads that tarball on every cold run.
393
+
394
+ ---
395
+
377
396
  ## License
378
397
 
379
398
  MIT — see [LICENSE](LICENSE).
@@ -29,6 +29,11 @@ import {
29
29
  stripGitCommitComments,
30
30
  looksLikeGitMessageFile,
31
31
  } from '../scripts/lib/lint.js';
32
+ // Shared argv authority. It used to be a private function here, which is how
33
+ // scripts/lint-argv.js came to authenticate a CLI's argv contract by FUNCTION
34
+ // NAME — any file declaring a local `validateAndExpandFlags` satisfied the gate
35
+ // (audit-2026-08-22 条目 14). The gate now requires this import.
36
+ import { validateAndExpandFlags } from '../scripts/lib/argv.js';
32
37
 
33
38
  const HERE = path.dirname(fileURLToPath(import.meta.url));
34
39
  const REPO_ROOT = path.resolve(HERE, '..');
@@ -142,42 +147,6 @@ function workTreeOf(git, cwd) {
142
147
  return cwd;
143
148
  }
144
149
 
145
- // Strict-validate flag-shaped args + normalize `--key=value` → `--key value`
146
- // pairs so the existing space-form parsing below works on either shape.
147
- // Catches the same antipattern the slash-command CLIs hit in v0.9.16/0.9.17:
148
- // `args.includes('--json')` returns false for `--json=yes`, so the flag was
149
- // silently dropped; `args.indexOf('--file')` returns -1 for `--file=PATH`,
150
- // so the value was silently ignored; an unknown `--jzon` typo was silently
151
- // stripped from positional and never surfaced. Each path now exits 2.
152
- function validateAndExpandFlags(args, knownBools, knownValues, sub) {
153
- const out = [];
154
- const bools = new Set(knownBools);
155
- const values = new Set(knownValues);
156
- for (const a of args) {
157
- if (!a.startsWith('--')) { out.push(a); continue; }
158
- if (a.includes('=')) {
159
- const eq = a.indexOf('=');
160
- const k = a.slice(0, eq);
161
- const v = a.slice(eq + 1);
162
- if (bools.has(k)) {
163
- process.stderr.write(`${sub}: '${k}' is a boolean flag and does not take a value (got '${a}'). Drop the '=...' suffix.\n`);
164
- process.exit(2);
165
- }
166
- if (values.has(k)) {
167
- out.push(k);
168
- out.push(v);
169
- continue;
170
- }
171
- process.stderr.write(`${sub}: unknown flag '${k}' (got '${a}').\n`);
172
- process.exit(2);
173
- }
174
- if (bools.has(a) || values.has(a)) { out.push(a); continue; }
175
- process.stderr.write(`${sub}: unknown flag '${a}'.\n`);
176
- process.exit(2);
177
- }
178
- return out;
179
- }
180
-
181
150
  function lintCmd(rawArgs) {
182
151
  const args = validateAndExpandFlags(
183
152
  rawArgs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemd-cli",
3
- "version": "0.68.4",
3
+ "version": "0.69.1",
4
4
  "description": "Standalone CLI for §10-V banned-vocab + transcript scanning. Companion to the claudemd Claude Code plugin (github.com/sdsrss/claudemd) for use in git pre-commit hooks, GitHub Actions, and other agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,126 @@
1
+ // Strict argv parser for slash-command CLI scripts (clean-residue / audit /
2
+ // sparkline). Three contracts the previous inline parsers silently violated:
3
+ // 1. `--key=value` is the ONLY accepted shape for value flags. The space form
4
+ // `--key value` falls back to default + ignores the value, exiting 0 — same
5
+ // footgun family as the v0.9.14 `claudemd-cli lint <path>` silent-success.
6
+ // 2. Unknown flags reject loudly. Pre-fix, every script's `args.find()` lookup
7
+ // silently dropped anything it didn't recognize, so a typo produced
8
+ // indistinguishable-from-success output.
9
+ // 3. `--key=value` for boolean flags rejects (`--apply=yes` shouldn't parse
10
+ // as `--apply` true).
11
+ // Caller catches `ArgvError` and exits 2 (distinct from numeric-validation
12
+ // exit 1) so wrappers can tell parsing-shape errors from validation errors.
13
+
14
+ export class ArgvError extends Error {
15
+ constructor(message) { super(message); this.name = 'ArgvError'; }
16
+ }
17
+
18
+ // Discoverability helper: when `--help` or `-h` is the first non-empty arg
19
+ // (or anywhere in argv for scripts with no flags), print usage to stdout and
20
+ // exit 0. Caller invokes BEFORE parseStrict so unknown-arg rejection doesn't
21
+ // shadow the universal first-probe of every Unix CLI. Pre-fix, every
22
+ // parseStrict-using script (audit / sparkline / hard-rules-audit /
23
+ // clean-residue / doctor) responded `Unknown argument: '--help'.` exit 2 —
24
+ // classic discoverability bug for new users.
25
+ export function printHelpAndExit(argv, usage) {
26
+ if (argv.some(a => a === '--help' || a === '-h')) {
27
+ process.stdout.write(usage.endsWith('\n') ? usage : usage + '\n');
28
+ process.exit(0);
29
+ }
30
+ }
31
+
32
+ // Strict positive-integer validator for numeric flags (`--days` / `--age-days`
33
+ // / `--prune-backups` / `--sample`). Returns the integer when `raw` (after
34
+ // trimming surrounding whitespace) is a plain base-10 positive integer with no
35
+ // leading zero, else null. `Number()` alone is too permissive — it coerces
36
+ // '0x1e' → 30, '1e2' → 100, ' 30 ' → 30 — all of which pass `Number.isInteger`
37
+ // despite help text promising a "positive integer", a silent contract
38
+ // divergence (inverse of the `parseInt` truncation footgun). Mirrors the
39
+ // `/^[1-9][0-9]*$/` guard already used for CLAUDEMD_BATCH_THRESHOLD in status.js.
40
+ export function parsePositiveInt(raw) {
41
+ if (raw == null) return null;
42
+ const s = String(raw).trim();
43
+ // Plain base-10 notation only — rejects hex ('0x1e'), exponential ('1e2'),
44
+ // signs, and interior junk that `Number()` would coerce. A trailing-zero
45
+ // decimal ('30.0', '30.00') is allowed through the shape gate so the
46
+ // integer-valued-float check below can accept it (existing contract: callers
47
+ // / scripts may pass '30.0'); a true fraction ('1.5') passes the shape gate
48
+ // but fails Number.isInteger and is rejected.
49
+ if (!/^[0-9]+(\.[0-9]+)?$/.test(s)) return null;
50
+ const n = Number(s);
51
+ if (!Number.isInteger(n) || n < 1) return null;
52
+ return n;
53
+ }
54
+
55
+ export function parseStrict(argv, { bools = [], values = [] } = {}) {
56
+ const out = { bools: new Set(), values: {} };
57
+ const knownBool = new Set(bools);
58
+ const knownValue = new Set(values);
59
+ for (const a of argv) {
60
+ if (knownBool.has(a)) { out.bools.add(a); continue; }
61
+ if (a.startsWith('--') && a.includes('=')) {
62
+ const eq = a.indexOf('=');
63
+ const k = a.slice(0, eq);
64
+ const v = a.slice(eq + 1);
65
+ if (knownValue.has(k)) { out.values[k] = v; continue; }
66
+ if (knownBool.has(k)) {
67
+ throw new ArgvError(`Boolean flag '${k}' does not take a value (got '${a}').`);
68
+ }
69
+ throw new ArgvError(`Unknown flag: '${k}'.`);
70
+ }
71
+ if (knownValue.has(a)) {
72
+ throw new ArgvError(`'${a}' requires '=value' form (got '${a}' bare). Use '${a}=N'.`);
73
+ }
74
+ throw new ArgvError(`Unknown argument: '${a}'.`);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ // Space-form argv validator for the published `claudemd-cli` binary.
80
+ //
81
+ // Lived in bin/claudemd-lint.js until audit-2026-08-22 条目 14: a second argv
82
+ // authority beside parseStrict, and scripts/lint-argv.js authenticated it BY
83
+ // FUNCTION NAME, so any file that declared a local function called
84
+ // `validateAndExpandFlags` satisfied the gate without validating anything. The
85
+ // gate had been widened to accommodate the duplicate instead of the duplicate
86
+ // being converged. It is not merged into parseStrict because the two contracts
87
+ // genuinely differ and the difference is published: parseStrict rejects the
88
+ // `--key value` space form and positional arguments, both of which
89
+ // `claudemd-cli lint <path> --comment-char ';'` documents and users' pre-commit
90
+ // hooks depend on.
91
+ //
92
+ // Strict-validate flag-shaped args + normalize `--key=value` → `--key value`
93
+ // pairs so the existing space-form parsing below works on either shape.
94
+ // Catches the same antipattern the slash-command CLIs hit in v0.9.16/0.9.17:
95
+ // `args.includes('--json')` returns false for `--json=yes`, so the flag was
96
+ // silently dropped; `args.indexOf('--file')` returns -1 for `--file=PATH`,
97
+ // so the value was silently ignored; an unknown `--jzon` typo was silently
98
+ // stripped from positional and never surfaced. Each path now exits 2.
99
+ export function validateAndExpandFlags(args, knownBools, knownValues, sub) {
100
+ const out = [];
101
+ const bools = new Set(knownBools);
102
+ const values = new Set(knownValues);
103
+ for (const a of args) {
104
+ if (!a.startsWith('--')) { out.push(a); continue; }
105
+ if (a.includes('=')) {
106
+ const eq = a.indexOf('=');
107
+ const k = a.slice(0, eq);
108
+ const v = a.slice(eq + 1);
109
+ if (bools.has(k)) {
110
+ process.stderr.write(`${sub}: '${k}' is a boolean flag and does not take a value (got '${a}'). Drop the '=...' suffix.\n`);
111
+ process.exit(2);
112
+ }
113
+ if (values.has(k)) {
114
+ out.push(k);
115
+ out.push(v);
116
+ continue;
117
+ }
118
+ process.stderr.write(`${sub}: unknown flag '${k}' (got '${a}').\n`);
119
+ process.exit(2);
120
+ }
121
+ if (bools.has(a) || values.has(a)) { out.push(a); continue; }
122
+ process.stderr.write(`${sub}: unknown flag '${a}'.\n`);
123
+ process.exit(2);
124
+ }
125
+ return out;
126
+ }
@@ -85,10 +85,14 @@ function posixClassesToJs(regex) {
85
85
  // not read as a value claim. `\b` treats '-', '/', '.' as word boundaries, so
86
86
  // `\bcomprehensive\b` fires INSIDE `comprehensive-parser.js` or a branch name
87
87
  // `docs/comprehensive-audit`. Mirrors hooks/banned-vocab-check.sh's v0.23.19
88
- // Path 2 sanitizer (fenced blocks → inline backtick spans → slashed-path runs)
89
- // and adds a bare dotted-file token strip, because the CLI's primary input —
90
- // commit messages commonly names bare files (`refactor comprehensive-parser.js`)
91
- // without backticks or a leading path. Token classes are ASCII-only so 中文
88
+ // Path 2 sanitizer stage for stage: fenced blocks → inline backtick spans →
89
+ // slashed-path runs bare dotted-file tokens. The last clause was JS-only when
90
+ // it was written — commit messages, the CLI's primary input, commonly name bare
91
+ // files (`refactor comprehensive-parser.js`) with no backticks and no leading
92
+ // path — but the bash side gained it on 2026-08-16 and
93
+ // tests/scripts/sanitize-stage-parity.test.js now extracts both programs and
94
+ // requires the clause lists to match, so it is no longer an extension of
95
+ // anything (audit-2026-08-22 条目 22). Token classes are ASCII-only so 中文
92
96
  // prose and bare-word claims (the real violations) stay intact and still match.
93
97
  export function stripIdentifiers(text) {
94
98
  if (!text) return text;
@@ -162,8 +166,10 @@ export function stripIdentifiers(text) {
162
166
  // The bash engines need no equivalent: POSIX sed does not backtrack and
163
167
  // the hook caps its input at `tail -c 4096`; the Node path caps nothing.
164
168
  .replace(/(?<![A-Za-z0-9._@~-])[A-Za-z0-9._@~-]*\/[A-Za-z0-9._/@~-]*/g, ' ')
165
- // 4. Bare dotted-file tokens (foo.js, comprehensive-parser.ts) — CLI
166
- // extension. The extension must start with a LOWERCASE letter, which
169
+ // 4. Bare dotted-file tokens (foo.js, comprehensive-parser.ts). JS-only
170
+ // when written; the bash sanitizer carries the same clause since
171
+ // 2026-08-16 and sanitize-stage-parity pins them together.
172
+ // The extension must start with a LOWERCASE letter, which
167
173
  // (a) excludes decimals / versions ("3.5x", "v6.14") whose ".5x"/".14"
168
174
  // could otherwise swallow a baseline-less ratio claim → false negative,
169
175
  // and (b) excludes sentence-boundary typos ("comprehensive.Next", capital