claudemd-cli 0.35.0 → 0.37.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,27 @@ 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.37.0] - 2026-07-11
12
+
13
+ **Minor — QA self-test loop output: `audit` string-shape skip warning (user-visible) + test-suite env hygiene + README↔implementation drift fixes.** Product of a 3-round QA self-test loop over CLI / lifecycle scripts / all 16 hooks / docs (8 issues filed, 8 fixed + replay-verified; ledger kept as local-only `qa/`). Spec unchanged (stays v6.17.0).
14
+
15
+ - **User-visible change** (`bin/claudemd-lint.js` + `scripts/lib/lint.js`): `claudemd-cli audit` now prints one stderr warning when the transcript carries assistant rows whose `message.content` is a plain string rather than the CC block array — such rows are outside `parseTranscript`'s input domain and were silently unscanned (same silent-success family as the v0.9.14 / v0.9.21 guards; real CC transcripts always use block arrays, but the CLI is documented for other-agent exports). Verdict and exit codes are unchanged; `--json` stdout stays pure JSON; pure block-array transcripts see no new output. The warning itself is the discoverability signal. Opt-out / revert: the warning is stderr-only (exit-code/stdout pipelines unaffected); to silence entirely, pin the prior version (`npx claudemd-cli@0.36.0`).
16
+ - **Fix** (`tests/run-all.sh` + new `tests/lib/env-hygiene.sh`): user-tunable claudemd env knobs inherited from the invoking shell (e.g. `DISABLE_RULE_HITS_LOG=1` — the documented telemetry-hygiene export for manual hook probing — or a live `TRANSCRIPT_STRUCTURE_SCAN=1` opt-in) flipped 15 suites red with no hint. `run-all.sh` now scrubs the `DISABLE_*` / `CLAUDEMD_*` families plus the explicit opt-in knobs before any suite runs. Known limit: directly invoking a single suite file still inherits the caller env.
17
+ - **Docs** (`README.md`, 6 drift fixes, implementation-is-correct in every case): uninstall state/log clearing is `CLAUDEMD_PURGE=1`-gated (command table + two-step-flow comment said otherwise); install backup wording now matches the v0.23.11 spec-on-spec no-backup design; `/claudemd-audit` flag documented as `--days=N` (= form only); `transcript-vocab-scan` marked opt-in default-OFF; `mem-audit` row rewritten to the current memory-file structure audit (old citation-scan description predated the hook's repurpose); §Project layout counts corrected (commands 12 → 15, scripts 16 → 18).
18
+ - **Tests**: node runner 659 → 662 (lint-cli +3: warning content+count, `--json` stdout purity under warning, no-noise on block-array transcripts) plus a new env-hygiene shell suite (3 cases, outside the node count).
19
+
20
+ ## [0.36.0] - 2026-07-11
21
+
22
+ **Minor — stale-cache downgrade loop fixed (never-downgrade guard + hook direction gates + doctor staleness check) + suppress-source pre-dedupe logging.** Fixes the reproduced 2026-07-11 defect (`tasks/manifest-pluginroot-stale-cache.md`): CC keeps versioned plugin cache dirs and can fire hooks from a STALE one after an upgrade; the bootstrap hooks' direction-blind version comparison (`!=` treated any mismatch as "upgrade") then ran the old root's `install.js`, regressing `~/.claude` spec + manifest every session — observed as v6.16.0 / v6.15.1 flapping. Spec unchanged (stays v6.17.0).
23
+
24
+ - **Fix** (`scripts/install.js`): never-downgrade guard at the choke point — refuses to install a version older than the manifest records (strict-semver compare, before ANY mutation incl. backups), with the refresh sequence in the error message. Deliberate rollbacks: `CLAUDEMD_ALLOW_DOWNGRADE=1`. Non-semver versions (dev-mode `unknown`, `9.9.9-test` fixtures) skip the guard — fail-open. RED anchor: sandbox repro downgraded home spec v6.16.0 → v6.15.1 and manifest 0.34.0 → 0.33.0 pre-guard.
25
+ - **Fix** (`hooks/session-start-check.sh` + `hooks/version-sync.sh`): direction gate in the mismatch branch — manifest NEWER than the hook's own plugin root means stale registration; the spawn is skipped (pre-gate the bootstrap log recorded `auto-upgrade: manifest 9.9.9 → plugin 0.35.0` and downgraded), a `stale-root` rule-hits row is written (`extra: {hook_version, installed_version}`), and SessionStart banners the 4-command refresh (version-sync keeps its 0-byte-stdout contract; bootstrap log carries the trail).
26
+ - **Feature** (`scripts/doctor.js`): `plugin cache:staleness` check — manifest.pluginRoot exists but holds an older plugin than the marketplace dir; surfaces the refresh fix. Skipped when either side lacks a strict-semver version.
27
+ - **Fix** (`hooks/memory-prompt-hint.sh`, v0.35.0 review finding #5): `suppress-source` now logs BEFORE the dedupe — a relay prompt whose matches were all previously suggested to a human prompt exited without a row, under-counting exactly the avalanche the event exists to measure. Row consequently carries the post-unRead pre-dedupe list in authoring order (documented in `docs/RULE-HITS-SCHEMA.md`).
28
+ - **Lib** (`scripts/lib/paths.js`): `SEMVER_RE` + `semverCmp` exported (shared by install guard + doctor check).
29
+ - **Tests**: install 3 new (guard refusal with no-mutation assertions, `CLAUDEMD_ALLOW_DOWNGRADE=1` rollback, non-semver fail-open); session-start 17 → 18 (stale gate: no spawn + banner + telemetry + manifest untouched); version-sync 7 → 8 (stale gate piggy-back); memory-prompt-hint 22 → 23 (all-deduped relay still rows); doctor +3 (staleness flag / current / not-comparable); paths +2 (`semverCmp`, `SEMVER_RE`); contract DOCUMENTED +2 (`stale-root` ×2 emitters).
30
+ - **Residual risk** (documented in the task file): pre-0.36.0 cache dirs still hold ungated hooks + unguarded install.js; a session registered against one of those can still downgrade until they age out of cache-prune (keep:3) or the user refreshes. Forward-looking protection only. `update.js` apply-all intentionally stays outside the guard — it is user-gated (diff shown first, explicit `CLAUDEMD_UPDATE_CHOICE=apply-all`), i.e. the sanctioned explicit-choice path.
31
+
11
32
  ## [0.35.0] - 2026-07-11
12
33
 
13
34
  **Minor — spec v6.17.0 audit letter-fix batch + R1 hint noise controls + R2 Tier-2 index budget.** Executes the 2026-07-11 four-method spec-audit recommendation backlog (`tasks/spec-audit-2026-07-11.md`) end-to-end: A-class plugin work (R1/R2/R3), B-class candidate-pool bookkeeping (C2 closed, C5/C6 consumed), C-class spec letter fixes (7 core edits, net −91B).
package/README.md CHANGED
@@ -63,7 +63,7 @@ Verify in one command (Linux): `node --version && jq --version && gh --version &
63
63
  | Spec v6.17 | `~/.claude/CLAUDE.md` · `CLAUDE-extended.md` · `CLAUDE-changelog.md` · `OPERATOR.md` (backup-before-overwrite) |
64
64
  | StatusLine (opt-out) | PS1-style line — `user@host:/path (branch) Model [ctx:N% · 5h:N% · 7d:N%]` (context / 5-hour quota / weekly quota, all **used %**, read from Claude Code's `rate_limits` payload; quota segments auto-hide when the data is absent, or force-hide with `DISABLE_STATUSLINE_QUOTA=1`) — wired into `~/.claude/settings.json` on install **only when the slot is empty**; an existing statusline is left untouched. Skip entirely with `CLAUDEMD_NO_STATUSLINE=1`. Manage via `/claudemd-statusline`. |
65
65
 
66
- Install moves any existing `~/.claude/CLAUDE*.md` to `~/.claude/backup-<ISO>/` (last 5 kept automatically). Uninstall offers `keep / restore / delete`; `delete` requires an extra confirmation.
66
+ Install backs up a hand-written `~/.claude/CLAUDE.md` (any file without the `# AI-CODING-SPEC` H1) to `~/.claude/backup-<ISO>/` before overwriting (last 5 kept automatically). An already-installed claudemd spec is overwritten **without** a backup — deliberate (v0.23.11): the sole backup is always your own content, so `restore` can never return a stale spec instead. Uninstall offers `keep / restore / delete`; `delete` requires an extra confirmation.
67
67
 
68
68
  > Since v0.1.5, hook registration lives in the plugin's own `hooks/hooks.json` — the Claude Code harness expands `${CLAUDE_PLUGIN_ROOT}` there automatically on every invocation, so hooks track the active plugin version without manual re-registration. `install.js`'s remaining jobs are (1) copy `spec/CLAUDE*.md` into `~/.claude/` (with backup-before-overwrite), (2) evict any legacy claudemd hook entries from prior installs (≤0.1.1 absolute-path form, 0.1.2-0.1.4 `${CLAUDE_PLUGIN_ROOT}`-in-settings.json form), and (3) write the installed manifest. It never touches other-plugin hooks. Claude Code's plugin-lifecycle `postInstall` field is not honored, so the script runs from `SessionStart` instead.
69
69
 
@@ -81,11 +81,11 @@ Once installed, hooks run silently in the background. Verbose log: `~/.claude/lo
81
81
  | Bash command matching ship/push/deploy/release with an unread matched `MEMORY.md` entry | `memory-read-check` | Blocks the command with a list of memory files to Read first. |
82
82
  | Session end with `~/.claude/tmp/` growth > 20 entries | `residue-audit` | Advisory stderr warning; never blocks. |
83
83
  | Session end with fresh `tmp.XXXXXX`-style directories | `sandbox-disposal-check` | Advisory stderr warning. |
84
- | Stop event with `Why:`-less hard-rule citations in the assistant turn | `mem-audit` (v0.9.4+) | Detects spec-citation patterns missing the `Why:` rationale token; advisory log. |
84
+ | Session end (Stop), at most once per 24h | `mem-audit` (v0.9.4+) | Scans CC auto-memory `~/.claude/projects/*/memory/feedback_*.md` for missing `**Why:**` / `**How to apply:**` body structure plus MEMORY.md ↔ files index drift; advisory stderr, never blocks. |
85
85
  | Session end | `session-summary` (v0.8.0+) | Writes `~/.claude/.claudemd-state/last-session-summary.json`; banner emit at next `SessionStart`. |
86
86
  | New session start with GitHub remote tag newer than local cache max version | `session-start-check` (v0.4.0+) | Injects an "upgrade available" banner via `additionalContext`. Rate-limited to once per 24h via `~/.claude/.claudemd-state/upstream-check.lastrun` sentinel. 3-second `git ls-remote` timeout, fail-open. |
87
87
  | First `UserPromptSubmit` after a mid-session `/plugin install` upgrade | `version-sync` (v0.3.1+) | Backgrounds `install.js` once per session when the manifest version diverges from the active plugin's `package.json`. Sentinel-gated; fail-open. |
88
- | `PostToolUse` after assistant text containing banned vocab | `transcript-vocab-scan` | Advisory; logs to rule-hits without blocking. |
88
+ | `PostToolUse` after assistant text containing banned vocab | `transcript-vocab-scan` | Advisory; logs to rule-hits without blocking. Opt-in (`TRANSCRIPT_VOCAB_SCAN=1`, default OFF) for FP signal collection. |
89
89
  | Session end with last assistant turn carrying §10 four-section out of order, `Done:` lines lacking evidence fingerprints, or `Uncertain:` short hedges without `because` | `transcript-structure-scan` (v0.9.10+) | Stop advisory — closes the audit gap that ~7 self-enforced HARD rules (§iron-law-2 / §10-four-section-order / §10-honesty) had no hook-side feedback signal. Opt-in (`TRANSCRIPT_STRUCTURE_SCAN=1`, default OFF) for FP signal collection; FP-tightened so single-section `Done:` lines never trigger. |
90
90
 
91
91
  ### Execution order (PreToolUse:Bash)
@@ -108,7 +108,7 @@ Per-hook timeout (3-5s in `hooks.json`); timeout = treated as exit 0 (pass) per
108
108
  | `/claudemd-install` | Bootstrap the current session right after `/plugin install` (copy spec into `~/.claude/`, write manifest, evict legacy entries). Idempotent. CC does not fire `postInstall`, so without this command `install.js` waits until the next `SessionStart`. |
109
109
  | `/claudemd-status [--verbose]` | Plugin version + spec version + kill-switch state + logs line count. `--verbose` adds per-hook env-var × event × effective vs persisted state table + 5 escape-token reference. |
110
110
  | `/claudemd-update` | Interactive diff against plugin-shipped spec, then apply-all or cancel (4-file spec set is lockstep — per-file select would dangle §EXT cross-references). |
111
- | `/claudemd-audit [--days N]` | Aggregate rule-hits over last N days (default 30). Top banned-vocab patterns, per-hook deny counts. |
111
+ | `/claudemd-audit [N]` | Aggregate rule-hits over last N days (default 30). Top banned-vocab patterns, per-hook deny counts. Slash form takes a bare number (`/claudemd-audit 90`); direct script invocation takes `--days=N` (= form only). |
112
112
  | `/claudemd-toggle <hook-name>` | Enable/disable a specific hook by toggling `DISABLE_*_HOOK` in `settings.json` env. |
113
113
  | `/claudemd-doctor [--prune-backups=N]` | Health checks; optionally prune `~/.claude/backup-*` dirs older than N. v0.7.1+ also flags rule sections whose bypass:deny ratio > 50% (R-N6 §0.1 demotion candidates). |
114
114
  | `/claudemd-rules [N]` | v0.8.0+ — audit `spec/hard-rules.json` manifest over last N days (default 30 — lowered from 90d in v0.13.1 after the 90d gate was structurally unreachable under typical log retention). Surfaces `demoteCandidates` (hook-enforced rules with 0 hits) and `staleReviews` (rules whose `last_demote_review` is null/old). |
@@ -116,7 +116,7 @@ Per-hook timeout (3-5s in `hooks.json`); timeout = treated as exit 0 (pass) per
116
116
  | `/claudemd-clean-residue [--apply]` | Dry-run-by-default cleanup of stale `claudemd-sync-*` sentinels and historical `claudemd-(mockgh\|work).*` test sandboxes. |
117
117
  | `/claudemd-design-adopt [check\|remove]` | v0.24.0 — for a UI project, generate a thin, fact-based `DESIGN.md` from its real design-token sources (deterministic detector `scripts/design-detect.js`; evidence-gated rules menu; never invents values) and wire a sentinel block into project CLAUDE.md. `check` verifies pointers resolve; `remove` unwires. Always shows the diff and asks before writing. Manual/opt-in — no SessionStart hook, nothing auto-fires. |
118
118
  | `/claudemd-statusline [check\|remove] [--force]` | v0.25.0 — register claudemd's PS1-style statusLine into `~/.claude/settings.json`. Default adopts into an empty slot; `check` reports the current owner with no writes; `remove` restores the prior statusline (or clears the slot) and deletes `~/.claude/claudemd-statusline.sh`; `--force` takes over another provider's slot, saving its command so `remove` can restore it. Always shows the diff and asks before writing. Install-time auto-adopt is empty-slot-only (opt-out: `CLAUDEMD_NO_STATUSLINE=1`). When another composite provider (e.g. code-graph) owns the slot, claudemd registers as a guest so both segments render; `--supersede=<id>` replaces a named provider. |
119
- | `/claudemd-uninstall` | Pre-uninstall cleanup: clears manifest + state + log + legacy `settings.json` hook entries. Run BEFORE `/plugin uninstall claudemd@claudemd` (see [Uninstall](#uninstall)). |
119
+ | `/claudemd-uninstall` | Pre-uninstall cleanup: clears manifest + legacy `settings.json` hook entries; add `CLAUDEMD_PURGE=1` to also drop `~/.claude/.claudemd-state/` + the rule-hits log. Run BEFORE `/plugin uninstall claudemd@claudemd` (see [Uninstall](#uninstall)). |
120
120
 
121
121
  ---
122
122
 
@@ -268,7 +268,7 @@ The command prints per-file diff summary, then prompts `apply-all` or `cancel`.
268
268
  Claude Code's marketplace lifecycle does not fire `preUninstall`, so `/plugin uninstall claudemd@claudemd` alone leaves orphan state behind (`~/.claude/.claudemd-manifest.json`, `~/.claude/.claudemd-state/`, `~/.claude/logs/claudemd.jsonl`). Use the **two-step flow**:
269
269
 
270
270
  ```
271
- /claudemd-uninstall # clear manifest + state + log (plugin still installed)
271
+ /claudemd-uninstall # clear manifest (+ state & log with CLAUDEMD_PURGE=1)
272
272
  /plugin uninstall claudemd@claudemd # CC removes plugin cache itself
273
273
  ```
274
274
 
@@ -335,9 +335,9 @@ claudemd/
335
335
  │ └── marketplace.json # marketplace catalog entry
336
336
  ├── hooks/ # 16 shell hooks + hooks/lib/ (hook-common, rule-hits, platform)
337
337
  │ └── hooks.json # authoritative hook registration (v0.1.5+); CC expands ${CLAUDE_PLUGIN_ROOT} here
338
- ├── commands/ # 12 slash-command markdown files
338
+ ├── commands/ # 15 slash-command markdown files
339
339
  ├── bin/ # standalone CLI entrypoint (claudemd-lint.js → `npx claudemd-cli` on npmjs.org)
340
- ├── scripts/ # 16 Node.js management scripts + scripts/lib/ (single-source registry, lint, etc.)
340
+ ├── scripts/ # 18 Node.js scripts + scripts/lib/ (single-source registry, lint, etc.)
341
341
  ├── spec/ # shipped v6.17.0 CLAUDE*.md trio + OPERATOR.md + hard-rules.json manifest
342
342
  ├── tests/ # hook shell tests + Node.js tests + integration + fixtures
343
343
  ├── docs/ # ADDING-NEW-HOOK.md + RULE-HITS-SCHEMA.md + superpowers/
@@ -21,6 +21,7 @@ import {
21
21
  scan,
22
22
  readPatterns,
23
23
  parseTranscript,
24
+ countStringContentAssistantRows,
24
25
  formatHumanReadable,
25
26
  formatJSON,
26
27
  } from '../scripts/lib/lint.js';
@@ -329,6 +330,20 @@ function auditCmd(rawArgs) {
329
330
  }
330
331
 
331
332
  const turns = parseTranscript(jsonl);
333
+ // QA ISSUE-002 (option c): string-shape assistant rows are outside the
334
+ // block-array input domain and never reach the scanner. Keep the verdict
335
+ // based on scanned turns, but say so on stderr — a silent skip here is the
336
+ // same silent-success family as the isFile()/no-type guards above. User
337
+ // rows are legitimately string-shape (typed prompts); only assistant rows
338
+ // count.
339
+ const skippedStringRows = countStringContentAssistantRows(jsonl);
340
+ if (skippedStringRows > 0) {
341
+ process.stderr.write(
342
+ `audit: warning: skipped ${skippedStringRows} assistant row(s) with string-shape ` +
343
+ `message.content — not the Claude Code block-array shape, so their text was NOT scanned. ` +
344
+ `Non-CC transcript export?\n`
345
+ );
346
+ }
332
347
  const patterns = readPatterns();
333
348
  const annotated = turns.map(t => ({
334
349
  ...t,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemd-cli",
3
- "version": "0.35.0",
3
+ "version": "0.37.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": {
@@ -163,6 +163,28 @@ export function parseTranscript(jsonlText) {
163
163
  return turns;
164
164
  }
165
165
 
166
+ // countStringContentAssistantRows(jsonlText) → number
167
+ // QA ISSUE-002: an assistant row whose .message.content is a STRING (not the
168
+ // CC block array) is outside parseTranscript's input domain — the for..of
169
+ // over a string yields characters, texts stays empty, and the row is
170
+ // silently skipped, so its text is never scanned. Real CC transcripts
171
+ // always use block arrays for assistant turns (only typed user prompts are
172
+ // string-shape), but the CLI is documented for other-agent exports too.
173
+ // The CLI uses this count to surface the skip on stderr (verdict unchanged)
174
+ // — same silent-success family as the v0.9.14 / v0.9.21 guards.
175
+ export function countStringContentAssistantRows(jsonlText) {
176
+ let count = 0;
177
+ for (const line of jsonlText.split('\n')) {
178
+ if (!line.trim()) continue;
179
+ let row;
180
+ try { row = JSON.parse(line); } catch { continue; }
181
+ if (row?.type !== 'assistant') continue;
182
+ const content = row.message?.content;
183
+ if (typeof content === 'string' && content.trim().length > 0) count++;
184
+ }
185
+ return count;
186
+ }
187
+
166
188
  // Format helpers — keep the CLI thin.
167
189
  export function formatHumanReadable({ scope, hits, turns }) {
168
190
  if (scope === 'lint') {