claudemd-cli 0.58.1 → 0.59.0

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,60 @@ 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.59.0] - 2026-07-26
12
+
13
+ Clears the audit's deferred queue: all 17 items from `tasks/audit-2026-07-26-deferred.md` A+B, in one release. Minor bump: two instruments change what they report and one hook gains a banner on a path where it previously stayed silent. No §8 verdict moves — the differential against the pre-fix hook is still 0 changes across all 330 baseline corpus rows.
14
+
15
+ Several of these are the same defect wearing different clothes: a measurement whose answer moved with an unrelated flag, a check that reported green when it had not run, a constant hand-copied next to a comment naming its source.
16
+
17
+ ### Also in this release — the three "claimed enforcement" items
18
+
19
+ Written up separately while 0.58.2 was in flight; that version was never tagged, so it ships here.
20
+
21
+ Closes the three remaining audit items that share a root with what 0.58.0 fixed — an artifact claiming enforcement or source-of-truth status that nothing backed. Everything else from that audit is triaged in `tasks/audit-2026-07-26-deferred.md` for the next §13.2 batch review. No behavior change: hooks are untouched apart from one comment.
22
+
23
+ - **fix: `spec/hard-rules.json` §11-memory-read `enforcement: "hook"` → `"both"`.** `memory-read-check.sh`'s trigger regex covers the ship/release verbs only — `grep -cE "destructive|L3"` over that hook returns 0 — while the rule reads "HARD at ship/release/destructive-path/L3". §13 META directs the agent to calibrate from this field, so it was promising mechanical coverage for two triggers that are Agent-enforced. `§10-specificity` already used `both` plus a coverage note for the same shape.
24
+ - **fix: Iron Law #3 gains a manifest entry.** It is graded by heading convention rather than a `(HARD)` token, so the manifest's coverage scan never required one and the rule was structurally invisible to §13.1/§13.2 accounting. Iron Laws #1 and #2 both had entries. Partition is now 6 hook / 16 self / 2 both / 1 external over 25 rules.
25
+ - **fix: `docs/ARCHITECTURE.md`'s hook-taxonomy table, plus the gate it never had.** The table calls itself source-of-truth for the literal `spec_section` arguments hooks pass, and had drifted twice: `§8-curl-sh` was absent though the curl|sh gate has filed denies under it since v0.51.x, and `session-start-check` was listed `n/a` while emitting `§11-post-compaction`. README's counts had `readme-drift.test.js`; this was the one shipped doc with a source-of-truth claim and nothing behind it. New `tests/scripts/architecture-drift.test.js` extracts sections from `hooks/**/*.sh` and requires each to appear in the table, both directions on the hook list; mutation-verified by deleting `§8-curl-sh` from the table and confirming red.
26
+ - **docs: the §8 comment claiming F24 closed option-with-arg wrapper forms now says which side.** True for the fetch side, which runs through `s8_strip_wrappers`; the sink side is matched by a regex that consumes no options, so `curl … | sudo -u root bash` remains allowed. Pre-existing and recorded as accepted residual, not a regression.
27
+
28
+ ### Instruments that measured the wrong thing
29
+
30
+ - **`hard-rules-audit`: review cadence decoupled from the audit window.** `staleReviews` used `--days` as its freshness threshold, so "which rules are overdue for review" moved with a flag about how far back to count hits — `--days=30` returned none, `--days=7` returned all 23. New `REVIEW_CADENCE_DAYS = 90`. An unparseable `last_demote_review` also produced `NaN < cutoff` = false, reading garbage as "reviewed recently"; it now counts as stale.
31
+ - **`memory-maintenance`: a log line with no parseable `ts` no longer counts as in-window.** Its `.md` mentions entered the liveness set unconditionally, so a genuinely stale memory could never surface. `rule-hits-parse.js` already treats a null ts as corruption rather than epoch-0.
32
+ - **`version-cascade-check`: presence floor.** The scan only reported tokens whose minor disagreed, so a file that lost every `vX.Y` mention produced an empty offender list — identical output to "all correct". These files are scanned precisely because they must carry the version.
33
+ - **`lesson-bypass-audit`: `EMIT_CAP` is read from `hooks/memory-prompt-hint.sh`'s `MAX=` line** instead of a hand-copied `5` sitting beside a comment naming that constant. Raising the hook's cap would have silently sliced the extra suggestions off before scoring.
34
+
35
+ ### Checks that reported green without running
36
+
37
+ - **`doctor`: a SKIPPED `memory-maintenance:promote` now reports not-ok.** `promoteSkipped != null || length === 0` rendered "could not run" and "0 candidates" the same colour.
38
+ - **`install-drift` is bidirectional.** It iterated source only, so a hook present in the marketplace root and absent from source — exactly what retiring a hook leaves behind, the shape v0.57.0 created — produced no diff.
39
+ - **`excludeTestSessions` keeps a non-string `session_id`.** `(12345).length` is undefined and `undefined > 7` is false, so such a row vanished from every audit view without being counted anywhere, unlike the deliberate sentinel filter which reports `testSessionsFiltered`.
40
+
41
+ ### Reachability and contracts
42
+
43
+ - **`session-start-check`: the session-summary banner fires on the fall-through path.** Its only call site was inside the version-MATCH branch, so the session right after every upgrade — and every fresh install — showed nothing. Merged with the bootstrap-failed banner through `jq -s`; verified across all four banner states that stdout is exactly one JSON object, or empty when neither fires.
44
+ - **`claudemd-cli`: `--help` works in any argv position** (`lint --help` exited 2 with "unknown flag").
45
+ - **`version-cascade-check`'s generic catch exits 1, not 2.** 2 is documented as "argv-shape error", which made a malformed `hard-rules.json` indistinguishable from a typo'd flag.
46
+
47
+ ### Single-sourcing and hygiene
48
+
49
+ - **`paths.js` gains `projectsRoot()` and `projectDir()`.** The cwd ENCODER was single-sourced by an earlier audit; the directory it feeds was still rebuilt from a `.claude/projects` literal in five call sites.
50
+ - **`status.js` uses `settingsPath()`** instead of a hand-rolled path whose `''` HOME fallback (cwd-relative) diverged from `paths.js`'s `os.homedir()`, silently emptying the kill-switch drift report when HOME was unset.
51
+ - **`doctor`'s `which()` resolves once per binary** (was four `execSync` spawns for two lookups).
52
+
53
+ ### Two deferred items closed as "premise did not hold"
54
+
55
+ Pre-tag review measured both claims and neither survived; recorded here rather than shipped as fixes.
56
+
57
+ - **The slashed-path sanitizer does not blank `12/12`.** The item said a lone-`/` match was eating ratios in commit messages. Measured: the first alternative matches `12/12` in full either way, so splitting the alternation changed only whitespace-on-both-sides (`and / or`) — and no shipped ratio pattern matches an N/M shape at all (they are `N% faster` / `Nx faster` plus the 中文 equivalents). Reverted rather than carry a 2x cost on long class-character runs for a fix that was not one.
58
+ - **`audit` deliberately does NOT honor `[allow-banned-vocab]`.** The item read the lint-honors/audit-ignores split as an asymmetry. It is not: `lint` scans text the caller hands it, so the token is explicit intent, while `audit` scans a transcript of assistant turns where the token is incidental — and in this repo, turns discussing the escape hatch are routine, so honoring it would let a turn suppress its own scan by mentioning it. The hook agrees (it reads the token from the Bash command, never from the prose it scans), `README.md` and `status.js` both scope it to "commit message", and `sampling-audit.js` does not honor it either — adding it to `audit` alone would have moved the asymmetry, not removed it. The reasoning is now a comment at the call site.
59
+
60
+ ### Test gates
61
+
62
+ - **`rule-hits` ARCH-2 parity pins `LC_CTYPE`.** `rule-hits.sh` documents that its slicing needs a UTF-8 locale and degrades byte-wise under `LC_ALL=C`, but nothing set it — the verdict was inherited from the invoking shell, so a green run did not say which mode ran. Adds a floor asserting the documented C-locale degradation still yields a usable name.
63
+ - **`hard-rules-4` asserts its own premise.** Its null-section arm joins on the rule id, which is sound only while ids and section names share a namespace — previously an unstated coincidence that would let a future rule pass vacuously.
64
+
11
65
  ## [0.58.1] - 2026-07-26
12
66
 
13
67
  Hotfix for a macOS regression shipped in 0.58.0. Live enforcement was off on macOS: `hooks/lib/hook-common.sh` failed to parse under `/bin/bash` 3.2, and every hook sources it.
package/README.md CHANGED
@@ -352,7 +352,7 @@ claudemd/
352
352
  ├── commands/ # 16 slash-command markdown files
353
353
  ├── bin/ # standalone CLI entrypoint (claudemd-lint.js → `npx claudemd-cli` on npmjs.org)
354
354
  ├── scripts/ # 18 Node.js scripts + scripts/lib/ (single-source registry, lint, etc.)
355
- ├── spec/ # shipped v6.23.0 CLAUDE*.md trio + OPERATOR.md + hard-rules.json manifest
355
+ ├── spec/ # shipped v6.23.1 CLAUDE*.md trio + OPERATOR.md + hard-rules.json manifest
356
356
  ├── tests/ # hook shell tests + Node.js tests + integration + fixtures
357
357
  ├── docs/ # ADDING-NEW-HOOK.md + RULE-HITS-SCHEMA.md + superpowers/
358
358
  └── .github/workflows/ # ci.yml (ubuntu+macOS × node 20) + npm-publish.yml (tag-triggered)
@@ -345,6 +345,17 @@ function auditCmd(rawArgs) {
345
345
  );
346
346
  }
347
347
  const patterns = readPatterns();
348
+ // NO escape hatch here, deliberately. A 2026-07-26 audit item read the
349
+ // lint-honors/audit-ignores split as an asymmetry to close; it is not one.
350
+ // `lint` scans text the caller hands it, so the token is that caller's explicit
351
+ // intent. `audit` scans a TRANSCRIPT of assistant turns, where the token is
352
+ // incidental text — and in this repo, turns discussing the escape hatch are
353
+ // routine, so honoring it here lets a turn suppress its own scan by mentioning
354
+ // it. The hook agrees: banned-vocab-check.sh:90 reads the token from the Bash
355
+ // command, never from the prose it scans (pinned by banned-vocab.test.sh:243),
356
+ // README.md:241 and status.js:24 both scope it to "commit message", and
357
+ // sampling-audit.js does not honor it at all — adding it here would have moved
358
+ // the asymmetry rather than removed it.
348
359
  const annotated = turns.map(t => ({
349
360
  ...t,
350
361
  hits: scan(t.text, { excludeRatio: !includeRatio, patterns, sanitize: true }),
@@ -363,7 +374,11 @@ function auditCmd(rawArgs) {
363
374
 
364
375
  function main() {
365
376
  const argv = process.argv.slice(2);
366
- if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
377
+ // `--help` in ANY position (2026-07-26 audit). Recognizing it only as argv[0]
378
+ // meant `claudemd-cli lint --help` exited 2 with "unknown flag '--help'" — the
379
+ // exact discoverability bug lib/argv.js#printHelpAndExit was written to fix,
380
+ // which this CLI predates.
381
+ if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) { // argv-lint:allow — help detection runs before subcommand routing; --help takes no value
367
382
  process.stdout.write(USAGE + '\n');
368
383
  process.exit(argv.length === 0 ? 2 : 0);
369
384
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemd-cli",
3
- "version": "0.58.1",
3
+ "version": "0.59.0",
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": {
@@ -120,6 +120,13 @@ export function stripIdentifiers(text) {
120
120
  // 2. Inline backtick spans.
121
121
  .replace(/`[^`]*`/g, ' ')
122
122
  // 3. Slashed-path runs (branch names, file paths, URLs) — Path 2's rule.
123
+ // A 2026-07-26 audit item claimed this blanked `12/12` / `2/3` and that
124
+ // splitting the alternation would preserve them. Measured: alternative 1
125
+ // matches `12/12` in full either way, so the split changed only
126
+ // whitespace-on-both-sides (`and / or`) — and no shipped ratio pattern
127
+ // matches an N/M shape at all (they are `N% faster` / `Nx faster` and the
128
+ // 中文 equivalents). The premise did not hold; reverted rather than ship a
129
+ // 2x cost on long class-character runs for a fix that was not one.
123
130
  .replace(/[A-Za-z0-9._@~-]*\/[A-Za-z0-9._/@~-]*/g, ' ')
124
131
  // 4. Bare dotted-file tokens (foo.js, comprehensive-parser.ts) — CLI
125
132
  // extension. The extension must start with a LOWERCASE letter, which