kodelyth-ecc 2.24.11 → 2.25.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
@@ -2,6 +2,56 @@
2
2
 
3
3
  All notable changes to Kodelyth ECC are documented here.
4
4
 
5
+ ## v2.25.0 — Foundations: honest numbers, a memory that learns, MCP beyond Claude (November 2026)
6
+
7
+ The first cut of the v3.0 "Foundations" track. Almost no new surface area — this
8
+ release makes existing systems tell the truth and wires up machinery that was
9
+ built but never connected. Every change ships with tests (48 test files, all
10
+ passing).
11
+
12
+ **Truth-in-measurement.** Three claims did not match the code, and an enterprise
13
+ audit would catch all three:
14
+
15
+ - The Terse runtime ledger claimed "every terse-active turn is recorded" — but
16
+ nothing wrote it, and its per-turn savings were modeled from a fixed multiplier,
17
+ not measured. Rows are now flagged `estimated: true`, the docs say plainly the
18
+ figure is an estimate (the Terse *file* compressor stays a real measurement),
19
+ and the unshipped statusline badge claim is gone.
20
+ - The RTK "5,107,394 tokens / 64.1% — all real" headline was hand-authored copy,
21
+ not a computed number. It is now labelled an example snapshot that points at
22
+ `rtk gain` for your own live figures. RTK's `gain` measurement was, and stays,
23
+ real.
24
+ - The codebase-graph binary's `--version` is unreliable — builds report an old
25
+ number while exposing much newer tools. ECC no longer trusts it: it detects
26
+ what the binary can actually do and flags the inconsistency instead of showing
27
+ a number as fact.
28
+
29
+ **The memory that learns, now actually learns.** The structured instinct engine
30
+ recorded every correction with confidence, decay, and outcome scoring — and
31
+ nothing ever read it, so none of it influenced a session. Session start now
32
+ injects the high-confidence, non-stale instincts for the current project. The
33
+ dormant loop is closed.
34
+
35
+ **RTK won't wire the wrong binary.** `reachingforthejack/rtk` (Rust Type Kit)
36
+ ships a binary also called `rtk`; ECC used to treat any `rtk` on PATH as Rust
37
+ Token Killer and would patch hooks into the wrong tool. It now verifies the
38
+ signature (`rtk gain`) before wiring or measuring anything.
39
+
40
+ **MCP — and therefore memory — reaches beyond Claude Code.** ECC auto-registered
41
+ its MCP server for Claude only, so the whole tool surface, memory included, was
42
+ effectively Claude-exclusive. It now also writes the MCP config for Cursor,
43
+ Windsurf, and Gemini CLI (the IDEs that use the same JSON config shape), reusing
44
+ the hardened merge that never clobbers your other servers. Codex (TOML) and
45
+ Cline/Roo (editor storage) are the next step.
46
+
47
+ **Fixes found along the way.** `statusAll()` in the MCP registrar called a
48
+ function that no longer existed (`readJson`), so `kodelythecc doctor` and the
49
+ `mcp` status command both threw "readJson is not defined". Fixed.
50
+
51
+ **Routing.** The intent router learned the newer codebase-graph capabilities:
52
+ smart test selection (`detect_changes scope:tests`), hot-path/complexity queries,
53
+ code-to-docs links, and ADRs.
54
+
5
55
  ## v2.24.11 — every docs page gets its own diagram (October 2026)
6
56
 
7
57
  Documentation and site only. No code change.
package/README.md CHANGED
@@ -524,13 +524,16 @@ kodelythecc rtk gain --all # raw rtk savings output
524
524
  kodelythecc rtk --help # focused help
525
525
  ```
526
526
 
527
- ### Live proof (from this repo maintainer's Mac)
527
+ ### Example snapshot (one maintainer's Mac, one point in time)
528
+
529
+ Not a guaranteed figure — RTK measures savings per command, and yours depend on
530
+ which commands you run. Run `rtk gain` to see your own live numbers.
528
531
 
529
532
  ```
530
533
  total_commands: 1,285
531
534
  total_input: 7,964,612 (raw)
532
535
  total_output: 2,858,501 (after RTK filter)
533
- total_saved: 5,107,394 ← 64.1% average reduction
536
+ total_saved: 5,107,394 ← 64.1% on this sample
534
537
  ```
535
538
 
536
539
  ---
@@ -1883,7 +1883,24 @@ function runPostInstall(status, args) {
1883
1883
  const marker = r.action === 'added' ? '✓ added' : r.action === 'updated' ? '✓ updated' : r.action === 'unchanged' ? '· unchanged' : `· ${r.action}`;
1884
1884
  process.stdout.write(` ${marker}: ${r.file}\n`);
1885
1885
  }
1886
- process.stdout.write(` · Restart Claude Code / Claude Desktop to pick up the server\n`);
1886
+
1887
+ // Also wire ECC's MCP server into the specific non-Claude IDE this install
1888
+ // targeted, when that IDE uses a JSON MCP config we can safely merge
1889
+ // (Cursor, Windsurf, Gemini CLI). This is what extends memory and the whole
1890
+ // MCP tool surface beyond Claude Code — previously they were Claude-only.
1891
+ const mcpTargetIdx = args.indexOf('--target');
1892
+ const mcpRawTarget = mcpTargetIdx >= 0 && args[mcpTargetIdx + 1] ? args[mcpTargetIdx + 1] : 'claude-code';
1893
+ if (!/^claude/.test(mcpRawTarget)) {
1894
+ for (const r of reg.registerForTarget(mcpRawTarget, { cwd: process.cwd(), log: () => {} })) {
1895
+ if (r.action === 'added' || r.action === 'updated') {
1896
+ process.stdout.write(` ✓ ${r.action}: ${r.file}\n`);
1897
+ } else if (r.action === 'error') {
1898
+ process.stdout.write(` · skipped ${r.file}: ${r.error}\n`);
1899
+ }
1900
+ // 'unsupported' / 'unchanged' stay quiet — not every target has a JSON MCP config
1901
+ }
1902
+ }
1903
+ process.stdout.write(` · Restart your AI tool to pick up the server\n`);
1887
1904
  } catch (e) {
1888
1905
  process.stderr.write(`[mcp] self-register skipped: ${e.message}\n`);
1889
1906
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodelyth-ecc",
3
- "version": "2.24.11",
3
+ "version": "2.25.0",
4
4
  "description": "Production-grade AI coding toolkit — 70 agents (incl. devil-mode adversarial crew), 196 skills, 103 commands, parallel multi-agent commands, semantic intent routing, self-learning memory, and a built-in MCP server (16 tools / 6 prompts / 377 resources) that bridges to Claude Desktop, LangGraph, AutoGen, CrewAI, and OpenAI Agents SDK. Works on macOS, Linux and Windows with Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, RooCode, Aider, Kimi, and Gemini CLI.",
5
5
  "author": "Kodelyth <github.com/sifxprime>",
6
6
  "license": "MIT",
@@ -60,6 +60,10 @@ Beyond the 10 priority tiers below, watch for these newer signals — they map t
60
60
  | "talk shorter" / "be brief" / "less verbose" / "caveman mode" | `/terse` (skill activation) |
61
61
  | "who calls X" / "where is X used" / "trace the call chain" / "impact analysis" | `codebase-memory-mcp` via `search_graph` / `trace_path` |
62
62
  | "index this project" / "build the graph" / "scan the codebase" | `codebase-memory-mcp` via `index_repository` |
63
+ | "which tests does this change affect" / "what tests should I run" / "smart test selection" | `codebase-memory-mcp` via `detect_changes` with `scope: "tests"` (Smart CI) |
64
+ | "find the bottleneck" / "hottest paths" / "most complex functions" / "refactor candidates" | `codebase-memory-mcp` via `query_graph` (complexity / hot-path metrics) |
65
+ | "what docs explain X" / "where is X documented" / "link code to docs" | `codebase-memory-mcp` via `trace_path` (inbound `MENTIONS` edges) |
66
+ | "record this decision" / "write an ADR" / "architecture decision record" | `codebase-memory-mcp` via `manage_adr` |
63
67
  | "remove ECC" / "uninstall kodelythecc" / "clean this up" | Suggest `kodelythecc uninstall --dry-run` |
64
68
  | "how much did I save" / "show my token savings" / "RTK stats" | `kodelythecc dashboard` OR `kodelythecc rtk gain` |
65
69
  | "install for another IDE" / "add to Cursor / Windsurf / Antigravity" | `kodelythecc --target <ide>` |
@@ -22,6 +22,52 @@ function getVersion() {
22
22
  catch { return null; }
23
23
  }
24
24
 
25
+ // The binary's `--version` string has proven unreliable: builds have reported an
26
+ // old number (e.g. 0.8.1) while exposing much newer tools and the `--ui` (CBM
27
+ // Lens) flag. So detect what the binary can actually DO from its own help text
28
+ // rather than trusting the version. Pure parser (unit-tested) + a thin probe.
29
+ const KNOWN_CAPABILITIES = {
30
+ lens: /--ui\b/, // CBM Lens UI (v0.12+)
31
+ traces: /\bingest_traces\b/, // runtime-trace ingestion
32
+ cypher: /\bquery_graph\b/, // Cypher + complexity/hot-path metrics
33
+ smart_ci: /\bdetect_changes\b/, // Smart CI test selection
34
+ adr: /\bmanage_adr\b/, // ADR nodes
35
+ cross_repo: /cross[-_]repo/, // cross-repo intelligence
36
+ };
37
+
38
+ function parseCapabilities(helpText) {
39
+ const h = String(helpText || '');
40
+ const caps = {};
41
+ for (const [name, re] of Object.entries(KNOWN_CAPABILITIES)) caps[name] = re.test(h);
42
+ return caps;
43
+ }
44
+
45
+ // When the binary exposes clearly-newer capabilities but reports an old version,
46
+ // the version string is not trustworthy — say so rather than displaying a number
47
+ // ECC would otherwise treat as fact. Returns a note string, or null.
48
+ function versionNote(reportedVersion, caps) {
49
+ const hasNew = !!(caps && (caps.lens || caps.traces || caps.cross_repo));
50
+ const looksOld = !!(reportedVersion && /\b0\.[0-8]\./.test(reportedVersion));
51
+ if (hasNew && looksOld) {
52
+ return `binary reports "${reportedVersion}" but exposes newer capabilities — the version string is unreliable; capability detection is authoritative. Run \`kodelythecc codebase install\` to refresh.`;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ // Probe the binary's help (different builds print tools under `--help` or
58
+ // `cli --help`) and parse once. Returns the detected capability map, or all-false
59
+ // if the binary is absent.
60
+ function capabilities() {
61
+ if (!isInstalled()) return parseCapabilities('');
62
+ let help = '';
63
+ for (const argv of [['--help'], ['cli', '--help']]) {
64
+ try {
65
+ help += '\n' + execFileSync(BIN, argv, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
66
+ } catch { /* try the next help surface */ }
67
+ }
68
+ return parseCapabilities(help);
69
+ }
70
+
25
71
  function install({ log = () => {} } = {}) {
26
72
  if (isInstalled()) return { installed: false, skipped: true, reason: 'already installed', version: getVersion() };
27
73
  if (os.platform() === 'win32') {
@@ -95,7 +141,15 @@ function status() {
95
141
  const home = os.homedir();
96
142
  const c = path.join(home, '.cache', 'codebase-memory-mcp');
97
143
  if (fs.existsSync(c)) cacheDir = c;
98
- return { installed: true, version, indexed_projects, cache_dir: cacheDir };
144
+ const caps = capabilities();
145
+ return {
146
+ installed: true,
147
+ version,
148
+ version_note: versionNote(version, caps),
149
+ capabilities: caps,
150
+ indexed_projects,
151
+ cache_dir: cacheDir,
152
+ };
99
153
  }
100
154
 
101
155
  // ── Structural query proxy (thin wrapper over `codebase-memory-mcp cli`) ────
@@ -140,4 +194,7 @@ function dashboardSnapshot() {
140
194
  return out;
141
195
  }
142
196
 
143
- module.exports = { isInstalled, getVersion, install, autoConfigureAgents, status, query, dashboardSnapshot };
197
+ module.exports = {
198
+ isInstalled, getVersion, install, autoConfigureAgents, status, query, dashboardSnapshot,
199
+ capabilities, parseCapabilities, versionNote,
200
+ };
@@ -22,6 +22,7 @@ const {
22
22
  log
23
23
  } = require('../lib/utils');
24
24
  const { resolveProjectContext, writeSessionLease, resolveSessionId } = require('../lib/observer-sessions');
25
+ const { summarizeStructuredInstincts } = require('../memory/instinct-inject');
25
26
  const { getPackageManager, getSelectionPrompt } = require('../lib/package-manager');
26
27
  const { listAliases } = require('../lib/session-aliases');
27
28
  const { detectProjectType } = require('../lib/project-detect');
@@ -380,6 +381,17 @@ async function main() {
380
381
  additionalContextParts.push(instinctSummary);
381
382
  }
382
383
 
384
+ // Also inject from the structured instinct store (scripts/memory/instincts.js).
385
+ // It was written on every correction but never read — this activates the
386
+ // dormant self-learning loop. Fully fail-safe: never breaks session start.
387
+ const structuredInstinctSummary = summarizeStructuredInstincts(
388
+ (observerContext && observerContext.projectRoot) || process.cwd(),
389
+ { log }
390
+ );
391
+ if (structuredInstinctSummary) {
392
+ additionalContextParts.push(structuredInstinctSummary);
393
+ }
394
+
383
395
  // Check for recent session files (last 7 days)
384
396
  const recentSessions = dedupeRecentSessions(sessionSearchDirs);
385
397
 
@@ -25,6 +25,67 @@ function claudeDesktopConfigPath() {
25
25
  return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
26
26
  }
27
27
 
28
+ // MCP config paths for the non-Claude IDEs ECC can auto-wire. Every target below
29
+ // uses the SAME `{ mcpServers: { <name>: { command, args, env } } }` shape as
30
+ // Claude Code, so the hardened readConfig + registerInFile merge works for all of
31
+ // them unchanged — only the file location differs. That is why memory and the
32
+ // whole MCP tool surface, previously Claude-only, reach these IDEs out of the
33
+ // box once this runs.
34
+ //
35
+ // Deliberately excluded (different format or fragile location — handled manually
36
+ // for now, tracked as a follow-up):
37
+ // - Codex CLI → ~/.codex/config.toml (TOML, not JSON)
38
+ // - Cline / Roo → VS Code globalStorage (editor-version-specific path)
39
+ function mcpPathsForTarget(target, cwd = process.cwd()) {
40
+ const home = os.homedir();
41
+ switch (target) {
42
+ case 'claude-code':
43
+ case 'claude-home':
44
+ return [claudeCodeConfigPath()];
45
+ case 'cursor':
46
+ case 'cursor-project':
47
+ return [path.join(cwd, '.cursor', 'mcp.json')];
48
+ case 'windsurf':
49
+ case 'windsurf-home':
50
+ case 'windsurf-project':
51
+ return [path.join(home, '.codeium', 'windsurf', 'mcp_config.json')];
52
+ case 'gemini-cli':
53
+ case 'gemini-home':
54
+ case 'gemini-project':
55
+ return [path.join(home, '.gemini', 'settings.json')];
56
+ default:
57
+ return [];
58
+ }
59
+ }
60
+
61
+ // Returns the list of targets this module can auto-wire (for help text / docs).
62
+ function supportedMcpTargets() {
63
+ return ['claude-code', 'cursor', 'windsurf', 'gemini-cli'];
64
+ }
65
+
66
+ // Register ECC's MCP server for one install target. Reuses the same merge that
67
+ // protects ~/.claude.json: absent files are created, present files are merged
68
+ // without clobbering other servers, and an unparseable file is refused, never
69
+ // overwritten. Never throws — a bad config for one IDE must not fail an install.
70
+ function registerForTarget(target, { cwd = process.cwd(), log = () => {}, dryRun = false } = {}) {
71
+ const paths = mcpPathsForTarget(target, cwd);
72
+ if (paths.length === 0) {
73
+ return [{ target, action: 'unsupported', reason: `no known JSON MCP config path for "${target}"` }];
74
+ }
75
+ const results = [];
76
+ for (const p of paths) {
77
+ try {
78
+ const r = registerInFile(p, { dryRun });
79
+ log(` ${r.action}: ${r.file}`);
80
+ results.push({ target, ...r });
81
+ } catch (e) {
82
+ log(` skipped ${p}: ${e.message}`);
83
+ results.push({ target, file: p, action: 'error', error: e.message });
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+
28
89
  // Reading this file has THREE outcomes, and collapsing them is destructive.
29
90
  //
30
91
  // readJson used to return null for every failure, and the caller did
@@ -145,9 +206,19 @@ function unregisterAll({ log = () => {} } = {}) {
145
206
  function statusAll() {
146
207
  const rows = [];
147
208
  for (const p of [claudeCodeConfigPath(), claudeDesktopConfigPath()]) {
148
- const cfg = readJson(p);
149
- const present = !!(cfg && cfg.mcpServers && cfg.mcpServers[SERVER_NAME]);
150
- rows.push({ file: p, present, exists: !!cfg });
209
+ // Was `readJson(p)` — a function that no longer exists after the
210
+ // absent/present/unreadable refactor, so statusAll() threw
211
+ // "readJson is not defined" and took `kodelythecc doctor` and the `mcp`
212
+ // status command down with it. Use the real reader and its tri-state result.
213
+ const cfg = readConfig(p);
214
+ const data = (cfg && cfg.data) || {};
215
+ const present = !!(data.mcpServers && data.mcpServers[SERVER_NAME]);
216
+ rows.push({
217
+ file: p,
218
+ present,
219
+ exists: cfg.state !== 'absent',
220
+ readable: cfg.state !== 'unreadable',
221
+ });
151
222
  }
152
223
  return rows;
153
224
  }
@@ -157,6 +228,9 @@ module.exports = {
157
228
  serverEntry,
158
229
  claudeCodeConfigPath,
159
230
  claudeDesktopConfigPath,
231
+ mcpPathsForTarget,
232
+ supportedMcpTargets,
233
+ registerForTarget,
160
234
  registerAll,
161
235
  unregisterAll,
162
236
  statusAll,
@@ -0,0 +1,67 @@
1
+ // scripts/memory/instinct-inject.js
2
+ //
3
+ // Consume the structured instinct store (scripts/memory/instincts.js) at session
4
+ // start. That store is written by the correction-capture hook, but until now
5
+ // nothing ever READ it — all of its confidence / decay / outcome machinery ran
6
+ // and never influenced a single session. This module closes that loop: it picks
7
+ // the high-confidence, non-stale instincts relevant to the current project and
8
+ // formats them for injection into SessionStart context.
9
+ //
10
+ // Kept separate from session-start.js (which executes main() at import time) so
11
+ // the selection logic can be unit-tested without running the whole hook.
12
+ 'use strict';
13
+
14
+ const DEFAULT_MAX = 6;
15
+ const DEFAULT_THRESHOLD = 0.7;
16
+
17
+ // Pure selection + formatting. Given raw instinct records, keep the ones that
18
+ // are non-stale, confident enough, and in scope for `projectPath`; rank by
19
+ // confidence; cap at `max`; render as injectable markdown. No I/O — this is the
20
+ // unit the tests exercise.
21
+ function formatStructuredInstincts(instincts, { projectPath, max = DEFAULT_MAX, threshold = DEFAULT_THRESHOLD } = {}) {
22
+ const candidates = (Array.isArray(instincts) ? instincts : [])
23
+ .filter(i => i && typeof i.rule === 'string' && i.rule.trim())
24
+ .filter(i => !i.stale && (i.confidence || 0) >= threshold)
25
+ // Global instincts always apply; project-scoped ones only in their project.
26
+ // A record with no project_path is treated as unscoped (applies anywhere).
27
+ .filter(i => i.scope === 'global' || !i.project_path || i.project_path === projectPath);
28
+
29
+ if (candidates.length === 0) return '';
30
+
31
+ const ranked = candidates
32
+ .slice()
33
+ .sort((a, b) => (b.confidence || 0) - (a.confidence || 0))
34
+ .slice(0, max);
35
+
36
+ const lines = ranked.map(i => {
37
+ const scope = i.scope === 'project' ? 'project' : 'global';
38
+ const confidence = `${Math.round((i.confidence || 0) * 100)}%`;
39
+ return `- [${scope} ${confidence}] ${i.rule.trim()}`;
40
+ });
41
+
42
+ return `Learned instincts (from past corrections):\n${lines.join('\n')}`;
43
+ }
44
+
45
+ // Read the store and format. Never throws: the structured store must not be able
46
+ // to break session start, so any failure degrades to injecting nothing.
47
+ function summarizeStructuredInstincts(projectPath, { log = () => {} } = {}) {
48
+ try {
49
+ const store = require('./instincts');
50
+ const summary = formatStructuredInstincts(store.list(), { projectPath });
51
+ if (summary) {
52
+ const count = summary.split('\n').length - 1;
53
+ log(`[SessionStart] Injecting ${count} learned instinct(s) from the structured store`);
54
+ }
55
+ return summary;
56
+ } catch (err) {
57
+ log(`[SessionStart] Structured instinct injection skipped: ${err && err.message}`);
58
+ return '';
59
+ }
60
+ }
61
+
62
+ module.exports = {
63
+ formatStructuredInstincts,
64
+ summarizeStructuredInstincts,
65
+ DEFAULT_MAX,
66
+ DEFAULT_THRESHOLD,
67
+ };
@@ -64,11 +64,51 @@ function detectInstalledTargets() {
64
64
  return targets;
65
65
  }
66
66
 
67
+ // Guard against the `rtk` name collision. reachingforthejack/rtk (Rust Type Kit)
68
+ // also ships a binary called `rtk`; wiring or measuring it as if it were Rust
69
+ // Token Killer would be wrong and silent — ECC would patch hooks and read a
70
+ // `gain` ledger that does not exist. Rust Token Killer is the only `rtk` with a
71
+ // `gain` subcommand (the token-savings ledger ECC reads), so probing it
72
+ // distinguishes the two with no side effects and without depending on exact
73
+ // `--version` text. `rtk gain` is the documented signature command in RTK.md.
74
+ //
75
+ // The decision is a pure function of two probe results so it can be unit-tested
76
+ // without spawning a binary:
77
+ // gainHelpOk — did `rtk gain --help` exit 0?
78
+ // helpText — stdout of `rtk --help` (or '' if it failed)
79
+ function looksLikeRealRtk({ gainHelpOk, helpText } = {}) {
80
+ if (gainHelpOk) return true;
81
+ const h = String(helpText || '');
82
+ // The token killer's help lists the `gain` command and calls itself an
83
+ // LLM-output proxy; Rust Type Kit's help does neither.
84
+ return /\bgain\b/.test(h) && /(LLM|token|proxy)/i.test(h);
85
+ }
86
+
87
+ function isRealRtk() {
88
+ let gainHelpOk = false;
89
+ try {
90
+ execFileSync('rtk', ['gain', '--help'], { stdio: 'ignore' });
91
+ gainHelpOk = true;
92
+ } catch { /* `gain` subcommand absent, or `rtk` not runnable */ }
93
+
94
+ let helpText = '';
95
+ if (!gainHelpOk) {
96
+ try {
97
+ helpText = execFileSync('rtk', ['--help'], {
98
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
99
+ });
100
+ } catch { /* no help available */ }
101
+ }
102
+ return looksLikeRealRtk({ gainHelpOk, helpText });
103
+ }
104
+
67
105
  function isInstalled() {
68
106
  try {
69
107
  execFileSync('rtk', ['--version'], { stdio: 'ignore' });
70
- return true;
71
108
  } catch { return false; }
109
+ // A binary named `rtk` exists — confirm it is Rust Token Killer, not the Rust
110
+ // Type Kit collision, before any caller wires or measures it.
111
+ return isRealRtk();
72
112
  }
73
113
 
74
114
  function getVersion() {
@@ -214,6 +254,8 @@ module.exports = {
214
254
  normalizeTarget,
215
255
  detectInstalledTargets,
216
256
  isInstalled,
257
+ isRealRtk,
258
+ looksLikeRealRtk,
217
259
  getVersion,
218
260
  install,
219
261
  enableFor,
@@ -1,9 +1,16 @@
1
1
  // scripts/terse/ledger.js
2
- // Output-token savings ledger. Stores one JSONL row per terse-active turn.
3
- // Zero deps. Reads only. Writes append-only.
2
+ // Output-token savings ledger. Stores one JSONL row per recorded terse turn.
3
+ // Zero deps. Writes append-only.
4
+ //
5
+ // IMPORTANT — these rows are ESTIMATES, not measured savings. There is no
6
+ // non-terse copy of the same reply to diff against, so `saved` is modeled:
7
+ // actual output tokens times a per-level baseline multiplier (RAW_MULT below).
8
+ // Every figure here is an estimate and each row is flagged `estimated: true`.
9
+ // (The Terse *file* compressor in compress.js, by contrast, reports real,
10
+ // measured before/after bytes.)
4
11
  //
5
12
  // Row shape:
6
- // { ts, level, rawEstimate, actual, saved, projectHash?, source }
13
+ // { ts, level, rawEstimate, actual, saved, estimated, baseline, projectHash?, source }
7
14
  //
8
15
  // Path: ~/.kodelythecc/terse/ledger.jsonl (overridable via KODELYTH_TERSE_DIR)
9
16
 
@@ -31,8 +38,10 @@ function estimateTokens(text) {
31
38
  return Math.round(Buffer.byteLength(text, 'utf8') / 4);
32
39
  }
33
40
 
34
- // Baseline output multiplier per level.
35
- // Empirically: full ≈ 0.5x normal, ultra ≈ 0.35x, lite ≈ 0.75x.
41
+ // Per-level baseline multiplier used to MODEL what the same reply would have
42
+ // cost without terse (full ≈ 2x the terse size, ultra ≈ 2.85x, lite ≈ 1.33x).
43
+ // These are assumptions, not measurements — every `saved` figure derived from
44
+ // them is an estimate, which is why each row is flagged `estimated: true`.
36
45
  const RAW_MULT = { lite: 1.33, full: 2.0, ultra: 2.85, off: 1.0 };
37
46
 
38
47
  function appendTurn({ actualText, level = 'full', source = 'unknown', projectHash = null }) {
@@ -46,6 +55,8 @@ function appendTurn({ actualText, level = 'full', source = 'unknown', projectHas
46
55
  rawEstimate,
47
56
  actual,
48
57
  saved,
58
+ estimated: true, // modeled from RAW_MULT, not a measured A/B
59
+ baseline: RAW_MULT[level] || 1,
49
60
  source,
50
61
  ...(projectHash ? { projectHash } : {}),
51
62
  };
@@ -83,7 +83,7 @@ Example — ~70% shorter:
83
83
 
84
84
  - Slash command: `/terse [lite|full|ultra|off]` — sticks for the session
85
85
  - CLI: `kodelyth-ecc terse enable [--target claude-code|--all]` — installs the skill + command into your AI tool
86
- - Statusline (Claude Code): shows `[TERSE ⚡ 12.4k]` — lifetime output tokens saved
86
+ - Dashboard (Claude Code): the **Token Savings** tab shows lifetime *estimated* output savings from the terse ledger — modeled, not measured (see the ledger note in `docs/terse-mode.md`)
87
87
 
88
88
  ## Honest numbers
89
89