Skip to content

Releases: biznuts/never-stale

v0.11.0 — gate hardening, honest CI, i18n catch-up

Choose a tag to compare

@biznuts biznuts released this 12 Jul 14:43
5c0721f

Adversarial full-project review → three PRs: gate hardening (#10), docs/CI truthfulness (#11), i18n catch-up (#12). Every gate defect was empirically reproduced before fixing and re-verified after. Suite: 92 tests, now run on Linux + Windows × Node 20/24.

Fixed (the gate)

  • Symlink/junction escape in syncPairs paths — a relative path could resolve through a symlink or junction to a file outside the repo (junctions are privilege-free on Windows). Every pair path is now realpath-confined to the repo root's real path; escaping pairs are skipped and the reminder stays byte-identical to the unconfigured one.
  • A corrupt local marker no longer silently disables a team opt-in — a .local.json that fails to parse falls back to the committed team marker beside it; a parsing local marker still overrides (including an enabled: false veto).
  • UTF-8 BOM no longer disables a marker (common with Windows editors) — stripped before JSON.parse.
  • Windows path-case mismatches no longer suppress the gate (x:\ vs X:\) — containment and pair-matching compares case-fold on win32.
  • TOCTOU in the capped readhash-mode source reads are fd-based (open → fstat → read the stat'd size, ≤ 512 KB), refusing non-regular files.
  • Mode-aware edit-retarget wording — "(and its synced-to marker)" only for hash pairs; reserved modes never retarget. Multi-pair drift lines each carry the [never-stale] prefix.

Added

  • NotebookEdit in the PostToolUse matcher — notebook edits get the same reminder and retargeting as Edit/Write/MultiEdit.
  • 13 regression tests for all of the above, and test/release.test.mjs pinning every version stamp to plugin.json so a bump cannot ship half-done.

Changed

  • CI is honest and broader: ubuntu + windows × Node 20/24 (the win32 junction/case-fold paths are now actually exercised); the marketplace version step no longer passes a vacuous comparison silently.
  • Docs truthfulness sweep: declared/version modes consistently described as deferred — not planned; CONTRIBUTING documents all four test files; stale bare /never-stale references fixed.
  • Translations caught up: all four READMEs (zh-Hant, zh-Hans, ja, ko) gain the full syncPairs section (absent since v0.9.0) plus this release's changes.

Full changelog: v0.10.1...v0.11.0

v0.10.1 — linear normalize (ReDoS fix)

Choose a tag to compare

@biznuts biznuts released this 19 Jun 14:56
0ecc5b1

Fixes a ReDoS surface in the documented body-hash one-liner (/never-stale:setup and /never-stale:status). The old /\s+$/ (plus /^\n+|\n+$/g) backtracks catastrophically on a long run that never reaches the anchor — a single ~200 KB line of spaces measured ~21.6 s. Both one-liners now use the same linear char-scan normalization the gate and the hash-mode preview already use.

  • Hash-compatible: linear and regex forms produce byte-identical hashes for all realistic bodies (verified against the dogfood fence hash) → no recorded hash changes, no migration.
  • A guard test runs the real shipped one-liner and asserts it stays linear and never reintroduces /\s+$/.
  • Roadmap honesty: version / declared modes documented as deferred and not planned (reserved no-ops).

This line also adopts hash-mode dogfooding in the repo (pairs the gate to its design doc). No plugin behaviour change vs 0.10.0 when syncPairs is unconfigured.

Full notes: CHANGELOG.md.

v0.10.0 — content-based drift detection (hash mode)

Choose a tag to compare

@biznuts biznuts released this 19 Jun 14:56
1e6bf5e

Phase 2's first increment: mode: "hash". A syncPairs entry can now do a true content comparison instead of the mtime heuristic.

  • The snapshot embeds a synced-to marker naming the source content it last reconciled to:
    <!-- never-stale:synced-to 8c2b42f56e6fd699 -->
  • On compact, the gate hashes the source's normalized content (CRLF, trailing whitespace, and leading/trailing blank lines normalized away → cosmetic churn is not drift) and flags a mismatch, reporting the new hash so you can paste it back.
  • A snapshot with no marker, or a source missing / larger than the 512 KB cap, is treated as unknown (never drift) and surfaced in /never-stale:status.

The synced-to marker is matched with a static, gate-owned pattern — no user-supplied regex, no ReDoS surface — and the source read is size-capped and happens only on the low-frequency compact path. /never-stale:status previews hash-mode pairs. With no syncPairs configured, the reminder stays byte-identical.

Full notes: CHANGELOG.md.

v0.9.0 — optional drift detection (syncPairs, mtime mode)

Choose a tag to compare

@biznuts biznuts released this 19 Jun 14:56
75ebc02

Phase 1 of drift detection. A marker may pair a source doc (a ledger/changelog that grows) with a snapshot doc meant to stay reconciled to it, via syncPairs. When the snapshot falls behind, the gate turns silent drift into a signal:

  • on compact — appends an advisory drift note to the reminder;
  • on edit — if you edited a configured source, the nudge retargets to "update the paired snapshot."

Ships mode: "mtime" only — a pure fs.stat comparison: no file read, no regex → no ReDoS surface, no per-edit I/O. hash / declared / version reserved.

A dedicated safety suite validates each pair's shape, rejects unsafe paths (absolute / drive-qualified / UNC / parent-escaping), and statically flags ReDoS-prone regexes. With no syncPairs configured, the reminder is byte-identical to 0.8.0.

Full notes: CHANGELOG.md.

v0.8.0 — update sweep + language codes

Choose a tag to compare

@biznuts biznuts released this 03 Jun 10:33

Tools for keeping opted-in projects tidy after a plugin upgrade — without ever changing the gate's behavior.

New: /never-stale:update

After you upgrade the plugin, projects you opted in earlier keep markers (and CLAUDE.md fences) stamped with the version that wrote them. The gate ignores that stamp, so the drift is purely cosmetic — but /never-stale:update sweeps your projects and reconciles the bookkeeping in one pass:

  • bumps each marker's recorded version to the installed version,
  • normalizes the recorded languages to canonical display strings and adds language codes,
  • refreshes the CLAUDE.md fence v= tag (body and hash= untouched, so the "edited since scaffold" signal is preserved),
  • migrates away legacy v0.5.0 residue.

It never re-asks your languages and never changes what the gate fires. Shows a per-project plan and confirms; --dry-run previews. Pass a parent path to sweep many repos at once, e.g. /never-stale:update ~/projects.

Canonical language codes

The marker gains optional spokenCode / writtenCode (en, zh-Hant, zh-HK, zh-Hans; omitted for an "Other" language) alongside the human display strings — so a recorded language stays comparable even when the display wording drifts (e.g. "Traditional Chinese (HK)" vs "Traditional Chinese (Hong Kong)"). setup writes them; update backfills older markers.

Version-drift surfacing

/never-stale:status and /never-stale:list now compare each marker's recorded version against the installed plugin version and flag a stale marker — making clear it's cosmetic and pointing at /never-stale:update.

Migration

None required. The gate reads only enabled + the per-event flags and ignores the version and language strings, so the marker's extra fields are inert and existing setups keep working. marker.schema.json gains the two optional code properties; markers without them stay valid.

Update: /plugin marketplace update biznuts then /plugin install never-stale@biznuts (restart or /reload-plugins), then optionally /never-stale:update to reconcile your projects.

Full changelog: https://github.com/biznuts/never-stale/blob/main/CHANGELOG.md

v0.7.0 — verb subcommands

Choose a tag to compare

@biznuts biznuts released this 03 Jun 09:43

never-stale is now driven by verb subcommands instead of one flag-laden command. Plugin commands are namespaced, so the old /never-stale:never-stale invocation and the --off/--list/--dry-run flags read as plain actions now.

New commands

Command What it does
/never-stale:setup Opt this project in (scaffold CLAUDE.md + write the marker). --dry-run previews.
/never-stale:off · /never-stale:on Pause · resume — flip the marker's enabled, keeping the marker, languages, and CLAUDE.md block.
/never-stale:status Read-only health check: what governs this project and whether the gate would fire.
/never-stale:list List every opted-in / legacy project on disk.
/never-stale:remove Full teardown — delete the marker and strip the CLAUDE.md block. --dry-run previews.

Highlights

  • Reversible pause/resume. /never-stale:off sets "enabled": false (gate goes silent) while keeping everything; /never-stale:on flips it back. On a committed team marker, off offers a local "enabled": false override so a single checkout can pause without touching the repo.
  • New /never-stale:status — governing marker (team vs local), enabled state, recorded languages, CLAUDE.md fence integrity (intact / edited / missing / malformed), and a one-line fires-vs-silent verdict.
  • The bare /never-stale command is now a deprecated help screen that lists the verbs and maps the old flags to them.

Migration

None required. The gate is untouched ("enabled": false was already honored) and the marker / CLAUDE.md formats are unchanged, so existing setups keep working. Old flags map to: --off/never-stale:remove, --list/never-stale:list, --dry-run/never-stale:setup --dry-run.

Update: /plugin marketplace update biznuts then /plugin install never-stale@biznuts (restart or /reload-plugins).

Full changelog: https://github.com/biznuts/never-stale/blob/main/CHANGELOG.md

v0.6.0

Choose a tag to compare

@biznuts biznuts released this 02 Jun 23:35

Changed

  • Hooks now ship inside the plugin (hooks/hooks.json +
    hooks/never-stale-gate.js, resolved via ${CLAUDE_PLUGIN_ROOT}) and register
    machine-wide. /never-stale no longer writes any hook or script into your project —
    only a marker and a CLAUDE.md block.
  • CLAUDE.md rules are now wrapped in <!-- never-stale:begin … --> /
    <!-- never-stale:end --> sentinels, so teardown is reliable even after you edit
    the text inside.

Added

  • Per-project opt-in marker.claude/never-stale.json (committed/team) or
    .claude/never-stale.local.json (gitignored/local, which overrides a committed
    marker and can veto it with "enabled": false").
  • Git-bounded upward walk: the gate resolves the governing project by walking up
    from the launch directory to the nearest ancestor marker, bounded by the git repo
    root — so launching from a subdirectory still works and a marker outside the repo
    never governs it.
  • /never-stale --list to enumerate every opted-in (and legacy) project on disk.
  • Automatic migration of 0.5.0 installs when you re-run /never-stale.
  • NEVER_STALE_DEBUG=1 diagnostic log in the OS temp directory.

Removed

  • The project-local never-stale-reminder.js script and the per-project
    settings.json hooks (replaced by the plugin-owned, marker-gated hook). Existing
    0.5.0 projects keep working until migrated.

v0.5.0

Choose a tag to compare

@biznuts biznuts released this 02 Jun 23:34

Added

  • /never-stale --off — per-project teardown that is symmetric to setup: it shows
    a plan, confirms, and removes only never-stale's own artifacts (hook script, its
    settings.json hooks, and CLAUDE.md sections still matching the template).
    --off --dry-run previews the removal only. Foreign hooks, user-edited sections,
    and settings.local.json are never touched.

v0.4.2

Choose a tag to compare

@biznuts biznuts released this 02 Jun 23:34

Changed

  • Renamed the marketplace to biznuts, so the install id is
    never-stale@biznuts (instead of the confusing never-stale@never-stale).

v0.4.1

Choose a tag to compare

@biznuts biznuts released this 02 Jun 23:34

Added

  • README "Updating" section and a note on how to check the installed version.