claudemd-cli 0.9.12 → 0.9.14

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
@@ -8,6 +8,57 @@ All notable changes to the `claudemd` plugin. This changelog tracks plugin artif
8
8
  - **Canonical spec version source**: `spec/CLAUDE.md` top-line title (`# AI-CODING-SPEC vX.Y.Z — Core`) + `spec/CLAUDE-changelog.md` top `##` entry.
9
9
  - **Plugin semver vs spec semver** are independent: plugin patch (0.2.0 → 0.2.1) may ship when spec is unchanged (this release); plugin minor (0.1.9 → 0.2.0) ships when spec minor updates (v0.2.0 shipped spec v6.10.0).
10
10
 
11
+ ## [0.9.14] - 2026-05-10
12
+
13
+ **Patch — `claudemd-cli lint <path>` silent-success fix.** Spec v6.11.7 unchanged. Surfaced while role-playing a real user of the standalone CLI: `claudemd lint /path/to/COMMIT_EDITMSG` (the natural pre-commit-hook shape, mirroring `audit <jsonl-path>`) silently scans the **literal path string** for banned-vocab — finds none — and exits 0, even when the file content would deny. CI / git-pre-commit integrations would have shipped commit messages full of `significantly` / `robust` / `production-ready` undetected.
14
+
15
+ ### Fixed
16
+
17
+ - `[fix]` **`bin/claudemd-lint.js` `lintCmd`**: a single positional arg that is an existing regular file is now auto-treated as `--file <path>` (file contents scanned), not as a literal text argument. Pre-fix: `claudemd lint .git/COMMIT_EDITMSG` → scans the string `".git/COMMIT_EDITMSG"` → exit 0. Post-fix: reads file contents → exit reflects banned-vocab presence. Backward-compatible: bare-text `claudemd lint "literal sentence"` still scans the sentence; non-existent paths fall through to text-scan (no surprise error). Asymmetry with `audit <jsonl-path>` (which already takes a path) was the root inconsistency.
18
+
19
+ ### Added
20
+
21
+ - `[feat]` **`--file <path>` flag on `lint`** for explicit file-mode (parallels `--stdin`). Mutual-exclusion enforced: `--file` + `--stdin`, `--file` + positional, `--stdin` + positional all → exit 2 with reason. `--file <missing>` → exit 2 `file not found`. Recommended form for scripts that don't want to depend on the auto-detect heuristic.
22
+ - `[doc]` **USAGE block updated** in `bin/claudemd-lint.js`: documents `--file <path>`, the auto-detect rule, and the opt-out (quote literal text or use `--stdin`).
23
+ - `[test]` **8 new cases in `tests/scripts/lint-cli.test.js` (12/12 → 20/20)**: `--file` happy-path (hit + clean), `--file` missing → exit 2, `--file` without arg → exit 2, the regression test for the auto-detect bugfix (`lint <existing-file>` exits 1 on banned-vocab content), pure-text path-shape stays text, and both mutex pairs (`--file + positional`, `--stdin + --file`).
24
+
25
+ ### Why no L3 / pre-ship-review chain
26
+
27
+ `fix:` per spec §2 hard-upgrade exclusion list — the package's stated use case in `package.json#description` is "git pre-commit hooks, GitHub Actions, and other agents," all of which require path-based input; the silent-success on path arg failed that intent. L2 ceiling applies. Diff: 1 CLI file (~50 lines net), 1 test file (~70 lines added), 0 spec/contract change. Standalone-CLI consumers (`npx claudemd-cli lint ...`) get a stricter, less-footgun-prone surface; in-CC plugin behavior is unchanged.
28
+
29
+ ### Versioning
30
+
31
+ - `package.json`, `plugin.json`, `marketplace.json` (×2 fields) → `0.9.14`. Spec trio unchanged at v6.11.7.
32
+
33
+ ---
34
+
35
+ ## [0.9.13] - 2026-05-10
36
+
37
+ **Patch — `session-summary.sh` window calculation: missing `platform.sh` source + self-owned sentinel.** Spec v6.11.7 unchanged. Two coupled defects in window discipline, both surfaced while reasoning about the v0.9.12 fix's "Bug 2" follow-up.
38
+
39
+ ### Fixed
40
+
41
+ - `[fix]` **`hook_lib/platform.sh` was never sourced by `hooks/session-summary.sh`** (regression latent since v0.8.0 ship of the hook). The window-start computation guards on `command -v platform_stat_mtime`, but with `platform.sh` not sourced the function name was undefined, the guard silently fell false, `SINCE_TS` stayed empty, and **every** session-summary invocation since v0.8.0 took the 24h-rolling fallback path instead of the documented "since last Stop" window. Hard evidence in the wild: pre-fix `last-session-summary.json` shipped with `since == ts - 24h00m00s` exactly — only the fallback `date -u -d '24 hours ago'` produces that. Banner counts were therefore mixing rule activity across multiple sessions in the same calendar day.
42
+ - `[fix]` **Window sentinel decoupled from `session-start.ref`**. The shared sentinel was also being written by `hooks/sandbox-disposal-check.sh` during the same Stop event (declared earlier in `hooks/hooks.json`). With the platform.sh source restored above, that sharing would surface as a parallel/serial-ordering race — parallel hooks may let session-summary stat the file before sandbox-disposal touches it (OK), but a future CC harness running Stop hooks serially in declaration order (sandbox first / summary last) would produce `mtime == NOW` → empty window → banner permanently empty. New private sentinel `session-summary.lastrun` is read + always-touched by session-summary alone; sandbox-disposal continues to own `session-start.ref` for its tmp-dir scan baseline.
43
+ - `[fix]` **Always-touch sentinel before the no-event early-exit**. A no-event Stop (zero rule activity) previously exited without advancing the window, so the next session would silently extend the window backward through the gap. New code touches `SUMMARY_REF` unconditionally before the `total > 0` guard so window discipline is event-count-independent.
44
+
45
+ ### Added
46
+
47
+ - `[test]` **Cases 8 + 9 in `tests/hooks/session-summary.test.sh` (7/7 → 9/9)**:
48
+ - Case 8: SUMMARY_REF is created on no-event Stop (no summary file written, but sentinel exists for next-window boundary).
49
+ - Case 9: SUMMARY_REF mtime correctly gates the window — a row dated 5 minutes before sentinel mtime is excluded; a fresh row is included; final summary asserts `denies==1 total==1`. Pre-fix this case fails with `denies==2 total==2` (24h fallback regression detector).
50
+ - `[test]` **Case 2 + new cases retargeted from `session-start.ref` to `session-summary.lastrun`** to match the new ownership boundary.
51
+
52
+ ### Why no L3 / pre-ship-review chain
53
+
54
+ `fix:` per spec §2 hard-upgrade exclusion list — both items restore documented/intended hook behavior (the v0.8.0 R-N4 design comment in the source file already says "Window: from session-start.ref mtime to now," which the missing source line silently broke). L2 ceiling applies. Diff: 1 hook (~25 lines net), 1 test (~45 lines added), 0 spec/contract change.
55
+
56
+ ### Versioning
57
+
58
+ - `package.json`, `plugin.json`, `marketplace.json` (×2 fields) → `0.9.13`. Spec trio unchanged at v6.11.7.
59
+
60
+ ---
61
+
11
62
  ## [0.9.12] - 2026-05-10
12
63
 
13
64
  **Patch — `session-summary.sh` `top_section` bucket pollution fix.** Spec v6.11.7 unchanged. Empirical session run produced banner `[claudemd] last session: 61 denies, 3 bypasses, 134 warns, top: (unset)` despite the log holding 95 `§8.V4` warns and 54 `§10-V` denies in window — i.e. `(unset)` should not have won.
@@ -32,6 +32,7 @@ const USAGE = `claudemd-lint — §10-V banned-vocab + transcript scanner
32
32
 
33
33
  Usage:
34
34
  claudemd lint <text> Scan text for banned-vocab.
35
+ claudemd lint --file <path> Scan the contents of a file.
35
36
  claudemd lint --stdin Read text from stdin.
36
37
  claudemd audit <jsonl-path> Scan all assistant turns in a CC transcript.
37
38
  claudemd --version Print plugin version.
@@ -43,6 +44,12 @@ Flags:
43
44
  Default OFF — chat prose has different
44
45
  baseline conventions from commit messages.
45
46
 
47
+ Notes:
48
+ A bare \`lint <arg>\` whose only positional is an existing regular file
49
+ is auto-treated as \`--file <arg>\` so \`claudemd lint .git/COMMIT_EDITMSG\`
50
+ works as expected in pre-commit hooks. Pass --stdin or quote literal
51
+ text to opt out.
52
+
46
53
  Exit codes:
47
54
  0 no hits
48
55
  1 one or more hits
@@ -63,7 +70,37 @@ function readPackageVersion() {
63
70
  function lintCmd(args) {
64
71
  const json = args.includes('--json');
65
72
  const stdin = args.includes('--stdin');
66
- const positional = args.filter(a => !a.startsWith('--'));
73
+
74
+ // --file <path> consumes the next non-flag arg.
75
+ let filePath = null;
76
+ const fileIdx = args.indexOf('--file');
77
+ if (fileIdx !== -1) {
78
+ const next = args[fileIdx + 1];
79
+ if (!next || next.startsWith('--')) {
80
+ process.stderr.write('lint: --file requires a path argument\n');
81
+ process.exit(2);
82
+ }
83
+ filePath = next;
84
+ }
85
+ const positional = args.filter((a, i) => {
86
+ if (a.startsWith('--')) return false;
87
+ if (fileIdx !== -1 && i === fileIdx + 1) return false;
88
+ return true;
89
+ });
90
+
91
+ // Mutual-exclusion: pick one source — stdin > --file > positional.
92
+ if (stdin && filePath) {
93
+ process.stderr.write('lint: choose one of --stdin or --file, not both\n');
94
+ process.exit(2);
95
+ }
96
+ if (stdin && positional.length > 0) {
97
+ process.stderr.write('lint: --stdin and positional text are mutually exclusive\n');
98
+ process.exit(2);
99
+ }
100
+ if (filePath && positional.length > 0) {
101
+ process.stderr.write('lint: --file and positional text are mutually exclusive\n');
102
+ process.exit(2);
103
+ }
67
104
 
68
105
  let text;
69
106
  if (stdin) {
@@ -73,10 +110,34 @@ function lintCmd(args) {
73
110
  process.stderr.write(`lint: failed to read stdin: ${e.message}\n`);
74
111
  process.exit(2);
75
112
  }
113
+ } else if (filePath) {
114
+ if (!fs.existsSync(filePath)) {
115
+ process.stderr.write(`lint: file not found: ${filePath}\n`);
116
+ process.exit(2);
117
+ }
118
+ try {
119
+ text = fs.readFileSync(filePath, 'utf8');
120
+ } catch (e) {
121
+ process.stderr.write(`lint: failed to read ${filePath}: ${e.message}\n`);
122
+ process.exit(2);
123
+ }
76
124
  } else if (positional.length > 0) {
77
- text = positional.join(' ');
125
+ // Auto-detect: a bare single positional that is an existing regular file
126
+ // is overwhelmingly the user's intent (they're piping a commit-msg path
127
+ // from a git pre-commit hook). Without this, `claudemd lint message.txt`
128
+ // silently scans the LITERAL STRING "message.txt" → exits 0 even when
129
+ // the file contents would deny.
130
+ if (positional.length === 1) {
131
+ try {
132
+ const st = fs.statSync(positional[0]);
133
+ if (st.isFile()) {
134
+ text = fs.readFileSync(positional[0], 'utf8');
135
+ }
136
+ } catch { /* not a path — fall through to text */ }
137
+ }
138
+ if (text === undefined) text = positional.join(' ');
78
139
  } else {
79
- process.stderr.write('lint: text required (positional arg or --stdin)\n');
140
+ process.stderr.write('lint: text required (positional arg, --file PATH, or --stdin)\n');
80
141
  process.exit(2);
81
142
  }
82
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemd-cli",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
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": {