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 +21 -0
- package/README.md +8 -8
- package/bin/claudemd-lint.js +15 -0
- package/package.json +1 -1
- package/scripts/lib/lint.js +22 -0
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
|
|
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
|
-
|
|
|
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 [
|
|
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 +
|
|
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
|
|
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/ #
|
|
338
|
+
├── commands/ # 15 slash-command markdown files
|
|
339
339
|
├── bin/ # standalone CLI entrypoint (claudemd-lint.js → `npx claudemd-cli` on npmjs.org)
|
|
340
|
-
├── scripts/ #
|
|
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/
|
package/bin/claudemd-lint.js
CHANGED
|
@@ -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.
|
|
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": {
|
package/scripts/lib/lint.js
CHANGED
|
@@ -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') {
|