Skip to main content
Kodelyth ECC
Changelog

Release history

Live-verified releases pulled from the kodelyth-ecc repo. Last 10 highlighted below, full history further down.

Latest 10 releases

v2.24.3 · remove decorative emoji from docs and catalog

October 2026

Documentation only. No code, no behaviour change.

rules/common/coding-style.md bans decorative emoji and names the permitted status indicators: PASS/FAIL or the text characters ✓ (U+2713) and ✗ (U+2717). Three shipped files were not following it.

README comparison table

39 emoji across 11 rows replaced with ✓ / ✗.

This also fixed a polarity bug the emoji were hiding. In every row ❌ meant "lacks this", where lacking it is the bad outcome. In the telemetry row it meant "none", where none is the *good* outcome — so one glyph carried opposite meanings in the same column, and ECC's ❌ sat beside a competitor's ❌ looking like agreement while reading as praise and criticism. A mechanical swap would have preserved that, so the row now states the value instead of a verdict:

| Telemetry | None | Varies | None | Varies |

Agent and command bodies

10 decorative ❌ prefixes removed from bullet lists in agents/chaos-engineer.md and commands/devil-mode.md. Dropped rather than swapped for ✗: both lists sit under headings that already state the negation — "What You DON'T Do" and "When NOT to Use" — so every bullet was marked negative twice.

These two files were also the upstream source of the emoji appearing on the website. ecc-web syncs agent bodies into src/lib/agents.json, so the site inherited them and could not be fixed there; a site-side edit would be overwritten by the next sync.

Deliberately left alone

rules/common/coding-style.md still contains ✅ 🔍 🎯 🚀 ❌. Every one sits in the Wrong column of the table teaching which emoji not to use. They are quoted specimens, not decoration — stripping them would empty the column and leave the rule unable to demonstrate its own point. Future emoji sweeps should skip that file.

The ← ↑ ↓ ⚙ in README.md are also kept. They sit inside terminal-output code blocks and reproduce what the CLI actually prints — the gear comes from scripts/cli/menu.js:243. Changing them would make the README stop matching the tool.

Catalog parsing verified unchanged: 70 agents, 103 commands, frontmatter intact. 665 tests across 43 files.

v2.24.2 · adversarial pass: path traversal in get_rule, unrecallable memories

October 2026

Security fix. Three defects found by attacking the memory store, MCP server and dashboard rather than reading them.

get_rule returned any markdown file on the machine

loadRule was the only catalog loader that built a filesystem path out of its argument. Every other loader — agents, skills, commands, bundles — enumerates a directory and matches by name, so a caller can only ever select something already listed. This one did path.join(PATHS.rules, name + '.md'), so:

get_rule("../../README") -> 51,297 bytes of ROOT/README.md get_rule("testing/../../../README") -> same, via nested traversal

The .md suffix was the only thing bounding the reach, which left every readable markdown file on the machine in scope.

This matters because the caller is not always the user. An MCP tool argument can originate in text the agent merely read — a dependency README, an issue comment, a fetched page — which made this an arbitrary-markdown-read primitive reachable by prompt injection. That is exactly the class ECC's own prompt-injection-hunter agent exists to catch.

Fixed with two guards that fail differently: a name pattern that rejects anything that is a path rather than a name before touching disk, and the existing resolveContained() from scripts/lib/safe-fs.js, which proves the target sits under rules/common once symlinks resolve. The pattern alone is not enough — escape-probe is a valid rule name, and a symlink under that name reads 51KB.

loadRule had no test coverage at all, which is why this survived. It now has four tests, including the symlink case.

One memory was mathematically unrecallable

search() defaulted to an absolute minScore of 0.5 against unnormalised BM25 scores. BM25 IDF is log(1 + (N - df + 0.5) / (df + 0.5)), so a term appearing in every memory drives IDF toward zero and can never clear a fixed floor at any corpus size. The floor is now relative to the top hit, so it scales with the corpus instead of fighting it. An explicitly passed minScore is still honoured exactly. recall_memory passes none, so it was using the broken default.

Malformed request URIs returned 500 and wrote to stderr

decodeURIComponent throws on /%, /%zz or a truncated /%e0%a4. The dashboard did not guard it, so a malformed *client* URI became a 500 "internal server error" plus one stderr line per request — meaning any local process could spam the terminal the dashboard runs in. A path that cannot be decoded names no file, so it is now a 404 like any other miss.

What resisted

Worth recording, since the absence of findings was tested rather than assumed. Dashboard traversal (plain, encoded, double-encoded, backslash, absolute, null-byte) all returned 404. Host validation rejects absent, empty and foreign Host headers. Non-GET is 405. Stored XSS was tested in a real browser: a memory captured through the real store with breakout payloads in problem, approach, tags, language and project rendered as visible text, setting no global, creating no element and no attribute, with the breakout still inside its

.

665 tests across 43 files.

v2.24.1 · verify Windows downloads before running them

October 2026

Security fix. The Windows installer added in 2.23.0 downloaded a release zip over HTTPS and extracted it straight into the user's PATH directory **with no integrity check at all**.

This repository ships a supply-chain-auditor agent whose entire job is catching "downloads and executes a binary without verifying it". The code doing it was ours. Both upstream projects publish a checksums.txt in the same release, in standard sha256sum format, and nothing was reading it.

What it does now

Fetches checksums.txt from the release, finds the line for the asset it just downloaded, hashes the file with Node's crypto, and compares.

  • Mismatch is fatal. It refuses to install and reports both hashes.
  • Missing or unlisted warns and proceeds. The fetch is HTTPS from
github.com so transport tampering is already hard, and a release that stops publishing checksums should not make ECC look broken. The case this control exists for is a mismatch, and that stays fatal.

Hashing uses Node's crypto rather than shelling out, because certutil, sha256sum and shasum differ in availability and output format across the machines this has to run on.

Proven, not asserted

Checked against the real artifact before a single test was written: the genuine RTK Windows asset hashes to its published sha256, and flipping one byte produces a mismatch.

Nine tests cover a matching checksum, a single changed byte, an unlisted asset, a malformed entry, picking the right line out of a full file, uppercase hex, and that a filename merely *ending* with the asset name does not satisfy the lookup (evil-asset.zip must not pass as asset.zip).

The Windows CI job asserts the gate is exercised on every push — a security control nobody runs is decoration. From a real windows-latest runner:

`` [win] checksum verified (sha256 1623e9b45d28…) ``

656 tests across 43 files.

v2.24.0 · the update notice now tells you why

October 2026

The CLI has always polled npm for new versions and cached the answer for 24 hours. That monthly touchpoint was spent on almost nothing: the result surfaced only in the interactive menu, as a version number and an install command.

`` Update to v2.23.2 npm i -g kodelyth-ecc (installs new version) `

A version number is a nag. It states a fact and gives no reason to act on it.

It now links what changed

` Update to v2.24.0 what changed: github.com/sifxprime/kodelyth-ecc/releases/tag/v2.24.0 `

check() returns a releaseNotes URL alongside the version, so every consumer gets it without doing its own string building.

doctor surfaces it too

A second natural touchpoint — doctor is run deliberately, usually when somebody is already paying attention:

` ! version — v2.23.2 installed, v2.24.0 available → npm i -g kodelyth-ecc what changed: .../releases/tag/v2.24.0 `

Cache-only, on purpose

The new cachedCheck() reads the 24-hour cache and never touches the network. doctor is the command people reach for when something is already wrong, so it must not wait on the registry — and an unreachable npm is not a fact about the health of your install. A cold or stale cache yields nothing and the row is simply absent, rather than reported as a warning. A check that fires on "I could not reach the network" teaches people to ignore the output.

run() stays synchronous and takes the result as a parameter, so it remains testable and doctor adds no latency.

Verified across every state: a newer version warns with the link, being current passes, a stale cache is ignored, a corrupt cache leaves doctor` working on its 11 checks, and a cold cache omits the row.

What this does not do

No nagging on every command. Nothing auto-updates. Nothing blocks.

647 tests passing.

v2.23.2 · the README leads with the product, not the badges

October 2026

No change to the toolkit. The README is the npm landing page, and npm only re-renders it on publish, so this exists to make the rewrite actually reach anyone.

What changed

It opened with a logo, a 900px hero and **21 shields — 35 lines before a single word of what this is**. Someone arriving from npm search had to scroll past a wall of badges to learn whether they cared.

The strongest thing in the file was buried too: the routing transcript, where

`` You: "I've been staring at this NullPointerException for two hours, I'm losing my mind."

AI: → Routing to debug-detective `

explains the product faster than any paragraph could, and sat at line 55.

The first screen is now: logo, one-line claim, that transcript, npx kodelyth-ecc, and the platform list. Badges and hero follow below a rule.

Nothing was cut. All 1,100+ lines and every badge are intact — just no longer first.

Corrected while reading

The comparison tables still claimed 194 skills, 97 slash commands and "22+" quality hooks in three places. Actual: 196, 103, 44. Those tables make direct claims against other kits, so wrong numbers there cost more than anywhere else.

Verified: 82 code fences balanced, 19 div`s balanced, zero stale figures left, and a line-by-line diff against the previous file confirming no content lost.

647 tests passing.

v2.23.1 · the shop window said the wrong numbers

October 2026

No change to the toolkit. This exists because npm metadata only refreshes when you publish, and three public-facing surfaces had gone stale.

Measured before changing anything: **9,233 npm downloads in the last 30 days against 11 GitHub stars** — roughly 840:1, where a healthy project runs 50:1 to 200:1. Discovery is not the constraint; thousands install this and nothing converts them.

Corrected

| Surface | Said | Actual | |---|---|---| | package.json description | 194 skills, 97 commands | 196, 103 | | GitHub repo description | 102 commands, 22+ hooks | 103, 44 | | README badges | 373 tests, Skills-194 | 647 tests, 196 |

Wrong numbers in the first thing anyone sees read as abandoned. Both descriptions now also name Windows, which became a real differentiator in 2.23.0 and went unmentioned.

Asked, once

The end of a successful install is the highest-intent moment there is, and an all-green doctor is the other. Both were silent. Both now print a single line.

It is gated hard, because an ask that repeats is an annoyance:

  • shown once per machine, via a marker beside the other ECC state
  • never after a failed install
  • never when doctor reports a warning or a failure
  • wrapped so it can never break an install
Someone wiring up four IDEs is asked once, not four times.

Keywords

Strong on the long tail (devil-mode, bm25, intent-routing), missing the broad terms people actually type. Added claude, agent, agents, ai, subagents, prompt-engineering, ai-assistant, code-review, windows. 41 to 50.

647 tests passing.

v2.23.0 · Windows has no manual step left

October 2026

2.22.0 made the Windows install work but left two components needing a manual download: RTK and the codebase graph. That turned out to be a gap in ECC, not a limitation upstream — both projects publish Windows builds and ECC simply never fetched them:

`` rtk-x86_64-pc-windows-msvc.zip codebase-memory-mcp-windows-amd64.zip / -arm64.zip `

Both now install automatically. Windows is at parity with macOS and Linux.

How it works

A shared scripts/lib/win-install.js, used by both. It pulls from GitHub's releases/latest/download/ URL, which always redirects to the newest release — no API call, so no token and no rate limit.

Download and extraction use curl.exe and tar.exe. Both ship with Windows 10 1803 and later, and tar there is bsdtar, which handles zip. That avoids PowerShell quoting entirely. Both are probed before use, so an older machine gets a clear message instead of a spawn error.

Architecture comes from process.arch: the codebase graph has a native arm64 build, while RTK publishes x86_64 only — fine on arm64 Windows, which runs x64 under emulation.

PATH is yours, not ours

Binaries land in %LOCALAPPDATA%\Kodelyth\bin. ECC does not edit your PATH. It prints the one command that does:

`powershell setx PATH "%PATH%;%LOCALAPPDATA%\Kodelyth\bin" `

This mirrors the POSIX path, which reports ~/.local/bin rather than writing to a shell rc. Whether the directory is already persistent is read from HKCU\Environment, not from process.env — the latter would always say yes right after the installer prepends it for its own process, and would wrongly tell you there is nothing left to do.

Verified, not asserted

The Windows CI job now installs for real and asserts both binaries exist and run:

` install dir: C:\Users\runneradmin\AppData\Local\Kodelyth\bin rtk.exe 10,285,056 bytes rtk 0.51.0 codebase-memory-mcp 0.11.0 `

doctor`'s Windows hints changed too. They previously said "no Windows installer yet" and pointed at a release page; the install command is now correct on every platform, and the hint names the PATH step rather than implying the command alone finishes the job.

647 tests passing.

v2.22.0 · Windows actually works now

September 2026

The Windows install was broken in three separate ways. Windows CI had been green throughout, because the test suite only ever exercised library functions — it never ran the installer, which takes a completely different path there (install.ps1 through PowerShell rather than install.sh through bash).

Every finding below was measured on a real windows-latest runner, not inferred from reading the code.

The documented command failed outright

install.sh matches claude-home|claude-code in every case arm, so both spellings work on macOS and Linux. install.ps1 had three separate switch ($Target) blocks that each matched only claude-home, so the other spelling hit the default arm:

`` Unknown target: claude-code Valid targets: claude-home, windsurf-project, ... `

claude-code is what the README, the docs and the website install page all tell people to use. **Every Windows user following the documented instructions got a hard failure.** Aliases are now normalised once at the top of the file, which covers all three switches and keeps the accepted set identical to install.sh rather than merely similar.

None of the post-install wiring ever ran

The Windows branch called process.exit() immediately after install.ps1, while the POSIX branch continued into MCP registration, RTK, Terse mode and the codebase graph. A Windows user got agents, skills and commands on disk and silently none of those, with no message explaining why.

That block is now a shared runPostInstall() called by both branches.

Hooks shipped without their scripts, and were never registered

install.ps1 copied only hooks/hooks.json — the same defect install.sh had before 2.21.0, where Windows was missed. So the manifest landed with none of the scripts it points at, and settings.json was never written at all. Claude Code reads hooks from settings.json, so every hook was inert.

Before and after, measured

` before after --target claude-code hard fail works hook files 1 12 settings.json ABSENT present, 8 events Terse mode absent present MCP registration absent present total files installed — 502 `

Guarded so it cannot regress

A Windows install workflow now runs on every push to main: it packs the package as npm would, installs it globally on windows-latest, runs a real install into a scratch HOME, and asserts on what lands — counts per module, whether settings.json was written and with how many events, whether Terse and MCP registration happened, then runs doctor. Green CI now means the Windows install works, which it never did before.

Also

doctor was telling Windows users to run kodelythecc rtk install and kodelythecc codebase install`. Neither has a Windows installer, so both commands report that they cannot proceed and the user follows a dead end. Both hints now point at the release page on Windows. Remediation that fails on the reader's platform is worse than none.

RTK and the codebase graph remain manual on Windows — they are native binaries without a Windows installer script. The difference is that ECC now says so during install rather than the feature being silently absent.

README gains a Windows section covering what is automatic, what is not, and why no execution-policy change is needed. Two stale claims corrected there: 373 tests became 647, and 22 hooks became 44 hook entries.

647 tests passing.

v2.21.2 · same content, with a GitHub release attached

September 2026

Identical to 2.21.1 in every shipped file. It exists because 2.21.1 could not be given a GitHub release.

What happened: the v2.21.1 tag was created against a commit that predated a fix to publish.yml, and GitHub runs a release workflow from the tagged ref rather than from the default branch — so the release fired the old workflow, which tried to publish a version npm already had and failed. Deleting the release to retag was the wrong move: this repository has immutable releases enabled, so publishing a release permanently reserves its tag name and deleting it does not give the name back. v2.21.1 is now unusable as a tag.

2.21.1 remains a perfectly good version on npm. It simply has no GitHub release, and cannot be given one.

The lesson is recorded in publish.yml itself: tag the commit that carries the workflow you want the release to run.

647 tests passing.

v2.21.1 · the release pipeline itself

September 2026

No change to the toolkit. The shipped package is byte-identical to 2.21.0 — 797 files, zero differences. Every change below is CI and deploy infrastructure, which the package excludes. Upgrading gains you nothing; it is recorded here because the changelog is the project's memory.

Hence a patch, not a minor: nothing was added to what you install.

Fixed — releases were not reaching the website

The workflow that tells the website a release happened had been removed after it was found reporting green while returning HTTP 403. Its last line was:

``bash [ "$STATUS" = "204" ] && echo ok || echo failed `

which exits 0 either way, so a broken dispatch and a working one produced the same green check. It had silently done nothing for an unknown number of releases.

Restored, with every failure path exiting non-zero. A preflight now separates the failure modes, which a bare response cannot: the site repo is private, so GitHub answers 404 rather than 403 to a token that cannot see it, making "wrong repo path", "no repository access", and "access but no write permission" look identical. It also reports the token type and its expiry, warning under 30 days and erroring once lapsed — an expired token is what broke this the first time.

One correction worth recording: the preflight originally passed on permissions.push from the repo API. That field is the permission of the *user* who owns the token, not the permissions granted *to* the token, so a token holding only Metadata: Read reported push=true and was still refused every write. It now logs that as context and never gates on it.

Fixed — the site redeployed every cron tick for identical content

Each content sync rewrites a syncedAt timestamp whether or not anything changed, so git status was never clean, changed was permanently true, and every scheduled run rebuilt and redeployed the server.

Measured over 24 hours: 14 sync commits, 2 with a real change. The other 12 each ran a full install, build, rsync, an rm -rf and mv over the live directories, and a container recreate — to ship byte-identical output. Timestamp-only churn is now discarded rather than committed.

Fixed — the cache purge reported success when it failed

The Cloudflare purge ended in the same shape as the dispatch bug — grep -q ... && echo OK || { echo failed; exit 0; } — always exiting 0 with the failure buried in a plain echo inside a green job. It now parses the response, surfaces Cloudflare's own error codes as annotations, and stays non-fatal on purpose: the site is live at origin and only the edge is stale, so failing the job would misreport a good deploy.

Added — deploys cannot collide

Push, dispatch and cron could all fire at once, and the swap step rm -rfs and mvs live directories on the server. Runs are now queued on one concurrency group rather than cancelled, since cancelling mid-swap is the state being avoided. It caught three real collisions on the day it was added.

Changed — actions on v7

actions/checkout and actions/setup-node moved v4 to v7, clearing GitHub's Node 20 deprecation warning on every run. The majors were checked rather than crossed blind: setup-node v5 auto-enables caching when package.json declares packageManager, which neither repo does. appleboy/ssh-action stays on v1, already current within that major.

Measured — the cron is not a 10-minute backstop

The schedule is configured */10`, but GitHub throttles high-frequency schedules. Across the last 14 scheduled runs the average gap was 228 minutes, the longest 345. Documented in the workflow so nobody plans around ten minutes again.

647 tests passing.