kodelyth-ecc 2.24.6 → 2.24.8

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,148 @@
2
2
 
3
3
  All notable changes to Kodelyth ECC are documented here.
4
4
 
5
+ ## v2.24.8 — adversarial pass: swarm, dashboard, supply-chain (October 2026)
6
+
7
+ Three defects found by attacking the remaining unprobed surfaces rather than
8
+ reading them.
9
+
10
+ ### `baseRef` could reach git's argv as an option
11
+
12
+ Every spawn in the swarm orchestrator uses `spawnSync`'s array form with no shell,
13
+ so shell injection does not work — and that was tested, not assumed: every
14
+ generated `send-keys` string was executed in a real shell with a payload that
15
+ would touch a marker file, across sessionName, worker name, task, repoRoot and
16
+ baseRef. No marker was ever created.
17
+
18
+ Array form does not stop **argument** injection, though. git parses its own argv,
19
+ so in `git worktree add -b <branch> <path> <commit-ish>` a commit-ish beginning
20
+ with `-` is read as an option: `--git-dir=/elsewhere` points git at another
21
+ repository, `--force` changes the command's meaning. `baseRef` was the only value
22
+ reaching argv without being slugified or path-resolved first — sessionName goes
23
+ through `slugify`, branchName carries a literal prefix, worktreePath is absolute —
24
+ and it is user-settable through `--base-ref`. Swarm configs get shared, so this is
25
+ not purely self-inflicted.
26
+
27
+ Rejecting a leading `-` costs nothing: `git check-ref-format` forbids refs that
28
+ start with one, so `HEAD`, `main`, `v1.2.3` and `origin/main` all still work.
29
+
30
+ ### An 800-character excerpt read the entire file
31
+
32
+ The dashboard's `safeReadExcerpt` called `readFileSync` on the whole file and then
33
+ sliced 800 characters off the front, so a fixed-size excerpt cost the full size of
34
+ the file. Measured on a 300MB `task.md`: **196ms and +628MB RSS** for 800
35
+ characters, scaling linearly — roughly twice the file, since the bytes land in a
36
+ Buffer and again as a UTF-16 string.
37
+
38
+ The dashboard is single threaded, so that blocks every other request, and it runs
39
+ three times per worker — task, handoff, status — across every session in
40
+ `.orchestration`. Those files are written by swarm workers, so their size is not
41
+ something the dashboard controls.
42
+
43
+ It now reads a bounded window: stat first, then `max * 4 + 4` bytes. Four because
44
+ UTF-8 encodes a character in at most four, so the window always holds at least
45
+ `max` characters. That detail is load bearing — the truncation decision counts
46
+ CHARACTERS, as it always did, so slicing a byte window would cut a multi-byte
47
+ character in half. 300MB now costs 0ms and +0MB. Output was diffed against the old
48
+ implementation across twelve cases including emoji on the boundary; all identical.
49
+
50
+ ### `verify` passed with a planted file
51
+
52
+ An EXTRA file — present on disk, absent from the manifest — was advisory:
53
+ `ok: true`, exit code 0. Confirmed against the real CLI, where tampering and
54
+ deletion correctly exit 1 but planting `rules/common/zz-injected.md` exits 0.
55
+
56
+ The human output did say `⚠ extra: 1 (advisory)`. Automation did not hear it:
57
+ `kodelyth-ecc verify && deploy` passed with an unknown file in place — and in this
58
+ product an extra `.md` under `rules/`, `agents/` or `commands/` is not inert data.
59
+ Those directories are enumerated and loaded as instructions, so an injected rule
60
+ file becomes an always-on rule in every future session. That is the payload the
61
+ session-bundle traversal delivered in 2.24.5, reached a different way.
62
+
63
+ The default is deliberately unchanged. This is a toolkit people are meant to
64
+ extend, and failing verify on extras would fail it forever for anyone who added
65
+ their own agent — a check that always fails gets ignored. The choice is offered
66
+ instead:
67
+
68
+ ```bash
69
+ kodelyth-ecc verify # extras advisory, exit 0 (unchanged)
70
+ kodelyth-ecc verify --strict # extras fail the verdict, exit 1
71
+ ```
72
+
73
+ The report now carries `strict`, so a `--json` consumer need not infer which mode
74
+ produced the verdict.
75
+
76
+ ### What held up
77
+
78
+ `scripts/evolve` was already hardened, and its comment records the same traversal
79
+ class found in replay along with how it was confirmed. Re-tested anyway: target
80
+ path traversal blocked, legitimate paths written, no prototype pollution. Swarm
81
+ worker names collapse through `slugify` rather than escaping the worktree root,
82
+ and `normalizeSeedPaths` rejects `..`, absolute paths and embedded traversal —
83
+ both now pinned by tests so a later change cannot quietly lose them.
84
+
85
+ **682 tests across 43 files.**
86
+
87
+ ## v2.24.7 — the brand render scripts could not run (October 2026)
88
+
89
+ Developer tooling only. Nothing here ships in the npm package — `brand/` is not in
90
+ the `files` allowlist — but it is what kept the published artwork correct.
91
+
92
+ 2.24.6 fixed brand assets that had drifted to v2.7.0 with counts seven releases
93
+ stale. This fixes the reason they drifted: **both scripts meant to regenerate them
94
+ were broken, and had been for months.**
95
+
96
+ `brand/convert.js` and `brand/veltix-convert.js` each required `puppeteer-core`,
97
+ which is neither installed nor declared in `package.json`, so a clean checkout
98
+ failed with module-not-found before rendering anything. Both also hardcoded
99
+ `/Applications/Google Chrome.app/...`, so neither could have run on Linux or
100
+ Windows even with the dependency present. `.gitignore` names `brand/convert.js`
101
+ as the way to regenerate `brand/*.png`; it pointed at a script that crashed on
102
+ line 9.
103
+
104
+ `convert.js` was wrong in two further ways. It referenced `favicon.svg`, which
105
+ does not exist in that directory, so three of its eleven entries rendered nothing.
106
+ And it omitted five assets that do have published PNGs — the ECC badge, the icon
107
+ at both 192 and 512, and both ECC lockups, plus the legacy post. Its list is now
108
+ derived from the files actually present.
109
+
110
+ ### What changed
111
+
112
+ Both now use `rsvg-convert`. That keeps ECC's no-runtime-dependency rule intact —
113
+ adding `puppeteer-core` would pull a large dependency into a package that
114
+ deliberately has none — and matches `scripts/brand/export-assets.js`, so the whole
115
+ brand pipeline renders through one tool.
116
+
117
+ Sizes reduce to a target **width**. Every published PNG was checked against its
118
+ SVG's viewBox and all twenty-one are exact aspect-preserving scales, with ratios
119
+ from 2x to 6x, so letting rsvg derive the height reproduces each one without a
120
+ second number that can drift from the artwork. Every output keeps its exact
121
+ published dimensions, captured before and after.
122
+
123
+ The renderer substitution was verified rather than assumed: re-rendering a
124
+ Chrome-produced export gave a byte-comparable file at identical dimensions, and
125
+ the text-bearing assets were viewed after rendering. The old `document.fonts.ready`
126
+ wait needs no counterpart — these SVGs ask for `'Space Grotesk'`, `'Inter'`,
127
+ `system-ui`, which are font-family names resolved against installed fonts rather
128
+ than `@font-face` webfonts, so rsvg resolves them the same way Chrome did.
129
+
130
+ ### One gate for all brand artwork
131
+
132
+ All four scripts now share a `--check` mode that exits non-zero when anything is
133
+ stale:
134
+
135
+ ```bash
136
+ node brand/convert.js --check \
137
+ && node brand/veltix-convert.js --check \
138
+ && node scripts/brand/sync-asset-version.js --check \
139
+ && node scripts/brand/export-assets.js --check
140
+ ```
141
+
142
+ This release is the first to run that pipeline end to end: bump, stamp the version
143
+ into 46 SVGs across both repositories, then render 70 PNGs.
144
+
145
+ **672 tests across 43 files.**
146
+
5
147
  ## v2.24.6 — brand assets advertised the wrong version and stale counts (October 2026)
6
148
 
7
149
  Assets and documentation only. No code changes to the toolkit.
package/CLAUDE.md CHANGED
@@ -26,7 +26,7 @@ scripts/ → Node.js utilities: MCP server, dashboard, swarm, replay, router
26
26
  bundles/ → 3 power bundles (indie-hacker, red-team, enterprise)
27
27
  actions/ → GitHub Action (CI/CD integration for PR review)
28
28
  docs/ → Feature docs (arena.md, mcp.md, dashboard.md, swarm.md, replay.md, evolve.md, supply-chain.md)
29
- tests/ → 672 passing tests across 43 test files
29
+ tests/ → 682 passing tests across 43 test files
30
30
  ```
31
31
 
32
32
  ## Running Tests
package/README.md CHANGED
@@ -118,7 +118,7 @@ Most "AI agent kits" are folders of markdown files you have to remember the name
118
118
  | Quality hooks | ✓ 44 | Some | ✗ | ✗ |
119
119
  | IDE platforms | **11** (Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, Roo Code, Aider, Kimi, Gemini CLI) | 1-2 | 1 | Varies |
120
120
  | Telemetry | **None** | Varies | None | Varies |
121
- | Test coverage | ✓ 672 tests | ✗ | ✗ | ✗ |
121
+ | Test coverage | ✓ 682 tests | ✗ | ✗ | ✗ |
122
122
  | Distributed via | `npx`, curl, clone | Manual | Manual | Manual |
123
123
 
124
124
  ---
@@ -1288,7 +1288,7 @@ if (args[0] === 'session-export' || args[0] === 'session-import' || args[0] ===
1288
1288
  // Usage:
1289
1289
  // kodelyth-ecc sbom [--root DIR] [--out FILE] [--json]
1290
1290
  // kodelyth-ecc manifest [--root DIR] [--out FILE] [--json]
1291
- // kodelyth-ecc verify [--root DIR] [--manifest FILE] [--json]
1291
+ // kodelyth-ecc verify [--root DIR] [--manifest FILE] [--json] [--strict]
1292
1292
  if (args[0] === 'sbom' || args[0] === 'manifest' || args[0] === 'verify') {
1293
1293
  (async () => {
1294
1294
  const fs = require('fs');
@@ -1355,7 +1355,12 @@ if (args[0] === 'sbom' || args[0] === 'manifest' || args[0] === 'verify') {
1355
1355
  throw new Error(`verify: manifest not found at ${manifestPath} (use --manifest FILE)`);
1356
1356
  }
1357
1357
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1358
- const report = verifyAgainstManifest({ rootDir, manifest });
1358
+ // --strict folds EXTRA files into the verdict. Off by default, because a
1359
+ // toolkit people extend would otherwise fail verify for everyone who added
1360
+ // their own agent; on for automation, where an unknown file under rules/ or
1361
+ // agents/ is loaded as instructions and should stop a deploy.
1362
+ const strict = has('--strict');
1363
+ const report = verifyAgainstManifest({ rootDir, manifest, strict });
1359
1364
 
1360
1365
  if (has('--json')) {
1361
1366
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
@@ -1370,7 +1375,7 @@ if (args[0] === 'sbom' || args[0] === 'manifest' || args[0] === 'verify') {
1370
1375
  console.log(` ✓ ok: ${s.ok}`);
1371
1376
  console.log(` ✗ modified: ${s.modified}`);
1372
1377
  console.log(` ✗ missing: ${s.missing}`);
1373
- console.log(` ⚠ extra: ${s.extra} (advisory)`);
1378
+ console.log(` ⚠ extra: ${s.extra} ${strict ? '(fails verify: --strict)' : '(advisory — use --strict to fail on these)'}`);
1374
1379
 
1375
1380
  if (report.details.modified.length) {
1376
1381
  console.log(`\nModified files:`);
@@ -1724,6 +1729,7 @@ if (args.includes('--help') || args.includes('-h')) {
1724
1729
  npx kodelyth-ecc sbom [--out file.json] Emit a CycloneDX 1.5 SBOM for the package
1725
1730
  npx kodelyth-ecc manifest [--out file.json] Emit a sha256 content manifest of all assets
1726
1731
  npx kodelyth-ecc verify [--manifest file] Verify installed copy against a content manifest
1732
+ npx kodelyth-ecc verify --strict ...and fail on unexpected extra files too
1727
1733
  npx kodelyth-ecc evolve analyze Generate skill / routing proposals from memory + miss signals
1728
1734
  npx kodelyth-ecc dashboard Boot localhost-only observability dashboard (zero telemetry)
1729
1735
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodelyth-ecc",
3
- "version": "2.24.6",
3
+ "version": "2.24.8",
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",
@@ -335,10 +335,36 @@ function sessionDetail({ session, coordRoot = defaultCoordRoot() } = {}) {
335
335
  }
336
336
 
337
337
  function safeReadExcerpt(p, max = 800) {
338
+ // Reads only the window it needs. This used to readFileSync the WHOLE file and
339
+ // then slice 800 characters off the front, which meant a large file in a
340
+ // coordination directory cost its full size in time and memory to produce a
341
+ // fixed-size excerpt. Measured: a 300MB task.md took 693ms and +733MB RSS to
342
+ // return 800 characters, scaling linearly — and the dashboard is single
343
+ // threaded, so that blocks every other request. It is called three times per
344
+ // worker (task, handoff, status) across every session, so the cost multiplies.
345
+ //
346
+ // The window is max*4 bytes because UTF-8 encodes a character in at most four,
347
+ // so it always contains at least `max` characters when the file has them. That
348
+ // matters: the truncation decision below counts CHARACTERS, as it always did,
349
+ // so slicing a byte window would otherwise cut a multi-byte character in half
350
+ // and change the output for any non-ASCII file.
351
+ let fd;
338
352
  try {
339
- const content = fs.readFileSync(p, 'utf8');
353
+ const st = fs.statSync(p);
354
+ if (!st.isFile()) return null;
355
+ const window = Math.min(st.size, max * 4 + 4);
356
+ const buf = Buffer.alloc(window);
357
+ fd = fs.openSync(p, 'r');
358
+ const bytesRead = fs.readSync(fd, buf, 0, window, 0);
359
+ const content = buf.subarray(0, bytesRead).toString('utf8');
340
360
  return content.length > max ? content.slice(0, max) + '\n…(truncated)' : content;
341
- } catch { return null; }
361
+ } catch {
362
+ return null;
363
+ } finally {
364
+ if (fd !== undefined) {
365
+ try { fs.closeSync(fd); } catch { /* already gone */ }
366
+ }
367
+ }
342
368
  }
343
369
 
344
370
  // ── IDE sessions (Claude Code / Windsurf / Antigravity) ──────────────────────
@@ -183,7 +183,25 @@ function buildOrchestrationPlan(config = {}) {
183
183
  config.coordinationRoot || path.join(repoRoot, '.orchestration')
184
184
  );
185
185
  const coordinationDir = path.join(coordinationRoot, sessionName);
186
+ // baseRef is the only value in this file that reaches a command's argv without
187
+ // being slugified or path-resolved first. Every spawn here uses the array form
188
+ // with no shell, which stops shell injection — but it does NOT stop ARGUMENT
189
+ // injection: git parses argv itself, so a ref beginning with "-" is read as an
190
+ // option rather than a commit-ish. In
191
+ // git worktree add -b <branch> <path> <commit-ish>
192
+ // a baseRef of "--git-dir=/elsewhere" silently redirects git at another
193
+ // repository, and "--force" changes the command's meaning. It is user-settable
194
+ // through --base-ref, and swarm configs get shared, so it is not only
195
+ // self-inflicted.
196
+ //
197
+ // Rejecting a leading "-" is enough: git refs cannot begin with one anyway
198
+ // (git check-ref-format forbids it), so nothing legitimate is lost.
186
199
  const baseRef = config.baseRef || 'HEAD';
200
+ if (typeof baseRef !== 'string' || baseRef.startsWith('-')) {
201
+ throw new Error(
202
+ `baseRef must be a ref name, not an option: ${JSON.stringify(String(baseRef))}`
203
+ );
204
+ }
187
205
  const defaultLauncher = config.launcherCommand || '';
188
206
 
189
207
  if (workers.length === 0) {
@@ -23,7 +23,7 @@ const { hashFile, walkFiles, MANIFEST_SCHEMA } = require('./manifest.js');
23
23
 
24
24
  const VERIFY_RESULT_KEYS = ['ok', 'modified', 'missing', 'extra'];
25
25
 
26
- function verifyAgainstManifest({ rootDir, manifest } = {}) {
26
+ function verifyAgainstManifest({ rootDir, manifest, strict = false } = {}) {
27
27
  if (!rootDir) throw new Error('verifyAgainstManifest: rootDir is required');
28
28
  if (!manifest) throw new Error('verifyAgainstManifest: manifest is required');
29
29
  if (manifest.schema !== MANIFEST_SCHEMA) {
@@ -81,11 +81,27 @@ function verifyAgainstManifest({ rootDir, manifest } = {}) {
81
81
  extra: extraFiles.length,
82
82
  };
83
83
 
84
- // verify is "ok" if no missing and no modified files. Extras are non-fatal.
85
- const ok = summary.missing === 0 && summary.modified === 0;
84
+ // By default an EXTRA file is advisory: it does not fail the verdict. That is
85
+ // deliberate and correct for a toolkit people are meant to extend — anyone who
86
+ // has added their own agent or skill would otherwise see verify fail forever.
87
+ //
88
+ // It is the wrong default for automation, though. A planted file returns ok
89
+ // true and exit code 0, so `verify && deploy` passes with an unknown file in
90
+ // place — and in this product an extra .md under rules/, agents/ or commands/
91
+ // is not inert data, it is loaded as instructions, so an injected
92
+ // rules/common/zz.md becomes an always-on rule. Confirmed: a clean tree reports
93
+ // ok true, planting that file still reports ok true with extra 1.
94
+ //
95
+ // So the choice is offered rather than made. strict folds extras into the
96
+ // verdict for callers that want a gate; the default is unchanged.
97
+ const ok =
98
+ summary.missing === 0 &&
99
+ summary.modified === 0 &&
100
+ (!strict || summary.extra === 0);
86
101
 
87
102
  return {
88
103
  ok,
104
+ strict,
89
105
  summary,
90
106
  details: {
91
107
  ok: okFiles,