kodelyth-ecc 1.5.10 → 1.7.1

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.
Files changed (273) hide show
  1. package/.github/workflows/publish.yml +19 -0
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +475 -0
  4. package/CLAUDE.md +48 -16
  5. package/README.md +285 -81
  6. package/VERSION +1 -1
  7. package/actions/ecc-review/README.md +243 -0
  8. package/actions/ecc-review/action.yml +158 -0
  9. package/actions/ecc-review/post-comment.js +171 -0
  10. package/actions/ecc-review/run-review.js +285 -0
  11. package/agents/backdoor-hunter.md +260 -0
  12. package/agents/chaos-engineer.md +251 -0
  13. package/agents/code-stealer-detector.md +228 -0
  14. package/agents/jailbreak-tester.md +222 -0
  15. package/agents/license-violation-finder.md +212 -0
  16. package/agents/prompt-injection-hunter.md +126 -0
  17. package/agents/secret-hunter.md +209 -0
  18. package/agents/supply-chain-auditor.md +195 -0
  19. package/bin/kodelyth-ecc.js +875 -1
  20. package/bundles/enterprise.md +172 -0
  21. package/bundles/indie-hacker.md +106 -0
  22. package/bundles/red-team.md +138 -0
  23. package/cat +0 -0
  24. package/commands/dashboard.md +67 -0
  25. package/commands/devil-mode.md +121 -0
  26. package/commands/memory-evolve.md +71 -0
  27. package/commands/replay.md +61 -0
  28. package/commands/route-model.md +48 -0
  29. package/commands/swarm.md +68 -0
  30. package/commands/verify-supply-chain.md +59 -0
  31. package/docs/dashboard.md +211 -0
  32. package/docs/evolve.md +303 -0
  33. package/docs/mcp-clients.md +167 -0
  34. package/docs/mcp.md +178 -0
  35. package/docs/replay.md +244 -0
  36. package/docs/supply-chain.md +207 -0
  37. package/docs/swarm.md +243 -0
  38. package/hooks/hooks.json +52 -0
  39. package/hooks/memory/auto-recall.js +29 -1
  40. package/hooks/safety/README.md +124 -0
  41. package/hooks/safety/lib/patterns.js +179 -0
  42. package/hooks/safety/prompt-injection-guard.js +179 -0
  43. package/hooks/safety/token-budget.js +229 -0
  44. package/install.ps1 +199 -2
  45. package/install.sh +285 -3
  46. package/package.json +30 -4
  47. package/rules/common/agent-intent-routing.md +182 -0
  48. package/rules/common/cost-aware-model-routing.md +152 -0
  49. package/scripts/dashboard/data.js +353 -0
  50. package/scripts/dashboard/server.js +324 -0
  51. package/scripts/dashboard/static/index.html +645 -0
  52. package/scripts/evolve/analyze.js +303 -0
  53. package/scripts/evolve/proposals.js +162 -0
  54. package/scripts/evolve/stats.js +219 -0
  55. package/scripts/mcp/catalog.js +244 -0
  56. package/scripts/mcp/client.js +181 -0
  57. package/scripts/mcp/prompts.js +133 -0
  58. package/scripts/mcp/resources.js +94 -0
  59. package/scripts/mcp/server.js +153 -0
  60. package/scripts/mcp/tools.js +465 -0
  61. package/scripts/replay/bundle.js +191 -0
  62. package/scripts/replay/replay.js +107 -0
  63. package/scripts/router/classify.js +232 -0
  64. package/scripts/supply-chain/manifest.js +155 -0
  65. package/scripts/supply-chain/sbom.js +202 -0
  66. package/scripts/supply-chain/verify.js +102 -0
  67. package/scripts/swarm/build-plan.js +193 -0
  68. package/skills/cost-aware-model-routing/SKILL.md +153 -0
  69. package/skills/kodelyth-quickstart/SKILL.md +7 -0
  70. package/skills/observability-dashboard/SKILL.md +119 -0
  71. package/skills/self-evolving-memory/SKILL.md +175 -0
  72. package/skills/session-replay/SKILL.md +199 -0
  73. package/skills/supply-chain-verification/SKILL.md +201 -0
  74. package/skills/swarm-orchestrator/SKILL.md +177 -0
  75. package/social/card-install.svg +1 -1
  76. package/social/facebook-group/POST.md +121 -0
  77. package/social/facebook-group/fb-1-3am-debug.png +0 -0
  78. package/social/facebook-group/fb-1-3am-debug.svg +97 -0
  79. package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
  80. package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
  81. package/social/facebook-group/fb-3-before-after.png +0 -0
  82. package/social/facebook-group/fb-3-before-after.svg +94 -0
  83. package/social/facebook-v150.svg +6 -6
  84. package/social/github-social-preview.svg +119 -100
  85. package/social/readme-hero.svg +12 -12
  86. package/social/section-agents.svg +57 -0
  87. package/social/section-author.svg +54 -0
  88. package/social/section-dashboard.svg +59 -0
  89. package/social/section-devil.svg +54 -0
  90. package/social/section-hooks.svg +51 -0
  91. package/social/section-install.svg +42 -0
  92. package/social/section-learning.svg +52 -0
  93. package/social/section-mcp.svg +46 -0
  94. package/social/section-memory.svg +57 -0
  95. package/social/section-parallel.svg +72 -0
  96. package/social/section-routing.svg +51 -0
  97. package/social/x-card-agents-grid.svg +6 -6
  98. package/tests/dashboard/data.test.js +235 -0
  99. package/tests/dashboard/server.test.js +240 -0
  100. package/tests/evolve/analyze.test.js +169 -0
  101. package/tests/evolve/proposals.test.js +173 -0
  102. package/tests/evolve/stats.test.js +167 -0
  103. package/tests/mcp/catalog.test.js +98 -0
  104. package/tests/mcp/client.test.js +113 -0
  105. package/tests/mcp/resources-prompts.test.js +70 -0
  106. package/tests/mcp/tools.test.js +159 -0
  107. package/tests/replay/bundle.test.js +181 -0
  108. package/tests/replay/replay.test.js +131 -0
  109. package/tests/router/classify.test.js +164 -0
  110. package/tests/safety/patterns.test.js +94 -0
  111. package/tests/safety/prompt-injection-guard.test.js +111 -0
  112. package/tests/safety/token-budget.test.js +119 -0
  113. package/tests/supply-chain/manifest.test.js +147 -0
  114. package/tests/supply-chain/sbom.test.js +170 -0
  115. package/tests/supply-chain/verify.test.js +146 -0
  116. package/tests/swarm/build-plan.test.js +188 -0
  117. package/wiki/Agent-Reference.md +58 -7
  118. package/wiki/FAQ.md +204 -7
  119. package/wiki/Home.md +104 -29
  120. package/wiki/Hook-Reference.md +1 -1
  121. package/wiki/Installation-Guide.md +109 -6
  122. package/wiki/Platform-Support.md +189 -72
  123. package/wiki/Skill-Reference.md +101 -6
  124. package/.agent/rules/README.md +0 -108
  125. package/.agent/rules/agents.md +0 -94
  126. package/.agent/rules/code-review.md +0 -124
  127. package/.agent/rules/coding-style.md +0 -122
  128. package/.agent/rules/design-quality.md +0 -63
  129. package/.agent/rules/development-workflow.md +0 -44
  130. package/.agent/rules/git-mastery.md +0 -366
  131. package/.agent/rules/git-workflow.md +0 -24
  132. package/.agent/rules/hooks.md +0 -25
  133. package/.agent/rules/kodelyth-always-on.md +0 -63
  134. package/.agent/rules/kodelyth-quickstart.md +0 -207
  135. package/.agent/rules/observability.md +0 -390
  136. package/.agent/rules/patterns.md +0 -50
  137. package/.agent/rules/performance.md +0 -178
  138. package/.agent/rules/security.md +0 -58
  139. package/.agent/rules/smart-debug.md +0 -241
  140. package/.agent/rules/soul.md +0 -32
  141. package/.agent/rules/testing.md +0 -46
  142. package/.agent/skills/api-guardian.md +0 -279
  143. package/.agent/skills/architect.md +0 -211
  144. package/.agent/skills/build-error-resolver.md +0 -114
  145. package/.agent/skills/chief-of-staff.md +0 -151
  146. package/.agent/skills/code-architect.md +0 -71
  147. package/.agent/skills/code-explorer.md +0 -69
  148. package/.agent/skills/code-reviewer.md +0 -237
  149. package/.agent/skills/code-simplifier.md +0 -47
  150. package/.agent/skills/comment-analyzer.md +0 -45
  151. package/.agent/skills/conversation-analyzer.md +0 -52
  152. package/.agent/skills/cpp-build-resolver.md +0 -90
  153. package/.agent/skills/cpp-reviewer.md +0 -72
  154. package/.agent/skills/csharp-reviewer.md +0 -101
  155. package/.agent/skills/dart-build-resolver.md +0 -201
  156. package/.agent/skills/database-reviewer.md +0 -91
  157. package/.agent/skills/debug-detective.md +0 -409
  158. package/.agent/skills/doc-updater.md +0 -107
  159. package/.agent/skills/docs-lookup.md +0 -68
  160. package/.agent/skills/e2e-runner.md +0 -107
  161. package/.agent/skills/flutter-reviewer.md +0 -243
  162. package/.agent/skills/gan-evaluator.md +0 -209
  163. package/.agent/skills/gan-generator.md +0 -131
  164. package/.agent/skills/gan-planner.md +0 -99
  165. package/.agent/skills/go-build-resolver.md +0 -94
  166. package/.agent/skills/go-reviewer.md +0 -76
  167. package/.agent/skills/harness-optimizer.md +0 -35
  168. package/.agent/skills/healthcare-reviewer.md +0 -83
  169. package/.agent/skills/java-build-resolver.md +0 -153
  170. package/.agent/skills/java-reviewer.md +0 -92
  171. package/.agent/skills/kodelyth-advisor.md +0 -172
  172. package/.agent/skills/kotlin-build-resolver.md +0 -118
  173. package/.agent/skills/kotlin-reviewer.md +0 -159
  174. package/.agent/skills/loop-operator.md +0 -36
  175. package/.agent/skills/migration-guide.md +0 -368
  176. package/.agent/skills/opensource-forker.md +0 -198
  177. package/.agent/skills/opensource-packager.md +0 -249
  178. package/.agent/skills/opensource-sanitizer.md +0 -188
  179. package/.agent/skills/pair-programmer.md +0 -260
  180. package/.agent/skills/performance-optimizer.md +0 -446
  181. package/.agent/skills/planner.md +0 -212
  182. package/.agent/skills/pr-test-analyzer.md +0 -45
  183. package/.agent/skills/python-reviewer.md +0 -98
  184. package/.agent/skills/pytorch-build-resolver.md +0 -120
  185. package/.agent/skills/refactor-cleaner.md +0 -85
  186. package/.agent/skills/rust-build-resolver.md +0 -148
  187. package/.agent/skills/rust-reviewer.md +0 -94
  188. package/.agent/skills/security-reviewer.md +0 -108
  189. package/.agent/skills/seo-specialist.md +0 -62
  190. package/.agent/skills/silent-failure-hunter.md +0 -50
  191. package/.agent/skills/tdd-guide.md +0 -91
  192. package/.agent/skills/type-design-analyzer.md +0 -41
  193. package/.agent/skills/typescript-reviewer.md +0 -112
  194. package/.agent/skills/ux-reviewer.md +0 -483
  195. package/.agent/workflows/agent-sort.md +0 -23
  196. package/.agent/workflows/aside.md +0 -164
  197. package/.agent/workflows/build-fix.md +0 -62
  198. package/.agent/workflows/checkpoint.md +0 -74
  199. package/.agent/workflows/claw.md +0 -23
  200. package/.agent/workflows/code-review.md +0 -289
  201. package/.agent/workflows/context-budget.md +0 -23
  202. package/.agent/workflows/cpp-build.md +0 -173
  203. package/.agent/workflows/cpp-review.md +0 -132
  204. package/.agent/workflows/cpp-test.md +0 -251
  205. package/.agent/workflows/devfleet.md +0 -23
  206. package/.agent/workflows/docs.md +0 -23
  207. package/.agent/workflows/e2e.md +0 -268
  208. package/.agent/workflows/eval.md +0 -23
  209. package/.agent/workflows/evolve.md +0 -178
  210. package/.agent/workflows/feature-dev.md +0 -49
  211. package/.agent/workflows/flutter-build.md +0 -164
  212. package/.agent/workflows/flutter-review.md +0 -116
  213. package/.agent/workflows/flutter-test.md +0 -144
  214. package/.agent/workflows/gan-build.md +0 -99
  215. package/.agent/workflows/gan-design.md +0 -35
  216. package/.agent/workflows/go-build.md +0 -183
  217. package/.agent/workflows/go-review.md +0 -148
  218. package/.agent/workflows/go-test.md +0 -268
  219. package/.agent/workflows/gradle-build.md +0 -70
  220. package/.agent/workflows/harness-audit.md +0 -73
  221. package/.agent/workflows/hookify-configure.md +0 -14
  222. package/.agent/workflows/hookify-help.md +0 -46
  223. package/.agent/workflows/hookify-list.md +0 -21
  224. package/.agent/workflows/hookify.md +0 -50
  225. package/.agent/workflows/instinct-export.md +0 -66
  226. package/.agent/workflows/instinct-import.md +0 -114
  227. package/.agent/workflows/instinct-status.md +0 -59
  228. package/.agent/workflows/jira.md +0 -106
  229. package/.agent/workflows/kotlin-build.md +0 -174
  230. package/.agent/workflows/kotlin-review.md +0 -140
  231. package/.agent/workflows/kotlin-test.md +0 -312
  232. package/.agent/workflows/learn-eval.md +0 -116
  233. package/.agent/workflows/learn.md +0 -70
  234. package/.agent/workflows/loop-start.md +0 -32
  235. package/.agent/workflows/loop-status.md +0 -24
  236. package/.agent/workflows/model-route.md +0 -26
  237. package/.agent/workflows/multi-backend.md +0 -158
  238. package/.agent/workflows/multi-execute.md +0 -315
  239. package/.agent/workflows/multi-frontend.md +0 -158
  240. package/.agent/workflows/multi-plan.md +0 -268
  241. package/.agent/workflows/multi-workflow.md +0 -191
  242. package/.agent/workflows/orchestrate.md +0 -135
  243. package/.agent/workflows/plan.md +0 -117
  244. package/.agent/workflows/pm2.md +0 -272
  245. package/.agent/workflows/projects.md +0 -39
  246. package/.agent/workflows/promote.md +0 -41
  247. package/.agent/workflows/prompt-optimize.md +0 -23
  248. package/.agent/workflows/prp-commit.md +0 -112
  249. package/.agent/workflows/prp-implement.md +0 -385
  250. package/.agent/workflows/prp-plan.md +0 -502
  251. package/.agent/workflows/prp-pr.md +0 -184
  252. package/.agent/workflows/prp-prd.md +0 -447
  253. package/.agent/workflows/prune.md +0 -31
  254. package/.agent/workflows/python-review.md +0 -297
  255. package/.agent/workflows/quality-gate.md +0 -29
  256. package/.agent/workflows/refactor-clean.md +0 -80
  257. package/.agent/workflows/resume-session.md +0 -156
  258. package/.agent/workflows/review-pr.md +0 -37
  259. package/.agent/workflows/rules-distill.md +0 -20
  260. package/.agent/workflows/rust-build.md +0 -187
  261. package/.agent/workflows/rust-review.md +0 -142
  262. package/.agent/workflows/rust-test.md +0 -308
  263. package/.agent/workflows/santa-loop.md +0 -175
  264. package/.agent/workflows/save-session.md +0 -275
  265. package/.agent/workflows/sessions.md +0 -333
  266. package/.agent/workflows/setup-pm.md +0 -80
  267. package/.agent/workflows/skill-create.md +0 -174
  268. package/.agent/workflows/skill-health.md +0 -54
  269. package/.agent/workflows/tdd.md +0 -231
  270. package/.agent/workflows/test-coverage.md +0 -69
  271. package/.agent/workflows/update-codemaps.md +0 -72
  272. package/.agent/workflows/update-docs.md +0 -84
  273. package/.agent/workflows/verify.md +0 -23
@@ -0,0 +1,113 @@
1
+ // Tests for scripts/mcp/client.js — registry + connect helpers.
2
+ // Uses the LOCAL Kodelyth MCP server itself as a real test target so we
3
+ // exercise the full stdio handshake without needing external dependencies.
4
+ 'use strict';
5
+
6
+ const test = require('node:test');
7
+ const assert = require('node:assert/strict');
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+
12
+ // Isolate the registry to a tmp dir so we don't pollute ~/.kodelyth.
13
+ const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-mcp-client-'));
14
+ process.env.KODELYTH_MCP_CLIENT_DIR = TMP;
15
+
16
+ const C = require('../../scripts/mcp/client.js');
17
+
18
+ const ROOT = path.join(__dirname, '..', '..');
19
+ const LOCAL_SERVER = path.join(ROOT, 'scripts', 'mcp', 'server.js');
20
+
21
+ // ── Registry tests (no SDK required) ─────────────────────────────────────────
22
+
23
+ test('registry: starts empty', () => {
24
+ const reg = C.loadRegistry();
25
+ assert.deepEqual(reg.servers, {});
26
+ assert.deepEqual(C.listServers(), []);
27
+ });
28
+
29
+ test('registry: addServer persists with sane defaults', () => {
30
+ const spec = C.addServer({
31
+ name: 'self',
32
+ command: process.execPath,
33
+ args: [LOCAL_SERVER],
34
+ env: { TEST_VAR: '1' },
35
+ description: 'local self-test target',
36
+ });
37
+ assert.equal(spec.name, 'self');
38
+ assert.equal(spec.command, process.execPath);
39
+ assert.deepEqual(spec.args, [LOCAL_SERVER]);
40
+ assert.equal(spec.env.TEST_VAR, '1');
41
+ assert.ok(spec.added_at);
42
+
43
+ // Persisted to disk:
44
+ assert.ok(fs.existsSync(C.REGISTRY_FILE));
45
+ const onDisk = JSON.parse(fs.readFileSync(C.REGISTRY_FILE, 'utf8'));
46
+ assert.equal(onDisk.servers.self.name, 'self');
47
+ });
48
+
49
+ test('registry: rejects bad names', () => {
50
+ assert.throws(() => C.addServer({ name: '', command: 'x' }));
51
+ assert.throws(() => C.addServer({ name: 'has space',command: 'x' }));
52
+ assert.throws(() => C.addServer({ name: 'self', command: '' }));
53
+ });
54
+
55
+ test('registry: getServer / listServers reflect adds + removes', () => {
56
+ C.addServer({ name: 'second', command: '/usr/bin/true', args: [] });
57
+ const list = C.listServers();
58
+ assert.equal(list.length, 2, `expected 2, got ${list.length}`);
59
+ const names = list.map(s => s.name).sort();
60
+ assert.deepEqual(names, ['second', 'self']);
61
+
62
+ assert.ok(C.getServer('self'));
63
+ assert.equal(C.getServer('not-real'), null);
64
+
65
+ assert.equal(C.removeServer('second'), true);
66
+ assert.equal(C.removeServer('second'), false); // already gone
67
+ assert.equal(C.listServers().length, 1);
68
+ });
69
+
70
+ // ── End-to-end: connect to local kodelyth MCP server and call tools ──────────
71
+ // These tests require @modelcontextprotocol/sdk (optionalDependency).
72
+ // Skip gracefully when it isn't installed (e.g. CI without optional deps).
73
+ let hasSdk = false;
74
+ try { require('@modelcontextprotocol/sdk/client/index.js'); hasSdk = true; } catch (_) {}
75
+
76
+ test('client: connects to local kodelyth MCP server and lists tools', { skip: !hasSdk }, async () => {
77
+ // self is already registered from the earlier test (running in same file).
78
+ const out = await C.listTools('self');
79
+ assert.ok(out.tools.length > 0, 'expected at least one tool from local kodelyth server');
80
+ const names = new Set(out.tools.map(t => t.name));
81
+ assert.ok(names.has('catalog_stats'), 'catalog_stats tool must be exposed');
82
+ assert.ok(names.has('list_agents'), 'list_agents tool must be exposed');
83
+ });
84
+
85
+ test('client: callTool on local server returns a real result', { skip: !hasSdk }, async () => {
86
+ const out = await C.callTool('self', 'catalog_stats', {});
87
+ assert.ok(Array.isArray(out.content));
88
+ assert.equal(out.content[0].type, 'text');
89
+ const data = JSON.parse(out.content[0].text);
90
+ assert.ok(data.agents > 0);
91
+ assert.ok(data.skills > 0);
92
+ assert.ok(data.commands > 0);
93
+ });
94
+
95
+ test('client: listResources surfaces kodelyth:// URIs', { skip: !hasSdk }, async () => {
96
+ const out = await C.listResources('self');
97
+ assert.ok(out.resources.length > 0);
98
+ assert.ok(out.resources.every(r => r.uri.startsWith('kodelyth://')),
99
+ 'every resource uri should start with kodelyth://');
100
+ });
101
+
102
+ test('client: listPrompts surfaces routing-rule', { skip: !hasSdk }, async () => {
103
+ const out = await C.listPrompts('self');
104
+ const names = new Set(out.prompts.map(p => p.name));
105
+ assert.ok(names.has('routing-rule'));
106
+ });
107
+
108
+ test('client: connect fails clearly for unregistered server', async () => {
109
+ await assert.rejects(
110
+ () => C.connect('this-was-never-registered'),
111
+ err => err.code === 'MCP_SERVER_NOT_REGISTERED'
112
+ );
113
+ });
@@ -0,0 +1,70 @@
1
+ // Tests for scripts/mcp/resources.js and scripts/mcp/prompts.js
2
+ 'use strict';
3
+
4
+ const test = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+
7
+ const RES = require('../../scripts/mcp/resources');
8
+ const PROMPTS = require('../../scripts/mcp/prompts');
9
+
10
+ test('resources.buildList returns agents/skills/commands/rules/bundles', () => {
11
+ const list = RES.buildList();
12
+ assert.ok(list.length > 200, `expected >200 resources, got ${list.length}`);
13
+ const schemes = new Set(list.map(r => r.uri.split('://')[1].split('/')[0]));
14
+ for (const expected of ['agents', 'skills', 'commands', 'rules', 'bundles']) {
15
+ assert.ok(schemes.has(expected), `missing resource bucket: ${expected}`);
16
+ }
17
+ for (const r of list.slice(0, 5)) {
18
+ assert.match(r.uri, /^kodelyth:\/\//);
19
+ assert.equal(r.mimeType, 'text/markdown');
20
+ }
21
+ });
22
+
23
+ test('resources.readResource returns body for a known agent', () => {
24
+ const out = RES.readResource('kodelyth://agents/debug-detective');
25
+ assert.ok(out.contents.length === 1);
26
+ assert.equal(out.contents[0].mimeType, 'text/markdown');
27
+ assert.match(out.contents[0].text, /debug-detective/);
28
+ });
29
+
30
+ test('resources.readResource rejects unknown URI scheme', () => {
31
+ assert.throws(() => RES.readResource('weird://foo/bar'));
32
+ });
33
+
34
+ test('resources.readResource rejects unknown name', () => {
35
+ assert.throws(() => RES.readResource('kodelyth://agents/no-such-agent'));
36
+ });
37
+
38
+ test('PROMPT_DEFINITIONS exposes routing-rule and overviews', () => {
39
+ const names = PROMPTS.PROMPT_DEFINITIONS.map(p => p.name);
40
+ for (const expected of [
41
+ 'routing-rule',
42
+ 'agents-overview',
43
+ 'skills-overview',
44
+ 'commands-overview',
45
+ 'handoff-chains',
46
+ 'devil-mode',
47
+ ]) {
48
+ assert.ok(names.includes(expected), `missing prompt: ${expected}`);
49
+ }
50
+ });
51
+
52
+ test('routing-rule prompt returns the full intent routing rule body', () => {
53
+ const out = PROMPTS.PROMPT_HANDLERS['routing-rule']();
54
+ assert.ok(out.messages.length === 1);
55
+ assert.equal(out.messages[0].role, 'user');
56
+ assert.match(out.messages[0].content.text, /Agent Intent Routing/);
57
+ });
58
+
59
+ test('agents-overview returns a summary of all agents', () => {
60
+ const out = PROMPTS.PROMPT_HANDLERS['agents-overview']();
61
+ const text = out.messages[0].content.text;
62
+ assert.match(text, /Kodelyth ECC — Agents/);
63
+ assert.match(text, /debug-detective/);
64
+ });
65
+
66
+ test('devil-mode prompt returns the parallel command body', () => {
67
+ const out = PROMPTS.PROMPT_HANDLERS['devil-mode']();
68
+ const text = out.messages[0].content.text;
69
+ assert.match(text, /devil-mode/i);
70
+ });
@@ -0,0 +1,159 @@
1
+ // Tests for scripts/mcp/tools.js — pure-function MCP tool handlers.
2
+ 'use strict';
3
+
4
+ const test = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+
10
+ // Isolate the memory store to a tmp dir so capture/recall don't pollute
11
+ // the user's real ~/.kodelyth/memory.
12
+ const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-mcp-'));
13
+ process.env.KODELYTH_MEMORY_DIR = TMP;
14
+
15
+ // Load AFTER setting env so the memory store picks up the tmp dir.
16
+ const T = require('../../scripts/mcp/tools');
17
+
18
+ function parseJSON(toolResult) {
19
+ const text = toolResult.content[0].text;
20
+ return JSON.parse(text);
21
+ }
22
+
23
+ test('TOOL_DEFINITIONS exposes the expected tool surface', () => {
24
+ const names = T.TOOL_DEFINITIONS.map(t => t.name);
25
+ for (const expected of [
26
+ 'route_intent',
27
+ 'list_agents',
28
+ 'list_skills',
29
+ 'list_commands',
30
+ 'list_rules',
31
+ 'list_bundles',
32
+ 'get_agent',
33
+ 'get_skill',
34
+ 'get_command',
35
+ 'get_rule',
36
+ 'get_bundle',
37
+ 'recall_memory',
38
+ 'capture_memory',
39
+ 'memory_stats',
40
+ 'catalog_stats',
41
+ 'audit_skill_match',
42
+ ]) {
43
+ assert.ok(names.includes(expected), `missing tool: ${expected}`);
44
+ }
45
+ // Every tool has a handler entry.
46
+ for (const t of T.TOOL_DEFINITIONS) {
47
+ assert.equal(typeof T.TOOL_HANDLERS[t.name], 'function', `${t.name} handler missing`);
48
+ assert.ok(t.description && t.description.length > 5, `${t.name} description too short`);
49
+ assert.ok(t.inputSchema && t.inputSchema.type === 'object', `${t.name} inputSchema malformed`);
50
+ }
51
+ });
52
+
53
+ test('route_intent rejects empty message', () => {
54
+ const r = T.tool_route_intent({});
55
+ assert.ok(r.isError, 'must error on missing message');
56
+ });
57
+
58
+ test('route_intent surfaces a relevant agent for a debug task', () => {
59
+ const r = T.tool_route_intent({ message: 'i have a hard intermittent bug in production' });
60
+ const data = parseJSON(r);
61
+ assert.ok(Array.isArray(data.suggestions));
62
+ assert.ok(data.suggestions.length > 0, 'should produce at least one suggestion');
63
+ });
64
+
65
+ test('route_intent honours top_k', () => {
66
+ const r = T.tool_route_intent({ message: 'review this code for security and quality issues', top_k: 2 });
67
+ const data = parseJSON(r);
68
+ assert.ok(data.suggestions.length <= 2);
69
+ });
70
+
71
+ test('list_agents returns the full catalog', () => {
72
+ const r = T.tool_list_agents();
73
+ const data = parseJSON(r);
74
+ assert.ok(data.count >= 60);
75
+ assert.ok(Array.isArray(data.agents) && data.agents.length === data.count);
76
+ });
77
+
78
+ test('get_agent returns body for known agent', () => {
79
+ const r = T.tool_get_agent({ name: 'debug-detective' });
80
+ assert.ok(!r.isError);
81
+ assert.match(r.content[0].text, /^# Agent: debug-detective/);
82
+ });
83
+
84
+ test('get_agent errors on unknown agent', () => {
85
+ const r = T.tool_get_agent({ name: 'no-such-agent' });
86
+ assert.ok(r.isError);
87
+ });
88
+
89
+ test('get_command tolerates leading slash', () => {
90
+ const r = T.tool_get_command({ name: '/code-review' });
91
+ assert.ok(!r.isError);
92
+ assert.match(r.content[0].text, /^# Command: \/code-review/);
93
+ });
94
+
95
+ test('get_rule returns the intent routing rule', () => {
96
+ const r = T.tool_get_rule({ name: 'agent-intent-routing' });
97
+ assert.ok(!r.isError);
98
+ assert.match(r.content[0].text, /Agent Intent Routing/);
99
+ });
100
+
101
+ test('get_bundle returns red-team cheat sheet', () => {
102
+ const r = T.tool_get_bundle({ name: 'red-team' });
103
+ assert.ok(!r.isError);
104
+ assert.ok(r.content[0].text.length > 100);
105
+ });
106
+
107
+ test('catalog_stats returns counts', () => {
108
+ const r = T.tool_catalog_stats();
109
+ const s = parseJSON(r);
110
+ assert.ok(s.agents > 0 && s.skills > 0 && s.commands > 0);
111
+ });
112
+
113
+ test('audit_skill_match suggests at least one skill for an observability task', () => {
114
+ const r = T.tool_audit_skill_match({ task: 'add structured logging and tracing to a fastapi service' });
115
+ const data = parseJSON(r);
116
+ assert.ok(Array.isArray(data.suggestions));
117
+ assert.ok(data.suggestions.length > 0, 'expected at least one skill suggestion');
118
+ });
119
+
120
+ test('capture_memory + recall_memory roundtrip in isolated tmp store', () => {
121
+ // Pre-seed a few unrelated memories so BM25 IDF can rise above the store's
122
+ // default minScore floor (with N=1 IDF stays ~0.29 which is filtered out).
123
+ for (const seed of [
124
+ { problem: 'fix a flaky CI build on github actions', approach: 'set timeout to 60s and retry once' },
125
+ { problem: 'wire up redis caching for product list', approach: 'use 60s ttl plus stale-while-revalidate' },
126
+ { problem: 'migrate postgres timestamps to utc', approach: 'add at::timestamptz coercion in alembic migration' },
127
+ ]) {
128
+ T.tool_capture_memory({ ...seed, tags: ['seed'], language: 'misc' });
129
+ }
130
+
131
+ const cap = T.tool_capture_memory({
132
+ problem: 'mcp test problem keyword splatzilla',
133
+ approach: 'documenting that capture_memory works end-to-end',
134
+ tags: ['mcp', 'test'],
135
+ language: 'javascript',
136
+ });
137
+ assert.ok(!cap.isError, 'capture must succeed');
138
+ const capData = parseJSON(cap);
139
+ assert.ok(capData.id && typeof capData.id === 'string');
140
+ assert.ok(capData.captured_at);
141
+
142
+ const rec = T.tool_recall_memory({ query: 'splatzilla', limit: 3 });
143
+ assert.ok(!rec.isError);
144
+ const recData = parseJSON(rec);
145
+ assert.ok(recData.count >= 1, 'should recall the memory we just captured');
146
+ assert.equal(recData.memories[0].problem, 'mcp test problem keyword splatzilla');
147
+ });
148
+
149
+ test('capture_memory rejects missing required fields', () => {
150
+ const r = T.tool_capture_memory({ problem: 'only problem, no approach' });
151
+ assert.ok(r.isError);
152
+ });
153
+
154
+ test('memory_stats reflects at least one stored memory', () => {
155
+ const r = T.tool_memory_stats();
156
+ assert.ok(!r.isError);
157
+ const s = parseJSON(r);
158
+ assert.ok(typeof s === 'object');
159
+ });
@@ -0,0 +1,181 @@
1
+ // Tests for scripts/replay/bundle.js — pure read/write of session bundles.
2
+ 'use strict';
3
+
4
+ const test = require('node:test');
5
+ const assert = require('node:assert/strict');
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const path = require('path');
9
+
10
+ const B = require('../../scripts/replay/bundle.js');
11
+
12
+ function tmp() {
13
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-bundle-'));
14
+ }
15
+
16
+ function seedSession(root, sessionName, workers) {
17
+ const sessionDir = path.join(root, sessionName);
18
+ fs.mkdirSync(sessionDir, { recursive: true });
19
+ for (const w of workers) {
20
+ const wdir = path.join(sessionDir, w.slug);
21
+ fs.mkdirSync(wdir);
22
+ fs.writeFileSync(path.join(wdir, 'task.md'), w.task || `# task for ${w.slug}`);
23
+ fs.writeFileSync(path.join(wdir, 'handoff.md'), w.handoff || `# handoff for ${w.slug}`);
24
+ fs.writeFileSync(path.join(wdir, 'status.md'), w.status || `# status for ${w.slug}\n- State: completed`);
25
+ }
26
+ return sessionDir;
27
+ }
28
+
29
+ // ── exportBundle ─────────────────────────────────────────────────────────────
30
+
31
+ test('exportBundle: rejects missing session dir', () => {
32
+ assert.throws(() => B.exportBundle({ sessionDir: '/no/such/path' }), /not found/);
33
+ });
34
+
35
+ test('exportBundle: gathers task/handoff/status per worker', () => {
36
+ const root = tmp();
37
+ const dir = seedSession(root, 'swarm-test', [
38
+ { slug: 'security-reviewer', task: 'audit', handoff: 'found 3 issues', status: 'completed' },
39
+ { slug: 'code-reviewer', task: 'review', handoff: 'all good', status: 'completed' },
40
+ ]);
41
+ const bundle = B.exportBundle({ sessionDir: dir, sessionName: 'swarm-test', meta: { task: 'audit oauth', harness: 'claude' } });
42
+ assert.equal(bundle.schema, B.BUNDLE_SCHEMA);
43
+ assert.equal(bundle.session, 'swarm-test');
44
+ assert.equal(bundle.workers.length, 2);
45
+ assert.equal(bundle.meta.task, 'audit oauth');
46
+ // Sorted by slug:
47
+ assert.deepEqual(bundle.workers.map(w => w.slug), ['code-reviewer', 'security-reviewer']);
48
+ // Content preserved:
49
+ const sec = bundle.workers.find(w => w.slug === 'security-reviewer');
50
+ assert.equal(sec.task, 'audit');
51
+ assert.equal(sec.handoff, 'found 3 issues');
52
+ });
53
+
54
+ test('exportBundle: handles empty session dir gracefully', () => {
55
+ const root = tmp();
56
+ const empty = path.join(root, 'empty');
57
+ fs.mkdirSync(empty);
58
+ const bundle = B.exportBundle({ sessionDir: empty });
59
+ assert.equal(bundle.workers.length, 0);
60
+ });
61
+
62
+ // ── writeBundle / readBundle round-trip ──────────────────────────────────────
63
+
64
+ test('writeBundle + readBundle: round-trips losslessly', () => {
65
+ const root = tmp();
66
+ const dir = seedSession(root, 's1', [{ slug: 'a' }, { slug: 'b' }]);
67
+ const bundle = B.exportBundle({ sessionDir: dir, meta: { task: 'foo' } });
68
+ const out = path.join(root, 'out.json');
69
+ B.writeBundle(bundle, out);
70
+
71
+ const reread = B.readBundle(out);
72
+ assert.equal(reread.session, bundle.session);
73
+ assert.equal(reread.workers.length, 2);
74
+ assert.equal(reread.meta.task, 'foo');
75
+ });
76
+
77
+ test('writeBundle: rejects bad schema', () => {
78
+ assert.throws(() => B.writeBundle({ schema: 'bogus' }, '/tmp/x.json'), /invalid schema/);
79
+ });
80
+
81
+ test('readBundle: rejects unknown schema', () => {
82
+ const root = tmp();
83
+ const out = path.join(root, 'bad.json');
84
+ fs.writeFileSync(out, JSON.stringify({ schema: 'not.kodelyth/v0', session: 'x', workers: [] }));
85
+ assert.throws(() => B.readBundle(out), /unsupported schema/);
86
+ });
87
+
88
+ test('readBundle: rejects empty workers', () => {
89
+ const root = tmp();
90
+ const out = path.join(root, 'empty.json');
91
+ fs.writeFileSync(out, JSON.stringify({ schema: B.BUNDLE_SCHEMA, session: 'x', workers: [] }));
92
+ assert.throws(() => B.readBundle(out), /non-empty array/);
93
+ });
94
+
95
+ // ── importBundle ─────────────────────────────────────────────────────────────
96
+
97
+ test('importBundle: restores worker files into target dir', () => {
98
+ const root = tmp();
99
+ const dir = seedSession(root, 'src', [
100
+ { slug: 'a', task: 'task-a', handoff: 'handoff-a', status: 'status-a' },
101
+ ]);
102
+ const bundle = B.exportBundle({ sessionDir: dir });
103
+
104
+ const target = path.join(root, 'restored');
105
+ const out = B.importBundle(bundle, { targetDir: target });
106
+ assert.equal(out.targetDir, target);
107
+ assert.deepEqual(out.workers, ['a']);
108
+
109
+ assert.equal(fs.readFileSync(path.join(target, 'a', 'task.md'), 'utf8'), 'task-a');
110
+ assert.equal(fs.readFileSync(path.join(target, 'a', 'handoff.md'), 'utf8'), 'handoff-a');
111
+ assert.equal(fs.readFileSync(path.join(target, 'a', 'status.md'), 'utf8'), 'status-a');
112
+ });
113
+
114
+ test('importBundle: refuses to overwrite without flag', () => {
115
+ const root = tmp();
116
+ const dir = seedSession(root, 'src', [{ slug: 'a' }]);
117
+ const bundle = B.exportBundle({ sessionDir: dir });
118
+ const target = path.join(root, 'exists');
119
+ fs.mkdirSync(target);
120
+ assert.throws(() => B.importBundle(bundle, { targetDir: target }), /already exists/);
121
+ });
122
+
123
+ test('importBundle: overwrite=true replaces existing dir', () => {
124
+ const root = tmp();
125
+ const dir = seedSession(root, 'src', [{ slug: 'a', task: 'fresh' }]);
126
+ const bundle = B.exportBundle({ sessionDir: dir });
127
+ const target = path.join(root, 'exists');
128
+ fs.mkdirSync(target);
129
+ fs.writeFileSync(path.join(target, 'stale.txt'), 'leftover');
130
+
131
+ B.importBundle(bundle, { targetDir: target, overwrite: true });
132
+ assert.equal(fs.existsSync(path.join(target, 'stale.txt')), false);
133
+ assert.equal(fs.readFileSync(path.join(target, 'a', 'task.md'), 'utf8'), 'fresh');
134
+ });
135
+
136
+ // ── diffBundles ──────────────────────────────────────────────────────────────
137
+
138
+ test('diffBundles: detects handoff changes', () => {
139
+ const root = tmp();
140
+ const a = B.exportBundle({ sessionDir: seedSession(root, 'a', [
141
+ { slug: 'x', handoff: 'aaa' }, { slug: 'y', handoff: 'shared' },
142
+ ]) });
143
+ const b = B.exportBundle({ sessionDir: seedSession(root, 'b', [
144
+ { slug: 'x', handoff: 'bbb' }, { slug: 'y', handoff: 'shared' },
145
+ ]) });
146
+ const d = B.diffBundles(a, b);
147
+ const x = d.find(r => r.slug === 'x');
148
+ const y = d.find(r => r.slug === 'y');
149
+ assert.equal(x.handoff_changed, true);
150
+ assert.equal(y.handoff_changed, false);
151
+ });
152
+
153
+ test('diffBundles: surfaces workers that exist on only one side', () => {
154
+ const root = tmp();
155
+ const a = B.exportBundle({ sessionDir: seedSession(root, 'a', [
156
+ { slug: 'only-a' }, { slug: 'shared' },
157
+ ]) });
158
+ const b = B.exportBundle({ sessionDir: seedSession(root, 'b', [
159
+ { slug: 'shared' }, { slug: 'only-b' },
160
+ ]) });
161
+ const d = B.diffBundles(a, b);
162
+ assert.equal(d.find(r => r.slug === 'only-a').in_a, true);
163
+ assert.equal(d.find(r => r.slug === 'only-a').in_b, false);
164
+ assert.equal(d.find(r => r.slug === 'only-b').in_b, true);
165
+ assert.equal(d.find(r => r.slug === 'only-b').in_a, false);
166
+ });
167
+
168
+ // ── nextReplayName ───────────────────────────────────────────────────────────
169
+
170
+ test('nextReplayName: generates -replay-1 for fresh origin', () => {
171
+ assert.equal(B.nextReplayName('swarm-2026-05-10-4a'), 'swarm-2026-05-10-4a-replay-1');
172
+ });
173
+
174
+ test('nextReplayName: skips taken numbers', () => {
175
+ const taken = new Set(['swarm-x-replay-1', 'swarm-x-replay-2']);
176
+ assert.equal(B.nextReplayName('swarm-x', taken), 'swarm-x-replay-3');
177
+ });
178
+
179
+ test('nextReplayName: strips existing -replay-N suffix to find canonical base', () => {
180
+ assert.equal(B.nextReplayName('swarm-x-replay-3'), 'swarm-x-replay-1');
181
+ });
@@ -0,0 +1,131 @@
1
+ // Tests for scripts/replay/replay.js — replay engine.
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 R = require('../../scripts/replay/replay.js');
9
+ const B = require('../../scripts/replay/bundle.js');
10
+ const O = require('../../scripts/lib/tmux-worktree-orchestrator.js');
11
+
12
+ const FAKE_BUNDLE = {
13
+ schema: B.BUNDLE_SCHEMA,
14
+ session: 'swarm-2026-05-10-4a',
15
+ exported_at: '2026-05-10T17:00:00Z',
16
+ exported_by: 'kodelyth-ecc@1.7.0',
17
+ meta: {
18
+ task: 'audit oauth flow for security regressions',
19
+ agents: ['security-reviewer', 'code-reviewer'],
20
+ harness: 'echo',
21
+ base_ref: 'HEAD',
22
+ },
23
+ workers: [
24
+ { slug: 'security-reviewer', task: '# task', handoff: '# handoff', status: '# status' },
25
+ { slug: 'code-reviewer', task: '# task', handoff: '# handoff', status: '# status' },
26
+ ],
27
+ };
28
+
29
+ // ── extractTaskFromBundle ────────────────────────────────────────────────────
30
+
31
+ test('extractTaskFromBundle: prefers meta.task when present', () => {
32
+ const t = R.extractTaskFromBundle(FAKE_BUNDLE);
33
+ assert.equal(t, 'audit oauth flow for security regressions');
34
+ });
35
+
36
+ test('extractTaskFromBundle: falls back to parsing first worker task.md (Shared Task)', () => {
37
+ const bundle = {
38
+ schema: B.BUNDLE_SCHEMA,
39
+ session: 's',
40
+ workers: [{
41
+ slug: 'a',
42
+ task: '# a — swarm task\n\nblah\n\n## Shared Task\n\nrefactor payments\n\n## Required handoff sections\n1. Summary',
43
+ }],
44
+ meta: {},
45
+ };
46
+ assert.equal(R.extractTaskFromBundle(bundle), 'refactor payments');
47
+ });
48
+
49
+ test('extractTaskFromBundle: falls back to orchestrator Objective block', () => {
50
+ const bundle = {
51
+ schema: B.BUNDLE_SCHEMA,
52
+ session: 's',
53
+ workers: [{
54
+ slug: 'a',
55
+ task: '# Worker Task\n\n## Objective\nfix the bug\n\n## Completion\nDo not spawn',
56
+ }],
57
+ meta: {},
58
+ };
59
+ assert.equal(R.extractTaskFromBundle(bundle), 'fix the bug');
60
+ });
61
+
62
+ test('extractTaskFromBundle: throws if no task can be recovered', () => {
63
+ const bundle = { schema: B.BUNDLE_SCHEMA, session: 's', workers: [], meta: {} };
64
+ assert.throws(() => R.extractTaskFromBundle(bundle), /cannot extract task/);
65
+ });
66
+
67
+ // ── buildReplayPlanConfig ────────────────────────────────────────────────────
68
+
69
+ test('buildReplayPlanConfig: defaults to bundle.meta values', () => {
70
+ const cfg = R.buildReplayPlanConfig(FAKE_BUNDLE, { repoRoot: process.cwd() });
71
+ assert.equal(cfg._meta.task, 'audit oauth flow for security regressions');
72
+ assert.deepEqual(cfg._meta.pickedAgents, ['security-reviewer', 'code-reviewer']);
73
+ assert.match(cfg.sessionName, /^swarm-2026-05-10-4a-replay-1$/);
74
+ assert.equal(cfg.baseRef, 'HEAD');
75
+ });
76
+
77
+ test('buildReplayPlanConfig: --harness override flows to plan', () => {
78
+ const cfg = R.buildReplayPlanConfig(FAKE_BUNDLE, { repoRoot: process.cwd(), harness: 'echo' });
79
+ // Harness override is preserved in the planConfig._meta.
80
+ assert.equal(cfg._meta.harness, 'echo');
81
+ });
82
+
83
+ test('buildReplayPlanConfig: --agents override replaces bundle.meta.agents', () => {
84
+ const cfg = R.buildReplayPlanConfig(FAKE_BUNDLE, {
85
+ repoRoot: process.cwd(),
86
+ agents: ['supply-chain-auditor', 'prompt-injection-hunter'],
87
+ });
88
+ assert.deepEqual(cfg._meta.pickedAgents, ['supply-chain-auditor', 'prompt-injection-hunter']);
89
+ });
90
+
91
+ test('buildReplayPlanConfig: --base-ref override flows to plan', () => {
92
+ const cfg = R.buildReplayPlanConfig(FAKE_BUNDLE, { repoRoot: process.cwd(), baseRef: 'main' });
93
+ assert.equal(cfg.baseRef, 'main');
94
+ });
95
+
96
+ test('buildReplayPlanConfig: --session override wins over auto-naming', () => {
97
+ const cfg = R.buildReplayPlanConfig(FAKE_BUNDLE, {
98
+ repoRoot: process.cwd(),
99
+ sessionName: 'my-replay',
100
+ });
101
+ assert.equal(cfg.sessionName, 'my-replay');
102
+ });
103
+
104
+ test('buildReplayPlanConfig: takenSessions skips conflicting names', () => {
105
+ const taken = new Set(['swarm-2026-05-10-4a-replay-1', 'swarm-2026-05-10-4a-replay-2']);
106
+ const cfg = R.buildReplayPlanConfig(FAKE_BUNDLE, { repoRoot: process.cwd(), takenSessions: taken });
107
+ assert.equal(cfg.sessionName, 'swarm-2026-05-10-4a-replay-3');
108
+ });
109
+
110
+ test('buildReplayPlanConfig: produces a plan-config the orchestrator accepts', () => {
111
+ const cfg = R.buildReplayPlanConfig(FAKE_BUNDLE, { repoRoot: process.cwd() });
112
+ // Verify orchestrator can build a real plan from it.
113
+ const plan = O.buildOrchestrationPlan(cfg);
114
+ assert.equal(plan.workerPlans.length, 2);
115
+ assert.ok(plan.workerPlans.every(w => w.task.includes('audit oauth flow')));
116
+ assert.match(plan.sessionName, /-replay-/);
117
+ });
118
+
119
+ test('buildReplayPlanConfig: falls back to worker slugs when no meta.agents AND no override', () => {
120
+ const minimalBundle = {
121
+ schema: B.BUNDLE_SCHEMA,
122
+ session: 'minimal',
123
+ workers: [
124
+ { slug: 'a', task: '## Shared Task\n\ndo X\n\n## Required handoff sections\n' },
125
+ { slug: 'b', task: '## Shared Task\n\ndo X\n\n## Required handoff sections\n' },
126
+ ],
127
+ meta: {},
128
+ };
129
+ const cfg = R.buildReplayPlanConfig(minimalBundle, { repoRoot: process.cwd(), harness: 'echo' });
130
+ assert.deepEqual(cfg._meta.pickedAgents, ['a', 'b']);
131
+ });