claudemd-cli 0.9.13 → 0.9.15
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 +50 -0
- package/bin/claudemd-lint.js +64 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,56 @@ 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.15] - 2026-05-10
|
|
12
|
+
|
|
13
|
+
**Patch — `memory-read-check.sh` two coupled silent-fail-open defects.** Spec v6.11.7 unchanged. Surfaced while end-to-end-testing the v0.9.14 fix from real cwd `/mnt/data_ssd/dev/projects/claudemd`: the §11 HARD memory-read hook had been a no-op for any project containing `_` in its path AND broke entirely when MEMORY.md held a tag beginning with `-`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `[fix]` **`tr '/.' '-'` → `tr '/._' '-'`** in `hooks/memory-read-check.sh:50` and `hooks/lib/rule-hits.sh:28`. Empirical: Claude Code encodes every non-`[a-zA-Z0-9-]` char to `-` (verified across all `~/.claude/projects/` entries). Pre-fix, any cwd containing `_` resolved to a non-existent `~/.claude/projects/-mnt-data_ssd-...` (vs CC's actual `-mnt-data-ssd-...`), the `[[ -f "$MEM_INDEX" ]] || exit 0` fail-open kicked in, and the §11 enforcement degraded to silent no-op. Stale-Memory note `feedback_cc_cwd_encoding_dots.md` claimed only `/` and `.` were encoded — incomplete; updated to reflect the broader rule.
|
|
18
|
+
- `[fix]` **`grep -qiF -- "$t"`** in `hooks/memory-read-check.sh:95` (and `--` added to the transcript-scan grep at L109 for parity). A MEMORY.md tag beginning with `-` (e.g. `--file`, `-h`) was parsed by `grep -qiF` as a flag, erroring `option '--file' requires an argument` and aborting the entire MEMORY scan with exit-0 fail-open. Discovered when v0.9.14's own MEMORY.md entry tagged itself with `[--file]` and broke the hook for the entire `git push` path.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `[test]` **3 new cases in `tests/hooks/memory-read-check.test.sh` (16/16 → 19/19)**:
|
|
23
|
+
- Case 17: cwd `/work/my_project` → underscore-encoded → tag match → deny.
|
|
24
|
+
- Case 18: cwd `/mnt/data_ssd/my.proj_v2` mixed `/`, `.`, `_` → all encoded → deny.
|
|
25
|
+
- Case 19: tag `--file` matched literally with `-- "$t"` separator → deny without grep crash.
|
|
26
|
+
|
|
27
|
+
### Why no L3 / pre-ship-review chain
|
|
28
|
+
|
|
29
|
+
`fix:` per spec §2 hard-upgrade exclusion — both items restore the hook's documented intent (HARD-block ship verbs without prior matching MEMORY.md Read). L2 ceiling. Diff: 2 hook files (~6 lines net), 1 test file (~50 lines added), 0 spec/contract change. Notable: this kind of bug class — silently-no-op'd HARD enforcement under specific cwd shapes — is the worst-quality fix to ship because users couldn't have noticed (no error signal); ratio of installed users on underscore-containing paths is high (Linux convention), so impact ≫ patch surface size.
|
|
30
|
+
|
|
31
|
+
### Versioning
|
|
32
|
+
|
|
33
|
+
- `package.json`, `plugin.json`, `marketplace.json` (×2 fields) → `0.9.15`. Spec trio unchanged at v6.11.7.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [0.9.14] - 2026-05-10
|
|
38
|
+
|
|
39
|
+
**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.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- `[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.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- `[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.
|
|
48
|
+
- `[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`).
|
|
49
|
+
- `[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`).
|
|
50
|
+
|
|
51
|
+
### Why no L3 / pre-ship-review chain
|
|
52
|
+
|
|
53
|
+
`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.
|
|
54
|
+
|
|
55
|
+
### Versioning
|
|
56
|
+
|
|
57
|
+
- `package.json`, `plugin.json`, `marketplace.json` (×2 fields) → `0.9.14`. Spec trio unchanged at v6.11.7.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
11
61
|
## [0.9.13] - 2026-05-10
|
|
12
62
|
|
|
13
63
|
**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.
|
package/bin/claudemd-lint.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.9.15",
|
|
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": {
|