kodelyth-ecc 1.5.9 → 1.7.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/.github/workflows/publish.yml +19 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +455 -0
- package/CLAUDE.md +48 -16
- package/README.md +265 -81
- package/VERSION +1 -1
- package/actions/ecc-review/README.md +243 -0
- package/actions/ecc-review/action.yml +158 -0
- package/actions/ecc-review/post-comment.js +171 -0
- package/actions/ecc-review/run-review.js +285 -0
- package/agents/backdoor-hunter.md +260 -0
- package/agents/chaos-engineer.md +251 -0
- package/agents/code-stealer-detector.md +228 -0
- package/agents/jailbreak-tester.md +222 -0
- package/agents/license-violation-finder.md +212 -0
- package/agents/prompt-injection-hunter.md +126 -0
- package/agents/secret-hunter.md +209 -0
- package/agents/supply-chain-auditor.md +195 -0
- package/bin/kodelyth-ecc.js +875 -1
- package/bundles/enterprise.md +172 -0
- package/bundles/indie-hacker.md +106 -0
- package/bundles/red-team.md +138 -0
- package/cat +0 -0
- package/commands/dashboard.md +67 -0
- package/commands/devil-mode.md +121 -0
- package/commands/memory-evolve.md +71 -0
- package/commands/replay.md +61 -0
- package/commands/route-model.md +48 -0
- package/commands/swarm.md +68 -0
- package/commands/verify-supply-chain.md +59 -0
- package/docs/dashboard.md +211 -0
- package/docs/evolve.md +303 -0
- package/docs/mcp-clients.md +167 -0
- package/docs/mcp.md +178 -0
- package/docs/replay.md +244 -0
- package/docs/supply-chain.md +207 -0
- package/docs/swarm.md +243 -0
- package/hooks/hooks.json +52 -0
- package/hooks/memory/auto-recall.js +29 -1
- package/hooks/safety/README.md +124 -0
- package/hooks/safety/lib/patterns.js +179 -0
- package/hooks/safety/prompt-injection-guard.js +179 -0
- package/hooks/safety/token-budget.js +229 -0
- package/install.ps1 +199 -2
- package/install.sh +286 -4
- package/package.json +30 -4
- package/rules/common/agent-intent-routing.md +182 -0
- package/rules/common/cost-aware-model-routing.md +152 -0
- package/scripts/dashboard/data.js +353 -0
- package/scripts/dashboard/server.js +324 -0
- package/scripts/dashboard/static/index.html +645 -0
- package/scripts/evolve/analyze.js +303 -0
- package/scripts/evolve/proposals.js +162 -0
- package/scripts/evolve/stats.js +219 -0
- package/scripts/mcp/catalog.js +244 -0
- package/scripts/mcp/client.js +181 -0
- package/scripts/mcp/prompts.js +133 -0
- package/scripts/mcp/resources.js +94 -0
- package/scripts/mcp/server.js +153 -0
- package/scripts/mcp/tools.js +465 -0
- package/scripts/replay/bundle.js +191 -0
- package/scripts/replay/replay.js +107 -0
- package/scripts/router/classify.js +232 -0
- package/scripts/supply-chain/manifest.js +154 -0
- package/scripts/supply-chain/sbom.js +202 -0
- package/scripts/supply-chain/verify.js +102 -0
- package/scripts/swarm/build-plan.js +193 -0
- package/skills/cost-aware-model-routing/SKILL.md +153 -0
- package/skills/kodelyth-quickstart/SKILL.md +7 -0
- package/skills/observability-dashboard/SKILL.md +119 -0
- package/skills/self-evolving-memory/SKILL.md +175 -0
- package/skills/session-replay/SKILL.md +199 -0
- package/skills/supply-chain-verification/SKILL.md +201 -0
- package/skills/swarm-orchestrator/SKILL.md +177 -0
- package/social/card-install.svg +1 -1
- package/social/facebook-group/POST.md +121 -0
- package/social/facebook-group/fb-1-3am-debug.png +0 -0
- package/social/facebook-group/fb-1-3am-debug.svg +97 -0
- package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
- package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
- package/social/facebook-group/fb-3-before-after.png +0 -0
- package/social/facebook-group/fb-3-before-after.svg +94 -0
- package/social/facebook-v150.svg +5 -5
- package/social/github-social-preview.svg +119 -100
- package/social/readme-hero.svg +11 -11
- package/social/section-agents.svg +57 -0
- package/social/section-author.svg +54 -0
- package/social/section-dashboard.svg +59 -0
- package/social/section-devil.svg +54 -0
- package/social/section-hooks.svg +51 -0
- package/social/section-install.svg +42 -0
- package/social/section-learning.svg +52 -0
- package/social/section-mcp.svg +46 -0
- package/social/section-memory.svg +57 -0
- package/social/section-parallel.svg +72 -0
- package/social/section-routing.svg +51 -0
- package/social/x-card-agents-grid.svg +6 -6
- package/tests/dashboard/data.test.js +235 -0
- package/tests/dashboard/server.test.js +240 -0
- package/tests/evolve/analyze.test.js +169 -0
- package/tests/evolve/proposals.test.js +173 -0
- package/tests/evolve/stats.test.js +159 -0
- package/tests/mcp/catalog.test.js +98 -0
- package/tests/mcp/client.test.js +109 -0
- package/tests/mcp/resources-prompts.test.js +70 -0
- package/tests/mcp/tools.test.js +159 -0
- package/tests/replay/bundle.test.js +181 -0
- package/tests/replay/replay.test.js +131 -0
- package/tests/router/classify.test.js +164 -0
- package/tests/safety/patterns.test.js +94 -0
- package/tests/safety/prompt-injection-guard.test.js +111 -0
- package/tests/safety/token-budget.test.js +119 -0
- package/tests/supply-chain/manifest.test.js +147 -0
- package/tests/supply-chain/sbom.test.js +170 -0
- package/tests/supply-chain/verify.test.js +146 -0
- package/tests/swarm/build-plan.test.js +188 -0
- package/wiki/Agent-Reference.md +58 -7
- package/wiki/FAQ.md +204 -7
- package/wiki/Home.md +104 -28
- package/wiki/Hook-Reference.md +1 -1
- package/wiki/Installation-Guide.md +109 -6
- package/wiki/Platform-Support.md +239 -25
- package/wiki/Skill-Reference.md +101 -6
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// scripts/supply-chain/sbom.js
|
|
2
|
+
//
|
|
3
|
+
// CycloneDX 1.5 Software Bill of Materials generator for kodelyth-ecc.
|
|
4
|
+
//
|
|
5
|
+
// Pure functions — given { rootDir }, produces a CycloneDX-shaped JS object.
|
|
6
|
+
// No network, no external deps. Reads package.json + package-lock.json (v3).
|
|
7
|
+
//
|
|
8
|
+
// Consumers should JSON.stringify with deterministic key order
|
|
9
|
+
// (we already build keys in fixed order so default JSON.stringify is fine).
|
|
10
|
+
//
|
|
11
|
+
// Exports:
|
|
12
|
+
// - SBOM_SPEC_VERSION
|
|
13
|
+
// - generateSBOM({ rootDir, timestamp? }) → CycloneDX object
|
|
14
|
+
// - readSBOMSources({ rootDir }) → { pkg, lock } (mostly for tests)
|
|
15
|
+
// - buildComponentFromLockEntry(name, entry) → CycloneDX component
|
|
16
|
+
//
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const SBOM_SPEC_VERSION = '1.5';
|
|
23
|
+
const SBOM_FORMAT = 'CycloneDX';
|
|
24
|
+
|
|
25
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function readJSON(p) {
|
|
28
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function safeReadJSON(p) {
|
|
32
|
+
try { return readJSON(p); } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readSBOMSources({ rootDir }) {
|
|
36
|
+
const pkgPath = path.join(rootDir, 'package.json');
|
|
37
|
+
const lockPath = path.join(rootDir, 'package-lock.json');
|
|
38
|
+
const pkg = readJSON(pkgPath);
|
|
39
|
+
const lock = safeReadJSON(lockPath);
|
|
40
|
+
return { pkg, lock };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// purl (package URL) per spec: pkg:npm/<name>@<version>
|
|
44
|
+
// Scoped packages keep the leading @ but the slash is encoded.
|
|
45
|
+
function npmPurl(name, version) {
|
|
46
|
+
if (!name || !version) return null;
|
|
47
|
+
// scoped: @scope/name → @scope/name (slash kept, leading @ kept)
|
|
48
|
+
// CycloneDX uses raw form; npm purl spec keeps "@scope/name"
|
|
49
|
+
return `pkg:npm/${name}@${version}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function bomRefFor(name, version) {
|
|
53
|
+
return `${name}@${version}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildComponentFromLockEntry(installPath, entry) {
|
|
57
|
+
// installPath is the lockfile key like "node_modules/foo" or "" (root)
|
|
58
|
+
// entry has: { name?, version, dev?, optional?, devOptional?, license?, integrity?, resolved? }
|
|
59
|
+
const name = entry.name || pkgNameFromPath(installPath);
|
|
60
|
+
if (!name || !entry.version) return null;
|
|
61
|
+
|
|
62
|
+
const component = {
|
|
63
|
+
type: 'library',
|
|
64
|
+
'bom-ref': bomRefFor(name, entry.version),
|
|
65
|
+
name,
|
|
66
|
+
version: entry.version,
|
|
67
|
+
purl: npmPurl(name, entry.version),
|
|
68
|
+
scope: entry.dev ? 'optional' : 'required',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (entry.license) component.licenses = normalizeLicense(entry.license);
|
|
72
|
+
if (entry.integrity) component.hashes = parseIntegrity(entry.integrity);
|
|
73
|
+
|
|
74
|
+
return component;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pkgNameFromPath(installPath) {
|
|
78
|
+
// "node_modules/foo" → "foo"
|
|
79
|
+
// "node_modules/@scope/bar" → "@scope/bar"
|
|
80
|
+
// "node_modules/foo/node_modules/@scope/bar" → "@scope/bar" (deepest)
|
|
81
|
+
if (!installPath) return null;
|
|
82
|
+
const parts = installPath.split('node_modules/').filter(Boolean);
|
|
83
|
+
if (parts.length === 0) return null;
|
|
84
|
+
return parts[parts.length - 1].replace(/\/$/, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeLicense(license) {
|
|
88
|
+
// license may be a string ("MIT") or an array
|
|
89
|
+
if (typeof license === 'string') {
|
|
90
|
+
return [{ license: { id: license } }];
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(license)) {
|
|
93
|
+
return license.map(l => typeof l === 'string' ? { license: { id: l } } : { license: l });
|
|
94
|
+
}
|
|
95
|
+
if (license && typeof license === 'object') {
|
|
96
|
+
return [{ license }];
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseIntegrity(integrity) {
|
|
102
|
+
// npm SRI strings look like "sha512-base64==" — possibly multiple, space-separated.
|
|
103
|
+
return integrity.split(/\s+/).filter(Boolean).map(sri => {
|
|
104
|
+
const idx = sri.indexOf('-');
|
|
105
|
+
if (idx === -1) return null;
|
|
106
|
+
const alg = sri.slice(0, idx).toLowerCase();
|
|
107
|
+
const b64 = sri.slice(idx + 1);
|
|
108
|
+
// CycloneDX uses hex; convert base64 to hex.
|
|
109
|
+
let hex;
|
|
110
|
+
try { hex = Buffer.from(b64, 'base64').toString('hex'); }
|
|
111
|
+
catch { return null; }
|
|
112
|
+
const algMap = { sha256: 'SHA-256', sha384: 'SHA-384', sha512: 'SHA-512' };
|
|
113
|
+
return algMap[alg] ? { alg: algMap[alg], content: hex } : null;
|
|
114
|
+
}).filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── main entry ────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function generateSBOM({ rootDir, timestamp } = {}) {
|
|
120
|
+
if (!rootDir) throw new Error('generateSBOM: rootDir is required');
|
|
121
|
+
const { pkg, lock } = readSBOMSources({ rootDir });
|
|
122
|
+
|
|
123
|
+
const ts = timestamp || new Date().toISOString();
|
|
124
|
+
|
|
125
|
+
const rootComponent = {
|
|
126
|
+
type: 'application',
|
|
127
|
+
'bom-ref': bomRefFor(pkg.name, pkg.version),
|
|
128
|
+
name: pkg.name,
|
|
129
|
+
version: pkg.version,
|
|
130
|
+
purl: npmPurl(pkg.name, pkg.version),
|
|
131
|
+
description: pkg.description || undefined,
|
|
132
|
+
};
|
|
133
|
+
if (pkg.license) rootComponent.licenses = normalizeLicense(pkg.license);
|
|
134
|
+
|
|
135
|
+
const components = [];
|
|
136
|
+
const dependencies = [];
|
|
137
|
+
|
|
138
|
+
if (lock && lock.packages) {
|
|
139
|
+
// Walk every entry except the root ("")
|
|
140
|
+
for (const [installPath, entry] of Object.entries(lock.packages)) {
|
|
141
|
+
if (!installPath) continue; // root handled as metadata.component
|
|
142
|
+
const c = buildComponentFromLockEntry(installPath, entry);
|
|
143
|
+
if (c) components.push(c);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Dependency graph for the root only — direct dependencies + optionalDependencies.
|
|
147
|
+
const rootEntry = lock.packages[''] || {};
|
|
148
|
+
const directDeps = []
|
|
149
|
+
.concat(Object.keys(rootEntry.dependencies || {}))
|
|
150
|
+
.concat(Object.keys(rootEntry.optionalDependencies || {}));
|
|
151
|
+
if (directDeps.length) {
|
|
152
|
+
const seen = new Map();
|
|
153
|
+
for (const c of components) seen.set(c.name, c['bom-ref']);
|
|
154
|
+
const dependsOn = directDeps
|
|
155
|
+
.map(n => seen.get(n))
|
|
156
|
+
.filter(Boolean);
|
|
157
|
+
dependencies.push({
|
|
158
|
+
ref: rootComponent['bom-ref'],
|
|
159
|
+
dependsOn,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const bom = {
|
|
165
|
+
bomFormat: SBOM_FORMAT,
|
|
166
|
+
specVersion: SBOM_SPEC_VERSION,
|
|
167
|
+
serialNumber: `urn:uuid:${deriveSerial(pkg, ts)}`,
|
|
168
|
+
version: 1,
|
|
169
|
+
metadata: {
|
|
170
|
+
timestamp: ts,
|
|
171
|
+
tools: [{
|
|
172
|
+
vendor: 'kodelyth-ecc',
|
|
173
|
+
name: 'kodelyth-ecc-sbom',
|
|
174
|
+
version: pkg.version,
|
|
175
|
+
}],
|
|
176
|
+
component: rootComponent,
|
|
177
|
+
},
|
|
178
|
+
components,
|
|
179
|
+
dependencies,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return bom;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Derive a stable, deterministic UUID-ish serial from package + timestamp.
|
|
186
|
+
// Not a cryptographic UUID; it's stable across re-runs at same time.
|
|
187
|
+
function deriveSerial(pkg, ts) {
|
|
188
|
+
const crypto = require('crypto');
|
|
189
|
+
const h = crypto.createHash('sha256').update(`${pkg.name}@${pkg.version}|${ts}`).digest('hex');
|
|
190
|
+
// 8-4-4-4-12 layout
|
|
191
|
+
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
SBOM_SPEC_VERSION,
|
|
196
|
+
SBOM_FORMAT,
|
|
197
|
+
generateSBOM,
|
|
198
|
+
readSBOMSources,
|
|
199
|
+
buildComponentFromLockEntry,
|
|
200
|
+
// exposed for testing only
|
|
201
|
+
_internals: { npmPurl, parseIntegrity, normalizeLicense, pkgNameFromPath, deriveSerial },
|
|
202
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// scripts/supply-chain/verify.js
|
|
2
|
+
//
|
|
3
|
+
// Verify an installed copy of kodelyth-ecc against a shipped content manifest.
|
|
4
|
+
//
|
|
5
|
+
// Pure functions — given { rootDir, manifest }, returns a structured report
|
|
6
|
+
// with categorized differences. No process.exit calls; CLI layer decides.
|
|
7
|
+
//
|
|
8
|
+
// Categories:
|
|
9
|
+
// ok — file exists with matching size + sha256
|
|
10
|
+
// modified — file exists but sha256 differs
|
|
11
|
+
// missing — file in manifest but not on disk
|
|
12
|
+
// extra — file on disk under shipped dirs but not in manifest (advisory)
|
|
13
|
+
//
|
|
14
|
+
// Exports:
|
|
15
|
+
// - verifyAgainstManifest({ rootDir, manifest }) → { ok, summary, details }
|
|
16
|
+
// - VERIFY_RESULT_KEYS
|
|
17
|
+
//
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { hashFile, walkFiles, MANIFEST_SCHEMA } = require('./manifest.js');
|
|
23
|
+
|
|
24
|
+
const VERIFY_RESULT_KEYS = ['ok', 'modified', 'missing', 'extra'];
|
|
25
|
+
|
|
26
|
+
function verifyAgainstManifest({ rootDir, manifest } = {}) {
|
|
27
|
+
if (!rootDir) throw new Error('verifyAgainstManifest: rootDir is required');
|
|
28
|
+
if (!manifest) throw new Error('verifyAgainstManifest: manifest is required');
|
|
29
|
+
if (manifest.schema !== MANIFEST_SCHEMA) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`verifyAgainstManifest: unsupported manifest schema "${manifest.schema}" (expected "${MANIFEST_SCHEMA}")`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const okFiles = [];
|
|
36
|
+
const modifiedFiles = [];
|
|
37
|
+
const missingFiles = [];
|
|
38
|
+
const extraFiles = [];
|
|
39
|
+
|
|
40
|
+
const expectedByPath = new Map();
|
|
41
|
+
for (const entry of manifest.files || []) {
|
|
42
|
+
expectedByPath.set(entry.path, entry);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pass 1: every manifest file → check disk.
|
|
46
|
+
for (const entry of expectedByPath.values()) {
|
|
47
|
+
const abs = path.join(rootDir, entry.path);
|
|
48
|
+
if (!fs.existsSync(abs)) {
|
|
49
|
+
missingFiles.push({ path: entry.path, expected_sha256: entry.sha256 });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let actualSha;
|
|
53
|
+
try { actualSha = hashFile(abs); }
|
|
54
|
+
catch (err) {
|
|
55
|
+
missingFiles.push({ path: entry.path, expected_sha256: entry.sha256, error: err.message });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (actualSha === entry.sha256) {
|
|
59
|
+
okFiles.push(entry.path);
|
|
60
|
+
} else {
|
|
61
|
+
modifiedFiles.push({
|
|
62
|
+
path: entry.path,
|
|
63
|
+
expected_sha256: entry.sha256,
|
|
64
|
+
actual_sha256: actualSha,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Pass 2: walk disk → flag files NOT in manifest. Advisory only.
|
|
70
|
+
const onDisk = walkFiles(rootDir);
|
|
71
|
+
for (const abs of onDisk) {
|
|
72
|
+
const rel = path.relative(rootDir, abs).split(path.sep).join('/');
|
|
73
|
+
if (!expectedByPath.has(rel)) extraFiles.push(rel);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const summary = {
|
|
77
|
+
total_in_manifest: expectedByPath.size,
|
|
78
|
+
ok: okFiles.length,
|
|
79
|
+
modified: modifiedFiles.length,
|
|
80
|
+
missing: missingFiles.length,
|
|
81
|
+
extra: extraFiles.length,
|
|
82
|
+
};
|
|
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;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
ok,
|
|
89
|
+
summary,
|
|
90
|
+
details: {
|
|
91
|
+
ok: okFiles,
|
|
92
|
+
modified: modifiedFiles,
|
|
93
|
+
missing: missingFiles,
|
|
94
|
+
extra: extraFiles,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
VERIFY_RESULT_KEYS,
|
|
101
|
+
verifyAgainstManifest,
|
|
102
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Kodelyth ECC — Swarm Plan Builder (Phase 2.7)
|
|
3
|
+
//
|
|
4
|
+
// Promotes the existing `tmux-worktree-orchestrator` infrastructure to a first-
|
|
5
|
+
// class CLI surface. Takes simple flags (--task, --agents N, --harness <h>, ...)
|
|
6
|
+
// and produces a full plan compatible with scripts/lib/tmux-worktree-orchestrator.js.
|
|
7
|
+
//
|
|
8
|
+
// Pure-function builder. Zero side effects. Tested without spawning tmux/git.
|
|
9
|
+
//
|
|
10
|
+
// Two modes:
|
|
11
|
+
// 1. Generic swarm: N agents work in parallel on the SAME task in isolated
|
|
12
|
+
// worktrees, then their handoff files are merged for review.
|
|
13
|
+
// 2. Specialist swarm: a list of named ECC agents (architect, code-reviewer,
|
|
14
|
+
// security-reviewer, …) each run with an agent-shaped prompt over the
|
|
15
|
+
// same task. The swarm IS a generalized /devil-mode.
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// ── Smart-default specialist rotations (4 / 6 / 8 agents) ────────────────────
|
|
23
|
+
// Picked for breadth — code, security, architecture, test, perf, UX, API, docs.
|
|
24
|
+
const DEFAULT_ROTATIONS = {
|
|
25
|
+
4: ['code-reviewer', 'security-reviewer', 'pair-programmer', 'tdd-guide'],
|
|
26
|
+
6: ['code-reviewer', 'security-reviewer', 'pair-programmer', 'tdd-guide', 'performance-optimizer', 'api-guardian'],
|
|
27
|
+
8: ['code-reviewer', 'security-reviewer', 'pair-programmer', 'tdd-guide', 'performance-optimizer', 'api-guardian', 'ux-reviewer', 'doc-updater'],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ── Task-keyword signal weights for smart agent rotation ─────────────────────
|
|
31
|
+
const SIGNAL_AGENTS = [
|
|
32
|
+
{ regex: /\b(security|auth|vuln|cve|secrets?|injection|owasp)\b/i, agent: 'security-reviewer' },
|
|
33
|
+
{ regex: /\b(perf(?:ormance)?|slow|p99|latency|throughput|bottleneck)\b/i, agent: 'performance-optimizer' },
|
|
34
|
+
{ regex: /\b(load[\s-]?test|stress[\s-]?test|capacity|k6|locust|artillery)\b/i, agent: 'load-tester' },
|
|
35
|
+
{ regex: /\b(architect|design|adr|rfc|trade[\s-]?off|system\s+design)\b/i, agent: 'architect' },
|
|
36
|
+
{ regex: /\b(api|endpoint|contract|breaking\s+change|rest|graphql)\b/i, agent: 'api-guardian' },
|
|
37
|
+
{ regex: /\b(test|tdd|coverage|spec|unit\s+test)\b/i, agent: 'tdd-guide' },
|
|
38
|
+
{ regex: /\b(refactor|cleanup|tech\s+debt|code\s+smell|simplify)\b/i, agent: 'refactor-cleaner' },
|
|
39
|
+
{ regex: /\b(ux|accessibility|wcag|a11y|usability|ui)\b/i, agent: 'ux-reviewer' },
|
|
40
|
+
{ regex: /\b(doc(?:s|umentation)?|readme|guide|tutorial|comment)\b/i, agent: 'doc-updater' },
|
|
41
|
+
{ regex: /\b(release|ship|tag|semver|rollback)\b/i, agent: 'release-captain' },
|
|
42
|
+
{ regex: /\b(incident|outage|down|p[01]|sev[12]|on[\s-]?call)\b/i, agent: 'incident-commander' },
|
|
43
|
+
{ regex: /\b(debug|why\s+is|stack\s+trace|silent\s+fail)\b/i, agent: 'debug-detective' },
|
|
44
|
+
{ regex: /\b(database|sql|postgres|mysql|index|n\+1)\b/i, agent: 'database-reviewer' },
|
|
45
|
+
{ regex: /\b(devil[\s-]?mode|adversarial|red[\s-]?team)\b/i, agent: 'prompt-injection-hunter' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Always-included anchors when count >= N.
|
|
49
|
+
const BASELINE_ANCHORS = ['code-reviewer', 'pair-programmer'];
|
|
50
|
+
|
|
51
|
+
// ── Harness launcher templates ───────────────────────────────────────────────
|
|
52
|
+
// Placeholders: {worker_name} {worker_slug} {repo_root} {worktree_path}
|
|
53
|
+
// {branch_name} {task_file} {handoff_file} {status_file}
|
|
54
|
+
// {session_name}
|
|
55
|
+
const HARNESS_LAUNCHERS = {
|
|
56
|
+
claude: 'claude --print --dangerously-skip-permissions "$(cat {task_file_sh})" 2>&1 | tee -a {handoff_file_sh}; printf "\\nstate: completed\\n" >> {status_file_sh}',
|
|
57
|
+
codex: `bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}`,
|
|
58
|
+
opencode: 'opencode run --task-file {task_file_sh} --output {handoff_file_sh} --status {status_file_sh}',
|
|
59
|
+
windsurf: 'windsurf-cli run --task-file {task_file_sh} --output {handoff_file_sh}',
|
|
60
|
+
echo: `printf 'demo worker {worker_slug}\\n' >> {handoff_file_sh}; printf 'state: completed\\n' >> {status_file_sh}`,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ── Core: pick agents for a swarm of size N ──────────────────────────────────
|
|
64
|
+
function pickAgents({ count, task, explicit }) {
|
|
65
|
+
if (Array.isArray(explicit) && explicit.length > 0) {
|
|
66
|
+
return explicit.map(String);
|
|
67
|
+
}
|
|
68
|
+
const n = Math.max(1, Math.min(Number(count) || 4, 12));
|
|
69
|
+
const text = String(task || '');
|
|
70
|
+
const picked = new Set();
|
|
71
|
+
|
|
72
|
+
// 1. Signal-driven picks first (highest priority).
|
|
73
|
+
for (const { regex, agent } of SIGNAL_AGENTS) {
|
|
74
|
+
if (regex.test(text)) picked.add(agent);
|
|
75
|
+
if (picked.size >= n) break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Baseline anchors next.
|
|
79
|
+
for (const anchor of BASELINE_ANCHORS) {
|
|
80
|
+
if (picked.size >= n) break;
|
|
81
|
+
picked.add(anchor);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Fill from rotation if we still need more.
|
|
85
|
+
const rotationKey = n <= 4 ? 4 : (n <= 6 ? 6 : 8);
|
|
86
|
+
for (const a of DEFAULT_ROTATIONS[rotationKey]) {
|
|
87
|
+
if (picked.size >= n) break;
|
|
88
|
+
picked.add(a);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 4. Fallback fill (shouldn't normally hit).
|
|
92
|
+
const fallback = DEFAULT_ROTATIONS[8];
|
|
93
|
+
for (const a of fallback) {
|
|
94
|
+
if (picked.size >= n) break;
|
|
95
|
+
picked.add(a);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return [...picked].slice(0, n);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Core: build a worker plan-config for a single agent ──────────────────────
|
|
102
|
+
function buildWorker({ agent, task, harness, launcherCommand }) {
|
|
103
|
+
const launcher = launcherCommand || HARNESS_LAUNCHERS[harness];
|
|
104
|
+
if (!launcher) {
|
|
105
|
+
throw new Error(`No launcher template for harness "${harness}". Pass --launcher-cmd or use one of: ${Object.keys(HARNESS_LAUNCHERS).join(', ')}.`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
name: agent,
|
|
109
|
+
task: shapeTaskForAgent(agent, task),
|
|
110
|
+
launcherCommand: launcher,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function shapeTaskForAgent(agent, task) {
|
|
115
|
+
return [
|
|
116
|
+
`# ${agent} — swarm task`,
|
|
117
|
+
'',
|
|
118
|
+
`You are running as the ECC \`${agent}\` specialist in a parallel swarm. ` +
|
|
119
|
+
'Other agents are running the same shared task in sibling worktrees. ' +
|
|
120
|
+
'Stay strictly inside your specialty — do not duplicate what other agents will cover. ' +
|
|
121
|
+
'Produce a focused handoff that names exactly which findings are yours and yours alone.',
|
|
122
|
+
'',
|
|
123
|
+
'## Shared Task',
|
|
124
|
+
'',
|
|
125
|
+
String(task || '').trim() || '(no task description provided)',
|
|
126
|
+
'',
|
|
127
|
+
'## Required handoff sections',
|
|
128
|
+
'1. Summary',
|
|
129
|
+
'2. Files Changed',
|
|
130
|
+
'3. Validation',
|
|
131
|
+
'4. Remaining Risks',
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Public API: full plan ────────────────────────────────────────────────────
|
|
136
|
+
function buildSwarmPlan({
|
|
137
|
+
task,
|
|
138
|
+
agents,
|
|
139
|
+
count = 4,
|
|
140
|
+
harness = 'claude',
|
|
141
|
+
launcherCmd,
|
|
142
|
+
sessionName,
|
|
143
|
+
repoRoot = process.cwd(),
|
|
144
|
+
worktreeRoot,
|
|
145
|
+
coordinationRoot,
|
|
146
|
+
baseRef = 'HEAD',
|
|
147
|
+
seedPaths = [],
|
|
148
|
+
replaceExisting = false,
|
|
149
|
+
} = {}) {
|
|
150
|
+
if (!task || typeof task !== 'string' || task.trim().length === 0) {
|
|
151
|
+
throw new Error('buildSwarmPlan: --task is required');
|
|
152
|
+
}
|
|
153
|
+
const explicit = Array.isArray(agents) ? agents : null;
|
|
154
|
+
const picked = pickAgents({ count, task, explicit });
|
|
155
|
+
|
|
156
|
+
const session = sessionName || `swarm-${new Date().toISOString().slice(0,10)}-${picked.length}a`;
|
|
157
|
+
|
|
158
|
+
const workers = picked.map(agent => buildWorker({
|
|
159
|
+
agent,
|
|
160
|
+
task,
|
|
161
|
+
harness,
|
|
162
|
+
launcherCommand: launcherCmd,
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
sessionName: session,
|
|
167
|
+
repoRoot,
|
|
168
|
+
worktreeRoot,
|
|
169
|
+
coordinationRoot,
|
|
170
|
+
baseRef,
|
|
171
|
+
replaceExisting: !!replaceExisting,
|
|
172
|
+
seedPaths,
|
|
173
|
+
workers,
|
|
174
|
+
// For debugging / dashboards:
|
|
175
|
+
_meta: {
|
|
176
|
+
task,
|
|
177
|
+
harness,
|
|
178
|
+
pickedAgents: picked,
|
|
179
|
+
count: picked.length,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
HARNESS_LAUNCHERS,
|
|
186
|
+
DEFAULT_ROTATIONS,
|
|
187
|
+
BASELINE_ANCHORS,
|
|
188
|
+
SIGNAL_AGENTS,
|
|
189
|
+
pickAgents,
|
|
190
|
+
shapeTaskForAgent,
|
|
191
|
+
buildWorker,
|
|
192
|
+
buildSwarmPlan,
|
|
193
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cost-aware-model-routing
|
|
3
|
+
description: Pick the right model tier (trivial/standard/hard) for each ECC session task. Companion to the cost-aware-model-routing rule and the token-budget safety hook. Use when the user asks "what model should I use" or when a session is burning frontier-model tokens on routine work.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Cost-Aware Model Routing — Explicit Skill
|
|
8
|
+
|
|
9
|
+
The companion skill to the always-on routing rule. Invoke this when the user explicitly asks "what model should I use for this task" or when you (the agent) want to surface a routing recommendation deliberately rather than via the implicit rule.
|
|
10
|
+
|
|
11
|
+
> Pairs with `rules/common/cost-aware-model-routing.md` (the always-on rule), `hooks/safety/token-budget.js` (spend control), and the `cost-aware-llm-pipeline` skill (patterns for user code).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## When to invoke
|
|
16
|
+
|
|
17
|
+
Trigger this skill when:
|
|
18
|
+
|
|
19
|
+
- The user asks "which model should I use", "is opus overkill for this", "can haiku handle this", "what's the cheapest tier for this".
|
|
20
|
+
- A session is approaching budget limits and the next task looks trivial.
|
|
21
|
+
- The user is on a frontier model but the next task is mechanical (rename, format, doc).
|
|
22
|
+
- You want to teach the user how to route deliberately for the next 10 turns instead of just this one.
|
|
23
|
+
|
|
24
|
+
Do NOT invoke when:
|
|
25
|
+
|
|
26
|
+
- The user is mid-task on something hard (architecture, incident, security audit) — keep them on the high tier.
|
|
27
|
+
- The user has explicitly chosen a model in this session — respect their choice.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
### 1. Classify the current task
|
|
34
|
+
|
|
35
|
+
Use the three-tier framework from the rule:
|
|
36
|
+
|
|
37
|
+
| Tier | Examples |
|
|
38
|
+
|---|---|
|
|
39
|
+
| **trivial** | Rename, format, fix typo, add JSDoc, summarize, list files, version bump, kebab→camel |
|
|
40
|
+
| **standard** | Code review of a PR, write a unit test, refactor one file, fix a non-critical bug, write a doc section |
|
|
41
|
+
| **hard** | Architecture decision, debug-blitz of intermittent prod bug, security audit, multi-file refactor (3+ files), incident postmortem, devil-mode, agent harness changes |
|
|
42
|
+
|
|
43
|
+
Weight signals together. Single signals are noisy; three or four together are reliable.
|
|
44
|
+
|
|
45
|
+
### 2. Read the team's config
|
|
46
|
+
|
|
47
|
+
Check for `.kodelyth/router.json` at the project root and these env vars:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
KODELYTH_ROUTER off | (unset)
|
|
51
|
+
KODELYTH_ROUTER_TRIVIAL override trivial-tier model id
|
|
52
|
+
KODELYTH_ROUTER_STANDARD override standard-tier model id
|
|
53
|
+
KODELYTH_ROUTER_HARD override hard-tier model id
|
|
54
|
+
KODELYTH_ROUTER_DEFAULT trivial | standard | hard (fallback for ambiguous tasks)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If `KODELYTH_ROUTER=off`, do NOT emit recommendations — the team has chosen to opt out.
|
|
58
|
+
|
|
59
|
+
### 3. Emit a single, structured recommendation
|
|
60
|
+
|
|
61
|
+
Use this exact format so the user can scan it instantly:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
[model-router] task=<tier> · suggested=<model-id> · current=<active or "unknown">
|
|
65
|
+
why: <one short sentence on the dominant signals>
|
|
66
|
+
next: <how to switch — provider-specific>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Only emit when the active model is mismatched with the suggested tier. If the user is already on the right tier, route silently and do not interrupt.
|
|
70
|
+
|
|
71
|
+
### 4. If the user wants ad-hoc routing for a future task
|
|
72
|
+
|
|
73
|
+
Tell them:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
npx kodelyth-ecc route "<task description>" # quick CLI hint
|
|
77
|
+
npx kodelyth-ecc route "<task>" --files 5 --agent debug-detective
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or for an in-session check, they can `use cost-aware-model-routing` and describe the task.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Hard rules (never break these)
|
|
85
|
+
|
|
86
|
+
1. **Security / incident / adversarial work never downgrades.** If the active agent is `security-reviewer`, `incident-commander`, `debug-detective`, or any devil-mode agent (`prompt-injection-hunter`, `supply-chain-auditor`, `secret-hunter`, `backdoor-hunter`, `chaos-engineer`, `jailbreak-tester`, `code-stealer-detector`, `license-violation-finder`) — recommend **hard** tier even if the surface task looks trivial.
|
|
87
|
+
2. **Production framing wins.** "Down", "leaking", "outage", "incident", "P0", "P1" → hard.
|
|
88
|
+
3. **Multi-file refactors (3+) never go to trivial.**
|
|
89
|
+
4. **Respect explicit user choice.** If the user said "use haiku", don't override unless a new turn introduces conflicting signals.
|
|
90
|
+
5. **Budget-aware:** if `token-budget` reports usage > 70% of budget, bias one tier downward when ambiguous to extend the session.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Examples
|
|
95
|
+
|
|
96
|
+
### Example 1 — clear downgrade opportunity
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
User: "rename getUserName to getUserDisplayName everywhere"
|
|
100
|
+
[active model: claude-opus-4-1]
|
|
101
|
+
|
|
102
|
+
Recommendation:
|
|
103
|
+
[model-router] task=trivial · suggested=claude-haiku-4-5-20251001 · current=claude-opus-4-1
|
|
104
|
+
why: single mechanical rename, no behavior change, no tests need updating.
|
|
105
|
+
next: switch to /model claude-haiku for this turn — saves ~80% in tokens with no quality loss.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Example 2 — never downgrade incident work
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
User: "production is down and the auth service is throwing 502s, help"
|
|
112
|
+
[active model: claude-haiku-4-5-20251001]
|
|
113
|
+
|
|
114
|
+
Recommendation:
|
|
115
|
+
[model-router] task=hard · suggested=claude-opus-4-1 · current=claude-haiku-4-5-20251001
|
|
116
|
+
why: production incident with security-relevant subsystem.
|
|
117
|
+
next: switch to /model claude-opus before continuing — this is incident-commander territory.
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Example 3 — match, no recommendation
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
User: "review this 40-line PR for typos and obvious bugs"
|
|
124
|
+
[active model: claude-sonnet-4-6]
|
|
125
|
+
|
|
126
|
+
Recommendation: (silent — already on the right tier)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Example 4 — devil-mode never downgrades
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
User: "/devil-mode --all"
|
|
133
|
+
[active model: claude-haiku-4-5-20251001]
|
|
134
|
+
|
|
135
|
+
Recommendation:
|
|
136
|
+
[model-router] task=hard · suggested=claude-opus-4-1 · current=claude-haiku-4-5-20251001
|
|
137
|
+
why: devil-mode adversarial sweep — security/quality assertions need frontier reasoning.
|
|
138
|
+
next: switch to /model claude-opus or your equivalent frontier model.
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Implementation references
|
|
144
|
+
|
|
145
|
+
- Rule: `rules/common/cost-aware-model-routing.md`
|
|
146
|
+
- Classifier: `scripts/router/classify.js` (pure-function, deterministic)
|
|
147
|
+
- CLI: `npx kodelyth-ecc route <task>`
|
|
148
|
+
- Companion safety hook: `hooks/safety/token-budget.js` (spend cap)
|
|
149
|
+
- Skill for user-code patterns (different scope): `cost-aware-llm-pipeline`
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
Built into [Kodelyth ECC](../../README.md). MIT licensed.
|
|
@@ -166,6 +166,12 @@ use debug-detective
|
|
|
166
166
|
```
|
|
167
167
|
Tell the agent the symptom. It will trace the bug to its root cause.
|
|
168
168
|
|
|
169
|
+
**Bonus: See your AI workspace at a glance**
|
|
170
|
+
```
|
|
171
|
+
npx kodelyth-ecc dashboard
|
|
172
|
+
```
|
|
173
|
+
Opens the local observability dashboard at `http://127.0.0.1:5747`. Inspect memory captures, BM25 search across past solutions, evolve proposals, the full skill and agent catalog, swarm sessions, and token-budget snapshots — all local, zero telemetry.
|
|
174
|
+
|
|
169
175
|
---
|
|
170
176
|
|
|
171
177
|
## Common Questions
|
|
@@ -200,6 +206,7 @@ BUILD BROKEN? → use build-error-resolver or /build-fix
|
|
|
200
206
|
UX REVIEW? → use ux-reviewer
|
|
201
207
|
LOAD LANGUAGE PATTERNS? → /python-patterns, /golang-patterns, etc.
|
|
202
208
|
GENERAL CODE STANDARDS? → /coding-standards
|
|
209
|
+
OBSERVABILITY DASHBOARD? → npx kodelyth-ecc dashboard (localhost:5747)
|
|
203
210
|
```
|
|
204
211
|
|
|
205
212
|
---
|