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,170 @@
|
|
|
1
|
+
// Tests for scripts/supply-chain/sbom.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
const SBOM = require('../../scripts/supply-chain/sbom.js');
|
|
11
|
+
const { generateSBOM, buildComponentFromLockEntry, _internals } = SBOM;
|
|
12
|
+
|
|
13
|
+
// ── helpers: build a fake mini-package on disk ───────────────────────────────
|
|
14
|
+
|
|
15
|
+
function makeFakePkg(layout) {
|
|
16
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-sbom-'));
|
|
17
|
+
for (const [rel, content] of Object.entries(layout)) {
|
|
18
|
+
const abs = path.join(dir, rel);
|
|
19
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
20
|
+
fs.writeFileSync(abs, typeof content === 'string' ? content : JSON.stringify(content, null, 2));
|
|
21
|
+
}
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cleanup(dir) {
|
|
26
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── SBOM_FORMAT / SBOM_SPEC_VERSION ──────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
test('SBOM constants exposed', () => {
|
|
32
|
+
assert.equal(SBOM.SBOM_FORMAT, 'CycloneDX');
|
|
33
|
+
assert.equal(SBOM.SBOM_SPEC_VERSION, '1.5');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── generateSBOM ─────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
test('generateSBOM: rejects missing rootDir', () => {
|
|
39
|
+
assert.throws(() => generateSBOM({}), /rootDir is required/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('generateSBOM: with no lockfile produces a valid root-only BOM', () => {
|
|
43
|
+
const dir = makeFakePkg({
|
|
44
|
+
'package.json': { name: 'fake-pkg', version: '0.1.0', license: 'MIT' },
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const bom = generateSBOM({ rootDir: dir, timestamp: '2026-05-10T17:00:00Z' });
|
|
48
|
+
assert.equal(bom.bomFormat, 'CycloneDX');
|
|
49
|
+
assert.equal(bom.specVersion, '1.5');
|
|
50
|
+
assert.equal(bom.metadata.component.name, 'fake-pkg');
|
|
51
|
+
assert.equal(bom.metadata.component.version, '0.1.0');
|
|
52
|
+
assert.equal(bom.metadata.component.purl, 'pkg:npm/fake-pkg@0.1.0');
|
|
53
|
+
assert.deepEqual(bom.components, []);
|
|
54
|
+
assert.deepEqual(bom.dependencies, []);
|
|
55
|
+
assert.equal(bom.metadata.timestamp, '2026-05-10T17:00:00Z');
|
|
56
|
+
assert.match(bom.serialNumber, /^urn:uuid:[0-9a-f-]+$/);
|
|
57
|
+
} finally { cleanup(dir); }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('generateSBOM: with lockfile emits library components for each package', () => {
|
|
61
|
+
const dir = makeFakePkg({
|
|
62
|
+
'package.json': { name: 'fake-pkg', version: '0.1.0', dependencies: { foo: '^1.0.0' } },
|
|
63
|
+
'package-lock.json': {
|
|
64
|
+
name: 'fake-pkg',
|
|
65
|
+
version: '0.1.0',
|
|
66
|
+
lockfileVersion: 3,
|
|
67
|
+
packages: {
|
|
68
|
+
'': {
|
|
69
|
+
name: 'fake-pkg',
|
|
70
|
+
version: '0.1.0',
|
|
71
|
+
dependencies: { foo: '^1.0.0' },
|
|
72
|
+
},
|
|
73
|
+
'node_modules/foo': {
|
|
74
|
+
version: '1.2.3',
|
|
75
|
+
license: 'MIT',
|
|
76
|
+
integrity: 'sha512-' + Buffer.from('a'.repeat(64), 'hex').toString('base64'),
|
|
77
|
+
},
|
|
78
|
+
'node_modules/foo/node_modules/@scope/bar': {
|
|
79
|
+
version: '2.0.0',
|
|
80
|
+
license: 'Apache-2.0',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
const bom = generateSBOM({ rootDir: dir });
|
|
87
|
+
assert.equal(bom.components.length, 2);
|
|
88
|
+
const foo = bom.components.find(c => c.name === 'foo');
|
|
89
|
+
assert.ok(foo);
|
|
90
|
+
assert.equal(foo.version, '1.2.3');
|
|
91
|
+
assert.equal(foo.purl, 'pkg:npm/foo@1.2.3');
|
|
92
|
+
assert.equal(foo.scope, 'required');
|
|
93
|
+
assert.equal(foo.licenses[0].license.id, 'MIT');
|
|
94
|
+
assert.equal(foo.hashes[0].alg, 'SHA-512');
|
|
95
|
+
|
|
96
|
+
const scoped = bom.components.find(c => c.name === '@scope/bar');
|
|
97
|
+
assert.ok(scoped);
|
|
98
|
+
assert.equal(scoped.purl, 'pkg:npm/@scope/bar@2.0.0');
|
|
99
|
+
|
|
100
|
+
// Direct deps wired in dependencies graph.
|
|
101
|
+
assert.equal(bom.dependencies.length, 1);
|
|
102
|
+
assert.equal(bom.dependencies[0].ref, 'fake-pkg@0.1.0');
|
|
103
|
+
assert.deepEqual(bom.dependencies[0].dependsOn, ['foo@1.2.3']);
|
|
104
|
+
} finally { cleanup(dir); }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('generateSBOM: dev dependencies marked as scope=optional', () => {
|
|
108
|
+
const dir = makeFakePkg({
|
|
109
|
+
'package.json': { name: 'fake-pkg', version: '0.1.0' },
|
|
110
|
+
'package-lock.json': {
|
|
111
|
+
lockfileVersion: 3,
|
|
112
|
+
packages: {
|
|
113
|
+
'': { name: 'fake-pkg', version: '0.1.0' },
|
|
114
|
+
'node_modules/dev-only': { version: '1.0.0', dev: true },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
const bom = generateSBOM({ rootDir: dir });
|
|
120
|
+
const c = bom.components.find(x => x.name === 'dev-only');
|
|
121
|
+
assert.equal(c.scope, 'optional');
|
|
122
|
+
} finally { cleanup(dir); }
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('generateSBOM: real repo lockfile produces parseable BOM', () => {
|
|
126
|
+
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
127
|
+
const bom = generateSBOM({ rootDir: repoRoot });
|
|
128
|
+
assert.equal(bom.metadata.component.name, 'kodelyth-ecc');
|
|
129
|
+
assert.ok(bom.components.length > 0, 'should have at least one component from real lockfile');
|
|
130
|
+
// Every component must have a purl.
|
|
131
|
+
for (const c of bom.components.slice(0, 10)) {
|
|
132
|
+
assert.match(c.purl, /^pkg:npm\//);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
test('buildComponentFromLockEntry: skips entries without name+version', () => {
|
|
139
|
+
assert.equal(buildComponentFromLockEntry('node_modules/x', {}), null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('_internals.npmPurl: scoped packages keep slash', () => {
|
|
143
|
+
assert.equal(_internals.npmPurl('@scope/x', '1.0.0'), 'pkg:npm/@scope/x@1.0.0');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('_internals.parseIntegrity: handles sha512 base64', () => {
|
|
147
|
+
const sri = 'sha512-' + Buffer.from('a'.repeat(64), 'hex').toString('base64');
|
|
148
|
+
const out = _internals.parseIntegrity(sri);
|
|
149
|
+
assert.equal(out[0].alg, 'SHA-512');
|
|
150
|
+
assert.match(out[0].content, /^[0-9a-f]+$/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('_internals.parseIntegrity: skips unknown algorithms', () => {
|
|
154
|
+
const out = _internals.parseIntegrity('md5-AAAA');
|
|
155
|
+
assert.deepEqual(out, []);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('_internals.deriveSerial: stable for same inputs', () => {
|
|
159
|
+
const a = _internals.deriveSerial({ name: 'x', version: '1.0.0' }, '2026-05-10T00:00:00Z');
|
|
160
|
+
const b = _internals.deriveSerial({ name: 'x', version: '1.0.0' }, '2026-05-10T00:00:00Z');
|
|
161
|
+
assert.equal(a, b);
|
|
162
|
+
assert.match(a, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('_internals.pkgNameFromPath: deepest scoped path wins', () => {
|
|
166
|
+
assert.equal(
|
|
167
|
+
_internals.pkgNameFromPath('node_modules/foo/node_modules/@scope/bar'),
|
|
168
|
+
'@scope/bar'
|
|
169
|
+
);
|
|
170
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Tests for scripts/supply-chain/verify.js
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
const M = require('../../scripts/supply-chain/manifest.js');
|
|
11
|
+
const V = require('../../scripts/supply-chain/verify.js');
|
|
12
|
+
|
|
13
|
+
function makeFakeRepo(layout) {
|
|
14
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-verify-'));
|
|
15
|
+
for (const [rel, content] of Object.entries(layout)) {
|
|
16
|
+
const abs = path.join(dir, rel);
|
|
17
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
18
|
+
fs.writeFileSync(abs, typeof content === 'string' ? content : JSON.stringify(content));
|
|
19
|
+
}
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
function cleanup(dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ } }
|
|
23
|
+
|
|
24
|
+
const FAKE_LAYOUT = {
|
|
25
|
+
'package.json': { name: 'fake', version: '0.1.0' },
|
|
26
|
+
'agents/a.md': 'agent a v1',
|
|
27
|
+
'skills/s/SKILL.md': '# s',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ── argument validation ──────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
test('verifyAgainstManifest: rejects missing rootDir', () => {
|
|
33
|
+
assert.throws(() => V.verifyAgainstManifest({ manifest: { schema: M.MANIFEST_SCHEMA, files: [] } }),
|
|
34
|
+
/rootDir is required/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('verifyAgainstManifest: rejects missing manifest', () => {
|
|
38
|
+
assert.throws(() => V.verifyAgainstManifest({ rootDir: '/tmp' }), /manifest is required/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('verifyAgainstManifest: rejects unsupported manifest schema', () => {
|
|
42
|
+
assert.throws(
|
|
43
|
+
() => V.verifyAgainstManifest({ rootDir: '/tmp', manifest: { schema: 'wrong', files: [] } }),
|
|
44
|
+
/unsupported manifest schema/
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── happy path ───────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
test('verifyAgainstManifest: clean repo returns ok=true with all files', () => {
|
|
51
|
+
const dir = makeFakeRepo(FAKE_LAYOUT);
|
|
52
|
+
try {
|
|
53
|
+
const manifest = M.generateManifest({ rootDir: dir });
|
|
54
|
+
const r = V.verifyAgainstManifest({ rootDir: dir, manifest });
|
|
55
|
+
assert.equal(r.ok, true);
|
|
56
|
+
assert.equal(r.summary.modified, 0);
|
|
57
|
+
assert.equal(r.summary.missing, 0);
|
|
58
|
+
assert.equal(r.summary.extra, 0);
|
|
59
|
+
assert.equal(r.summary.ok, manifest.file_count);
|
|
60
|
+
} finally { cleanup(dir); }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── modified file ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
test('verifyAgainstManifest: detects modified file with sha mismatch', () => {
|
|
66
|
+
const dir = makeFakeRepo(FAKE_LAYOUT);
|
|
67
|
+
try {
|
|
68
|
+
const manifest = M.generateManifest({ rootDir: dir });
|
|
69
|
+
fs.writeFileSync(path.join(dir, 'agents/a.md'), 'agent a v2');
|
|
70
|
+
const r = V.verifyAgainstManifest({ rootDir: dir, manifest });
|
|
71
|
+
assert.equal(r.ok, false);
|
|
72
|
+
assert.equal(r.summary.modified, 1);
|
|
73
|
+
assert.equal(r.details.modified[0].path, 'agents/a.md');
|
|
74
|
+
assert.match(r.details.modified[0].expected_sha256, /^[0-9a-f]{64}$/);
|
|
75
|
+
assert.match(r.details.modified[0].actual_sha256, /^[0-9a-f]{64}$/);
|
|
76
|
+
assert.notEqual(
|
|
77
|
+
r.details.modified[0].expected_sha256,
|
|
78
|
+
r.details.modified[0].actual_sha256
|
|
79
|
+
);
|
|
80
|
+
} finally { cleanup(dir); }
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── missing file ─────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
test('verifyAgainstManifest: detects missing file', () => {
|
|
86
|
+
const dir = makeFakeRepo(FAKE_LAYOUT);
|
|
87
|
+
try {
|
|
88
|
+
const manifest = M.generateManifest({ rootDir: dir });
|
|
89
|
+
fs.unlinkSync(path.join(dir, 'agents/a.md'));
|
|
90
|
+
const r = V.verifyAgainstManifest({ rootDir: dir, manifest });
|
|
91
|
+
assert.equal(r.ok, false);
|
|
92
|
+
assert.equal(r.summary.missing, 1);
|
|
93
|
+
assert.equal(r.details.missing[0].path, 'agents/a.md');
|
|
94
|
+
} finally { cleanup(dir); }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── extra file ───────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
test('verifyAgainstManifest: extra files reported but do not fail verify', () => {
|
|
100
|
+
const dir = makeFakeRepo(FAKE_LAYOUT);
|
|
101
|
+
try {
|
|
102
|
+
const manifest = M.generateManifest({ rootDir: dir });
|
|
103
|
+
fs.writeFileSync(path.join(dir, 'agents/extra.md'), 'unexpected');
|
|
104
|
+
const r = V.verifyAgainstManifest({ rootDir: dir, manifest });
|
|
105
|
+
assert.equal(r.ok, true, 'extras alone should not fail verify');
|
|
106
|
+
assert.equal(r.summary.extra, 1);
|
|
107
|
+
assert.ok(r.details.extra.includes('agents/extra.md'));
|
|
108
|
+
} finally { cleanup(dir); }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── combined ─────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
test('verifyAgainstManifest: counts modified, missing, extra independently', () => {
|
|
114
|
+
const dir = makeFakeRepo(FAKE_LAYOUT);
|
|
115
|
+
try {
|
|
116
|
+
const manifest = M.generateManifest({ rootDir: dir });
|
|
117
|
+
fs.writeFileSync(path.join(dir, 'agents/a.md'), 'tampered');
|
|
118
|
+
fs.unlinkSync(path.join(dir, 'skills/s/SKILL.md'));
|
|
119
|
+
fs.writeFileSync(path.join(dir, 'agents/new.md'), 'fresh');
|
|
120
|
+
const r = V.verifyAgainstManifest({ rootDir: dir, manifest });
|
|
121
|
+
assert.equal(r.ok, false);
|
|
122
|
+
assert.equal(r.summary.modified, 1);
|
|
123
|
+
assert.equal(r.summary.missing, 1);
|
|
124
|
+
assert.equal(r.summary.extra, 1);
|
|
125
|
+
} finally { cleanup(dir); }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── round-trip integration ───────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
test('verifyAgainstManifest: round-trips clean across copy', () => {
|
|
131
|
+
const dir1 = makeFakeRepo(FAKE_LAYOUT);
|
|
132
|
+
const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-verify-2-'));
|
|
133
|
+
try {
|
|
134
|
+
// Copy files to dir2.
|
|
135
|
+
for (const f of M.walkFiles(dir1)) {
|
|
136
|
+
const rel = path.relative(dir1, f);
|
|
137
|
+
const dest = path.join(dir2, rel);
|
|
138
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
139
|
+
fs.copyFileSync(f, dest);
|
|
140
|
+
}
|
|
141
|
+
const manifest = M.generateManifest({ rootDir: dir1 });
|
|
142
|
+
const r = V.verifyAgainstManifest({ rootDir: dir2, manifest });
|
|
143
|
+
assert.equal(r.ok, true);
|
|
144
|
+
assert.equal(r.summary.ok, manifest.file_count);
|
|
145
|
+
} finally { cleanup(dir1); cleanup(dir2); }
|
|
146
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Tests for scripts/swarm/build-plan.js — pure builder, no tmux/git side effects.
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const B = require('../../scripts/swarm/build-plan');
|
|
9
|
+
const O = require('../../scripts/lib/tmux-worktree-orchestrator.js');
|
|
10
|
+
|
|
11
|
+
// ── pickAgents ───────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
test('pickAgents: explicit list wins over count + signals', () => {
|
|
14
|
+
const picked = B.pickAgents({
|
|
15
|
+
count: 4,
|
|
16
|
+
task: 'security audit oauth flow',
|
|
17
|
+
explicit: ['code-architect', 'release-captain'],
|
|
18
|
+
});
|
|
19
|
+
assert.deepEqual(picked, ['code-architect', 'release-captain']);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('pickAgents: signal-driven for security task', () => {
|
|
23
|
+
const picked = B.pickAgents({ count: 4, task: 'security audit oauth flow for OWASP top 10' });
|
|
24
|
+
assert.ok(picked.includes('security-reviewer'), `expected security-reviewer in ${picked}`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('pickAgents: signal-driven for performance task', () => {
|
|
28
|
+
const picked = B.pickAgents({ count: 4, task: 'investigate p99 latency spike on the payments API' });
|
|
29
|
+
assert.ok(picked.includes('performance-optimizer'));
|
|
30
|
+
assert.ok(picked.includes('api-guardian'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('pickAgents: signal-driven for incident task picks incident-commander + debug-detective', () => {
|
|
34
|
+
const picked = B.pickAgents({ count: 4, task: 'production is down, P0 incident, why is auth service failing' });
|
|
35
|
+
assert.ok(picked.includes('incident-commander'));
|
|
36
|
+
assert.ok(picked.includes('debug-detective'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('pickAgents: includes baseline anchors when room remains', () => {
|
|
40
|
+
const picked = B.pickAgents({ count: 4, task: 'investigate p99 latency on payments API' });
|
|
41
|
+
// After signals (perf, api), should fill with code-reviewer + pair-programmer.
|
|
42
|
+
assert.ok(picked.includes('code-reviewer') || picked.includes('pair-programmer'));
|
|
43
|
+
assert.equal(picked.length, 4);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('pickAgents: count clamped to upper bound 12 even with many signals', () => {
|
|
47
|
+
// A task with many signals so the picker has plenty of candidates.
|
|
48
|
+
const richTask = 'security audit with perf load-test architecture API tests refactor docs release incident debug database';
|
|
49
|
+
const picked = B.pickAgents({ count: 99, task: richTask });
|
|
50
|
+
assert.ok(picked.length <= 12, `expected ≤12, got ${picked.length}`);
|
|
51
|
+
assert.ok(picked.length > 8, `expected >8 with rich signals, got ${picked.length}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('pickAgents: count clamps to available pool when signals empty', () => {
|
|
55
|
+
// No matching signals + no signal agent rotation past 8 = bounded by pool size.
|
|
56
|
+
const picked = B.pickAgents({ count: 99, task: 'any' });
|
|
57
|
+
assert.ok(picked.length >= 8 && picked.length <= 12);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('pickAgents: count of 1 produces 1 agent', () => {
|
|
61
|
+
assert.equal(B.pickAgents({ count: 1, task: 'any' }).length, 1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('pickAgents: falsy count falls back to default of 4', () => {
|
|
65
|
+
assert.equal(B.pickAgents({ count: 0, task: 'any' }).length, 4);
|
|
66
|
+
assert.equal(B.pickAgents({ count: undefined, task: 'any' }).length, 4);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('pickAgents: deduplicates if signal + anchor + rotation overlap', () => {
|
|
70
|
+
const picked = B.pickAgents({ count: 6, task: 'do a code review of payment refactor with security implications' });
|
|
71
|
+
assert.equal(new Set(picked).size, picked.length, 'no duplicate agents');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── shapeTaskForAgent ────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
test('shapeTaskForAgent: includes agent name + required sections', () => {
|
|
77
|
+
const out = B.shapeTaskForAgent('security-reviewer', 'audit oauth flow');
|
|
78
|
+
assert.match(out, /# security-reviewer — swarm task/);
|
|
79
|
+
assert.match(out, /audit oauth flow/);
|
|
80
|
+
assert.match(out, /## Required handoff sections/);
|
|
81
|
+
assert.match(out, /1\. Summary/);
|
|
82
|
+
assert.match(out, /2\. Files Changed/);
|
|
83
|
+
assert.match(out, /3\. Validation/);
|
|
84
|
+
assert.match(out, /4\. Remaining Risks/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('shapeTaskForAgent: handles empty task gracefully', () => {
|
|
88
|
+
const out = B.shapeTaskForAgent('code-reviewer', '');
|
|
89
|
+
assert.match(out, /no task description provided/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── buildWorker ──────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
test('buildWorker: claude harness produces a launcher with task placeholder', () => {
|
|
95
|
+
const w = B.buildWorker({ agent: 'security-reviewer', task: 'foo', harness: 'claude' });
|
|
96
|
+
assert.equal(w.name, 'security-reviewer');
|
|
97
|
+
assert.match(w.launcherCommand, /\{task_file_sh\}/);
|
|
98
|
+
assert.match(w.launcherCommand, /claude/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('buildWorker: codex harness uses orchestrate-codex-worker.sh', () => {
|
|
102
|
+
const w = B.buildWorker({ agent: 'security-reviewer', task: 'foo', harness: 'codex' });
|
|
103
|
+
assert.match(w.launcherCommand, /orchestrate-codex-worker\.sh/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('buildWorker: unknown harness throws helpful error', () => {
|
|
107
|
+
assert.throws(
|
|
108
|
+
() => B.buildWorker({ agent: 'x', task: 'y', harness: 'bogus' }),
|
|
109
|
+
/No launcher template for harness "bogus"/
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('buildWorker: custom launcherCommand overrides harness', () => {
|
|
114
|
+
const w = B.buildWorker({ agent: 'x', task: 'y', harness: 'claude', launcherCommand: 'my-custom {task_file}' });
|
|
115
|
+
assert.equal(w.launcherCommand, 'my-custom {task_file}');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── buildSwarmPlan (full plan-config) ────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
test('buildSwarmPlan: rejects empty task', () => {
|
|
121
|
+
assert.throws(() => B.buildSwarmPlan({ task: '' }), /--task is required/);
|
|
122
|
+
assert.throws(() => B.buildSwarmPlan({}), /--task is required/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('buildSwarmPlan: produces a plan-config the orchestrator can consume', () => {
|
|
126
|
+
const planConfig = B.buildSwarmPlan({
|
|
127
|
+
task: 'audit oauth flow for security',
|
|
128
|
+
count: 4,
|
|
129
|
+
harness: 'echo',
|
|
130
|
+
repoRoot: process.cwd(),
|
|
131
|
+
});
|
|
132
|
+
assert.ok(planConfig.workers.length === 4);
|
|
133
|
+
assert.ok(planConfig.sessionName.startsWith('swarm-'));
|
|
134
|
+
assert.ok(planConfig._meta.pickedAgents.includes('security-reviewer'));
|
|
135
|
+
|
|
136
|
+
// Verify orchestrator accepts it without throwing.
|
|
137
|
+
const plan = O.buildOrchestrationPlan(planConfig);
|
|
138
|
+
assert.equal(plan.workerPlans.length, 4);
|
|
139
|
+
assert.ok(plan.workerPlans.every(w => w.launchCommand.includes('demo')));
|
|
140
|
+
assert.ok(plan.workerPlans.every(w => w.taskFilePath.includes('.orchestration')));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('buildSwarmPlan: explicit agents preserved in worker order', () => {
|
|
144
|
+
const planConfig = B.buildSwarmPlan({
|
|
145
|
+
task: 'ship v2.0',
|
|
146
|
+
agents: ['release-captain', 'security-reviewer', 'e2e-runner'],
|
|
147
|
+
harness: 'echo',
|
|
148
|
+
});
|
|
149
|
+
assert.deepEqual(
|
|
150
|
+
planConfig.workers.map(w => w.name),
|
|
151
|
+
['release-captain', 'security-reviewer', 'e2e-runner']
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('buildSwarmPlan: passes through orchestrator-only fields', () => {
|
|
156
|
+
const planConfig = B.buildSwarmPlan({
|
|
157
|
+
task: 'test',
|
|
158
|
+
count: 2,
|
|
159
|
+
harness: 'echo',
|
|
160
|
+
sessionName: 'my-custom-session',
|
|
161
|
+
baseRef: 'main',
|
|
162
|
+
replaceExisting: true,
|
|
163
|
+
seedPaths: ['scripts'],
|
|
164
|
+
coordinationRoot: '/tmp/coord',
|
|
165
|
+
worktreeRoot: '/tmp/worktrees',
|
|
166
|
+
});
|
|
167
|
+
assert.equal(planConfig.sessionName, 'my-custom-session');
|
|
168
|
+
assert.equal(planConfig.baseRef, 'main');
|
|
169
|
+
assert.equal(planConfig.replaceExisting, true);
|
|
170
|
+
assert.deepEqual(planConfig.seedPaths, ['scripts']);
|
|
171
|
+
assert.equal(planConfig.coordinationRoot, '/tmp/coord');
|
|
172
|
+
assert.equal(planConfig.worktreeRoot, '/tmp/worktrees');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('buildSwarmPlan + orchestrator: worker tasks contain agent-shaped prompt', () => {
|
|
176
|
+
const planConfig = B.buildSwarmPlan({
|
|
177
|
+
task: 'audit oauth flow',
|
|
178
|
+
count: 2,
|
|
179
|
+
harness: 'echo',
|
|
180
|
+
agents: ['security-reviewer', 'code-reviewer'],
|
|
181
|
+
});
|
|
182
|
+
const plan = O.buildOrchestrationPlan(planConfig);
|
|
183
|
+
for (const w of plan.workerPlans) {
|
|
184
|
+
assert.match(w.task, /Required handoff sections/);
|
|
185
|
+
assert.match(w.task, /audit oauth flow/);
|
|
186
|
+
assert.match(w.task, new RegExp(w.workerName));
|
|
187
|
+
}
|
|
188
|
+
});
|
package/wiki/Agent-Reference.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Reference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
70 specialist subagents — all auto-routed via semantic intent routing. You never need to type agent names unless you want to.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -149,6 +149,7 @@ These 16 agents exist only in ECC — not in any other toolkit.
|
|
|
149
149
|
| `doc-updater` | Update README, write JSDoc, docstrings, codemaps |
|
|
150
150
|
| `docs-lookup` | Library / framework API questions (uses Context7) |
|
|
151
151
|
| `comment-analyzer` | Comment quality — explain why, not what |
|
|
152
|
+
| `code-explorer` | Deep codebase analysis — trace execution paths, map dependencies |
|
|
152
153
|
|
|
153
154
|
---
|
|
154
155
|
|
|
@@ -173,12 +174,26 @@ Use in sequence: `opensource-forker` → `opensource-sanitizer` → `opensource-
|
|
|
173
174
|
|
|
174
175
|
---
|
|
175
176
|
|
|
176
|
-
##
|
|
177
|
+
## Adversarial / Devil-Mode Agents (v1.7.0)
|
|
177
178
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
|
181
|
-
|
|
179
|
+
8 dedicated red-team agents that find vulnerabilities and weaknesses through adversarial analysis.
|
|
180
|
+
|
|
181
|
+
| Agent | What it does | When to use |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `prompt-injection-hunter` | Finds AI/LLM injection vulnerabilities, jailbreak attempts | "Find prompt injection vectors in my AI code" |
|
|
184
|
+
| `supply-chain-auditor` | Audits dependency tree, build artifacts, artifact provenance | "Audit our supply chain security" |
|
|
185
|
+
| `secret-hunter` | Hunts hardcoded API keys, tokens, credentials in code | "Find any secrets that might be exposed" |
|
|
186
|
+
| `license-violation-finder` | Detects GPL, license compliance issues, forbidden deps | "Check for license violations" |
|
|
187
|
+
| `jailbreak-tester` | Tests safety bypass techniques, red-team AI behavior | "Test if my safety guardrails can be bypassed" |
|
|
188
|
+
| `code-stealer-detector` | Finds data exfiltration vectors, unauthorized access paths | "Find ways attackers could steal our code" |
|
|
189
|
+
| `backdoor-hunter` | Identifies inserted vulnerabilities, supply chain attacks | "Scan for backdoors or intentional vulnerabilities" |
|
|
190
|
+
| `chaos-engineer` | Failure mode analysis, edge case testing, resilience | "What happens if X fails? Test all failure modes" |
|
|
191
|
+
|
|
192
|
+
**Invoke together:**
|
|
193
|
+
```bash
|
|
194
|
+
/devil-mode # Top 4 agents (injection, supply chain, secrets, license)
|
|
195
|
+
/devil-mode --all # All 8 agents (adds jailbreak, code theft, backdoors, chaos)
|
|
196
|
+
```
|
|
182
197
|
|
|
183
198
|
---
|
|
184
199
|
|
|
@@ -188,5 +203,41 @@ Use in sequence: `opensource-forker` → `opensource-sanitizer` → `opensource-
|
|
|
188
203
|
|---|---|
|
|
189
204
|
| `seo-specialist` | SEO, meta tags, schema.org, Core Web Vitals, sitemap |
|
|
190
205
|
| `ux-reviewer` | UX behavior, WCAG 2.1 AA, keyboard nav, form states |
|
|
191
|
-
| `code-explorer` | Deep codebase analysis — trace execution paths, map dependencies |
|
|
192
206
|
| `image-architect` | AI image gen — hero, OG cards, social, thumbnails |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Phase 2 Infrastructure Agents (v1.7.0)
|
|
211
|
+
|
|
212
|
+
Newer specialist agents for advanced orchestration and observability.
|
|
213
|
+
|
|
214
|
+
| Agent | Purpose |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `swarm-orchestrator` | Manage parallel agent execution and work distribution |
|
|
217
|
+
| `cost-aware-router` | Route tasks to the right model tier (Haiku/Sonnet/Opus) |
|
|
218
|
+
| `memory-evolve` | Self-improving memory — analyze patterns, surface insights |
|
|
219
|
+
| `observability-dashboard` | System metrics, agent performance, session replay |
|
|
220
|
+
| `supply-chain-analyst` | Deep SBOM analysis, vulnerability tracking |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Parallel Commands (multi-agent)
|
|
225
|
+
|
|
226
|
+
| Command | Agents fired simultaneously |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `/project-launch` | architect + pair-programmer + security-reviewer + tdd-guide + ux-reviewer |
|
|
229
|
+
| `/team-review` | code-reviewer + security-reviewer + performance-optimizer + api-guardian |
|
|
230
|
+
| `/security-audit` | security-reviewer + dependency-doctor + api-guardian |
|
|
231
|
+
| `/debug-blitz` | debug-detective + silent-failure-hunter + env-debugger |
|
|
232
|
+
| `/refactor-sprint` | refactor-cleaner + code-simplifier + type-design-analyzer + tdd-guide |
|
|
233
|
+
| `/pre-release` | release-captain + security-reviewer + code-reviewer |
|
|
234
|
+
| `/onboard` | code-explorer + architect + doc-updater |
|
|
235
|
+
| `/devil-mode` | prompt-injection-hunter + supply-chain-auditor + secret-hunter + license-violation-finder (4) or all 8 with `--all` |
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Agent Count Summary
|
|
240
|
+
|
|
241
|
+
- **Standard Agents:** 62
|
|
242
|
+
- **Adversarial Devil-Mode Agents:** 8
|
|
243
|
+
- **Total Agents:** 70
|